关于block的那些事(参考大神博客+自己想法)

来源:互联网 发布:魔音软件电脑版 编辑:程序博客网 时间:2024/05/29 17:51

理解“块”这一个概念

块可以实现闭包。这项语言特性是作为“扩展”而加入GCC编译器中的,从技术上讲,这是个C语言层面的特性,因此,只要有支持此特性的编译器,以及能执行块的运行期组件,就可以在C、C++,OC,OC++代码中使用

块的基本知识

块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享一个范围内的东西。块用“^”符号来表示,后面跟着一对花括号,括号里面是块的实现代码,例如,下面就是一个简单的块:

^{

    //Block implementation here

}

块其实就是一个值,而且有其相关类型。与int、float或者OC对象一样,也可以吧块赋值给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。下面列出的这个块很简单,没有参数,也不返回值:

void (^someBlock)() = ^{

    //Block implementation here

}

下面就是有两个参数并且有返回值的:

int (^addBlock)(int a,int b) = ^(int a,int b){

    //Block implementation here

    return a + b;

};

我们可以像使用C语言的函数一样使用:

int add = addBlock(4,5);

块的强大之处是:在声明它的范围内,所有的变量都可以为其所捕获。也就是说,那个范围的全部变量,在块里依然可用,比如,下面这段代码所定义的快,就使用了块外的变量:

int addition = 6;

int (^addBlock)(int a,int b) = ^(int a,int b){

    //Block implementation here

    return a + b + addition;

};

int add = addBlock(4,5);

默认情况下,为块捕获的变量,是不可以在块里面修改的,在本例中,假如块内的代码改动了addition变量的值,那么编译器就会报错,不过声明变量的时候可以加上__block修饰符,这样就可以在块内修改了。Block不允许修改外部变量的值Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。

我们可以打印下内存地址来进行验证:

__block int a = 0;

NSLog(@"定义前:%p", &a);        //栈区

void (^foo)(void) = ^{

    a = 1;

    NSLog(@"block内部:%p", &a);    //堆区

};

NSLog(@"定义后:%p", &a);        //堆区

foo();

2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义前:0x16fda86f8

2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义后:0x155b22fc8

2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] block内部: 0x155b22fc8

“定义后”和“block内部”两者的内存地址是一样的,我们都知道 block 内部的变量会被 copy 到堆区,“block内部”打印的是堆地址,因而也就可以知道,“定义后”打印的也是堆的地址。

那么如何证明“block内部”打印的是堆地址?

把三个16进制的内存地址转成10进制就是:

定义后前:6171559672

block内部:5732708296

定义后后:5732708296

中间相差438851376个字节,也就是 418.5M 的空间,因为堆地址要小于栈地址,又因为iOS中一个进程的栈区内存只有1M,Mac也只有8M,显然a已经是在堆区了。

这也证实了:a 在定义前是栈区,但只要进入了 block 区域,就变成了堆区。这才是 __block 关键字的真正作用。

理解到这是因为堆栈地址的变更,而非所谓的“写操作生效”,这一点至关重要,要不然你如何解释下面这个现象:

以下代码编译可以通过,并且在block中成功将a的从Tom修改为Jerry。

NSMutableString *a = [NSMutableString stringWithString:@"Tom"];

NSLog(@"\n 定以前:------------------------------------\n\

a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);              //a在栈区

void (^foo)(void) = ^{

    a.string = @"Jerry";

    NSLog(@"\n block内部:------------------------------------\n\

    a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);              //a在栈区

    a = [NSMutableString stringWithString:@"William"];

};

foo();

NSLog(@"\n 定以后:------------------------------------\n\

a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);

定以前:------------------------------------

a指向的堆中地址:0x7fdd8aa01260;a在栈中的指针地址:0x7fff5c5e4a58

block内部:------------------------------------

a指向的堆中地址:0x7fdd8aa01260;a在栈中的指针地址:0x7fdd88c180e0

定以后:------------------------------------

a指向的堆中地址:0x7fdd8aa01260;a在栈中的指针地址:0x7fff5c5e4a58

我们还能经常看到“内联块” 的用法:

NSArray *array = [NSArray array];

[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];

这种常见的编码习惯也是可以看出来块为何如此有用。在OC中引入块这个特性之前,想要编出同样功能的代码,就必须传入函数指针或者是选择子的名称,这样就会再写几行代码了,而且还会令方法变得有些松散,与之相反,若声明内联形式的块,就能把业务逻辑都放在一起了。

如果块所捕获的变量就是对象类型,那么就睡自动保存它,系统在释放这个块的时候,也会将其一并释放。这就引出了一个与块有关的问题。块本身可视为对象。实际上,在其他OC对象所能响应的选择子中,有很多是块也可以响应的。而最重要之处则在于,块本身也和其他对象一样有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获是所执行的保留操作。

如果将块定义在OC类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量。块总能修改实例变量,所以在声明时无需加__block。不过,如果通过读取或者写入操作捕获了实例变量,那么也会自动把self变量一并捕获了,因为实例变量与self所指代的实例关联在一起。

@property (nonatomic,copy) NSString *value;

@end

@implementation ViewController

- (void)viewDidLoad

{

    [super viewDidLoad];

    void (^someBlock)() = ^{

    _value = @"someBlock";

    };

}

@end

在这种情况下,self变量就指向此block。由于在块内没有明确的使用self变量,所以很容易忘记self变量其实以为块所捕获了。直接访问实例变量和通过self来访问是等效的。然而一定要记住:self也是一个对象,因而块在捕获它时也会将其保留。如果self所指代的那个对象同时保留了块,那么就会出现保留环,在这样的情况下,我们一般会采用weak-strong dance方法(在ARC的情况下)来解决这个问题。

块的内部结构

每个OC对象都占据着某个内存区域。因为实例变量的个数及对象所包含的关联数据互不相同,所以每个对象所占的内存区域也有大小之分。块本身也就是对象,在存放块对象内存区域中,首个变量是指向Class对象的指针,该指针叫做isa,其他结构如图。



在内存布局中,最重要的就是invoke变量,这是一个函数指针,指向块的实现代码。函数原型至少要接受一个void*型 的参数,此参数代表块。descriptor变量是指向结构体指针,每个块里都包含这个结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃对象时运行,其中会执行一些操作,比方说,前者要保留捕获的对象,后者将之释放块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量后面,捕获了多少个变量,就要占据多少内存空间,请注意,拷贝并不是对象的本身,而是指向这些对象的指针变量。

栈块和堆块

定义块的时候,其所站的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。例如,下面这段代码就是有危险的:

void (^block)();

if (<#condition#>) {

    block = ^{

        NSLog(@"BlockA");

       };

}else{

    block = ^{

    NSLog(@"BlockB");

    };

}

定义在if和else语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只能保证在对应的if和else语句范围内有效。这样写出来的代码可以编辑,但是运行起来有时正确有时不正确,若编译器没有覆写待执行的块,程序正常运行,若覆写,程序崩溃。为解决这个问题,可给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆 了。拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到了堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。如果不再使用这个块,那就应将其释放,在ARC下会自动释放,而在手动管理应用计数则需要自己来调用release方法。当应用计数降为0后,“分配到堆上的块”就会被系统回收。在“栈上的块”无须释放,占内存本身就会自动回收。

void (^block)();

if (<#condition#>) {

    block = [^{

    NSLog(@"BlockA");

    } copy];

}else{

    block = [^{

    NSLog(@"BlockB");

    } copy];

}

这样就能够变得安全了,如果手动管理引用计数,那么在用完块之后还需要将其释放。

站在巨人的肩膀上,同时有个人的想法……

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 回奶胀痛的厉害怎么办 淡水龟的壳变软了怎么办 棕色的泰迪变白怎么办 大班教案泥石流来了怎么办 鸟类的嘴巴坏了怎么办 四川麦蚊子咬了怎么办 脸上被晒脱皮了怎么办 额头被晒脱皮了怎么办 脸黑一块白一块怎么办 小乌龟的壳软了怎么办 把田螺后面吃了怎么办 微生物生态菌群异常怎么办 怀孕初期感染了动物病毒怎么办? 金龙和银龙打架怎么办 海水缸盐度高了怎么办 洗空调洗坏了怎么办 老师是条青花鱼怎么办 吃了带鱼和南瓜怎么办 苹果平板ad忘了怎么办 小米6进海水了怎么办 小米手机掉海水里怎么办 苹果7进海水了怎么办 7p手机进海水怎么办 育海参苗出现红细菌怎么办 苹果手机掉进厕所怎么办 2个月的婴儿肺炎怎么办 甜甜圈珊瑚脱骨怎么办 宝宝吃了生物球怎么办 狗尾巴被剪掉了怎么办 魟鱼尾巴刺了怎么办 狗咬过了24小时怎么办 狗咬超过24小时怎么办 狗咬超过48小时怎么办 狗咬超过72小时怎么办 狗抓超过24小时怎么办 龙须树叶子发黄怎么办? 车被广告牌砸了怎么办 开花店压力好大怎么办 快成熟葡萄软果怎么办 木本叶长白斑点了怎么办 天猫卖家超过72小时未发货怎么办