时隔一年,再次读 《Objective-C 高级编程》,看到 block 一章,这一次从头至尾的跟着编译了一次,理清楚了很多之前不理解的地方,但是也同时多出了许多疑问。本文是在和学渣裙的朋友们分享以后的梳理笔记,有问题欢迎指出,如果能解决最后的几个小疑问,就更好了。
环境信息
macOS 10.12.1
Xcode 8.2.1
iOS 10.12
Block 编译后,有两个最为重要的部分,impl 结构体 与 desc 结构体指针。我们从最为简单基础的开始:
int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... // 定义一个参数列表与返回值均为空的 block dispatch_block_t block = ^{ // 仅输出一句话 NSLog(@"123"); }; // 调用 block(); } return 0; }
使用 clang -rewrite-objc xxx.m
命令,编译后(已删除一些影响阅读的字符,用 xxx 代替):
// block 结构体 struct __main_block_impl_0 { struct __block_impl impl; // 实现 struct __main_block_desc_0* Desc; // 描述 // 在定义 block 时,所调用的 block 初始化方法 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; // block 的类型(之后会谈到) impl.Flags = flags; impl.FuncPtr = fp; // block 实现编译后的函数指针 Desc = desc; // 描述信息 } }; // block 的描述 static struct __main_block_desc_0 { size_t reserved; size_t Block_size; // block 所占的内存大小 } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; // block 描述的初始化方法,可以看出这里的大小计算,仅仅是进行了 sizeof // block 实现编译过后的函数 // 即在 block 初始化方法中,赋值给 impl.FuncPtr 的函数指针 // 参数 cself 是 __main_block_impl_0 类型,即与 block 类型相同,其实这里的参数,本身就是 block 自己 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { // 输出 NSLog((NSString *)&__NSConstantStringImpl__var_xxx_main_c44db5_mi_0); } // main 函数 int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; // block 的定义 // 可以看出,在 block 定义的时候,就调用了 __main_block_impl_0,即 block 的构造方法 // 传的参数分别为 __main_block_func_0,即 block 对应的编译后的实现函数 // __main_block_desc_0_DATA,即 block 描述 dispatch_block_t block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); // block 的调用 // 这里也可以看出,block 的调用即是调用了,初始化时拿到的 FuncPtr 函数指针 // FuncPtr 函数有一个参数,即传入的 block 自身 ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); } return 0; }
通过编译后得到的 block 结构体,能大致看出,在没有引用外部变量的 block 是这样的:
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
此时的内存结构如下:
在 block 调用构造方法时,编译器已经自动给 isa 指针赋了初值。我们知道,isa 指针其实很形象,就称作 is a
,在 OC 中表达了对象是什么类型,类所属哪个元类,其实 block 也是对象,所以它的 isa 指针也是说明它是什么的。如果直接打印 block,则可看到以下三种情况:
但是你会发现,如果直接打印上面我们所写的 block,输出的是 __NSGlobalBlock__
类型,而我们看到的编译代码,明明是 stack 的。这是因为 block 的存储区域,与定义在什么位置、是否引用外部变量、是否作为范围值、是被哪种类型的变量所接收等等情况相关,这个会在下一小节谈到。
之前介绍了一个空(并为引用变量)的 block,下面来看一个稍微复杂一点的:
int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... // 定义局部变量 a int a = 10; // 定义 block dispatch_block_t block = ^{ // 输出 a 变量 NSLog(@"%d", a); }; // 调用 block block(); } return 0; }
在学习 block 的基础知识时,就知道,此时如果在 block 定义之后,去修改 a
的值,block 中的输出依然不会改变,我们来看一下为什么。
编译文件:
// 下面仅标注了有变化的变量 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int a; // 在 block 结构体中,多了一个名为 a 的变量 // block 构造方法也多了一个 _a 参数 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; // 描述依然没有变,size 是直接计算的 __main_block_impl_0 static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { // 在 block 的实现函数中,访问 block 结构体中的 a 变量,并且编译器在此还说明了是 bound by copy,即值拷贝 int a = __cself->a; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_xxx_0, a); }
从编译代码可以看出,在 block 定义时,就传入了 block 内部需要用到的 a
变量的 值
,而 并不是引用
,所以即使在 block 定义之后, a
变量怎么变,之前 block 所有的 a
的瞬时值,是没有变化的。
此时,block 的内存结构为:
多出了捕获的变量 a
的存储空间,并且,捕获的变量会接在 Desc 内存后面。
__block
修饰的外部变量
如果没有 __block
修饰,除了在 block 定义之后,就不能拿到变量最新的值以外,我们还不能对变量进行重新赋值(如果是堆上的内存,就是改变地址,即 NSMutableArray
是可以 addObject
的,只是不能 array = @[]
)。那么,想要解决这两个问题,我们就需要引入 __block
修饰符:
int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... // 定义变量 a,并使用 __block 修饰 __block int a = 10; dispatch_block_t block = ^{ // 输出 a NSLog(@"%d", a); // 输出 100 // 在 block 内部对 a 重新赋值 a = 50; }; // 在 block 定义后,对 a 重新赋值 a = 100; // 调用 block block(); // 输出 a NSLog(@"%d", a); // 输出 50 } return 0; }
这一次,编译后的代码变得很复杂了:
// block 结构体 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; // 比起没有被 __block 修饰的变量(编译后,block 中是 int a),这里 block 却不是简单的拿到 a 地址,即 int *a,而是一个类型为 __Block_byref_a_0 的结构体指针 __Block_byref_a_0 *a; // by ref // 构造方法多出的参数也变成了,__Block_byref_a_0 结构体指针,并且 a 的值是 a->__forwarding,这个 __forwarding 指针的作用,会在之后介绍 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; // a 变量的结构体定义 struct __Block_byref_a_0 { void *__isa; // isa 指针 __Block_byref_a_0 *__forwarding; // 类型与 a 变量一模一样的 __forwarding 结构体指针 int __flags; int __size; int a; // a 真正的值 }; // block 描述 static struct __main_block_desc_0 { size_t reserved; size_t Block_size; // 这是比起以前,多出的两个函数指针,一个 copy,一个 dispose // 这也是 block 中尤为重要的两个函数 // copy 负责将 block 复制到堆 // dispose 负责在 block 释放时,释放 block 所持有的内存 void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; // block 实现 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { // 访问到 block 中的 a 结构体指针变量 __Block_byref_a_0 *a = __cself->a; // bound by ref // 输出 // 可以看到,这里访问的 a 的值,是通过 __forwarding 指针访问的,包括之后的赋值,也是用的 __forwarding NSLog((NSString *)&__NSConstantStringImpl__var_folders_xxx_0, (a->__forwarding->a)); // 将 a 的值重新赋为 50 (a->__forwarding->a) = 50; } // copy 函数 static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { // 使用 _Block_object_assign 函数进行拷贝 _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/); } // dispose 函数 static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/); } int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; // 初始化 a 的结构体变量 // 传入的参数分别是: // isa: void *0; // __forwarding: &a,即 a 结构体变量的地址 // __flags: 0 // __size: sizeof(结构体) // a: 10,即 a 的值 __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10}; // block 定义 dispatch_block_t block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344)); // 对 a 变量赋值 (a.__forwarding->a) = 100; // block 的调用 ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); // 输出 a NSLog((NSString *)&__NSConstantStringImpl__var_folders_xxx_1, (a.__forwarding->a)); } return 0; }
经过了这一坨编译后代码的理解,现在已经脑子已经是一团糨糊了,莫名多出的结构体、 __forwarding
指针,copy 与 dispose 函数无一不在提高理解的门槛。代码都看得懂,但是就是不知道这样做的目的,下面我们便来将多出的东西,重新整理一次,然后注意理解:
a
被 __block
修饰以后,就变成了 __Block_byref_a_0
结构体? __forwarding
指针是什么? a
都变成了 a->__forwarding->a
? __block int a = 10;
进行使用,指针会怎样指向?
从 main 函数中 _Block_byref_a_0
的初始化可以看出,给 __forwarding
指针赋的值就是 (__Block_byref_a_0 *)&a
,所以 __forwarding
指针是同样是指向 a
结构体变量本身的。
在全局区或者是栈上的 block,我们并不能控制它的释放时机,但是如果 block 在堆中,就可以由我们来控制了。所以,大多数情况下,比如将 block 作为回调方法等时候,block 一般都是在堆上的。
那么,block 是如何拷贝到堆上的呢?这就和 copy 函数有关了。在 ARC 环境下,如果将 block 声明为:
@property (copy) block; @property (strong) block;
在赋值时,其实都会调用 Block_copy() 函数,将栈上的 block 拷贝到堆中,此时,block 中所持有的变量就都在堆中了,我们通过管理 block 的生命周期,就能间接管理到 block 持有的变量的生命周期。
那么 block 何时会 copy 到堆上呢?是显式,还是隐式?
copy
和 strong
修饰; [block copy]
; __strong
修饰的变量时。因为 ARC 下, __strong
是缺省值,所以只要不是显式标记了 __unsafe_unretained
或 __weak
,block 均会被拷贝到堆上; __forwarding
指针存在的意义
在阅读编译代码时可以发现,block 在读写被 __block
标记的变量时,均使用 var->__forwarding->var
来访问。 var
是指针能理解,因为它肯定是对临时变量进行地址引用,要不然也不能获得最新的值。但是为什么要在中间加一个 __forwarding
呢?而且 __forwarding
指针还是指向的自己。
来看一个例子(ARC 下):
int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... __block int a = 10; NSLog(@"1. block 定义之前 a 的地址 : %p", &a); dispatch_block_t __unsafe_unretained block = ^{ a = 100; NSLog(@"2. 调用 block 时 a 的地址 : %p", &a); }; NSLog(@"3. block 定义之后 a 的地址 : %p", &a); dispatch_block_t heapBlock = block; NSLog(@"4. block 拷贝到堆上 a 的地址 : %p", &a); block(); } return 0; }
上面的例子中,一共输出了四次 a
的地址。其中,1 和 3 的地址是一样的,2 和 4 的地址是一样。
这里我还对 block 特地标记了 __unsafe_unretained
,防止在定义赋值的时候,就拷贝到堆。而这之后的 heapBlock
则是因为被 __strong
修饰所以将 block 拷贝到了堆。
在 1、3 输出的时候, a
还在栈上,此时的 block 内存为:
而在 block 拷贝到堆上以后, __forwarding
指针则指向堆上的 a
结构体,所以,内存变成了这样:
这样就保证了栈上和堆上的 block,都能访问到同一个 a 变量,这也是 __forwarding
指针的作用。
之前谈到 block 根据存储位置不同,可分为三种, 堆 、 栈 、 全局区 。那么这三种 block 是怎样的呢?
__NSStackBlock__
:block 被定义为临时变量,并且引用了外部变量; __NSMallocBlock__
:调用了 copy 函数,被拷贝到堆上的 block; __NSGlobalBlock__
:定义为全局变量,或者临时变量但是没有引用外部变量的 block。 __block
变量的引用
在 block 引用使用 __block
修饰的外部变量时,编译器去针对这个外部变量生成了结构体,比如我们上面谈到的 __Block_byref_a_0
结构体。
之所以这样做,也是为了能在多个 block 引用时,能够给对 __Block_byref_a_0
进行复用。所以,当多个 block 引用该变量时,并不会重复生成结构体,而是对该结构体内存进行持有,在 block 销毁,调用 dispose 时,对内存进行释放。
OC 中的循环引用是一个老生常谈的问题,其中最容易出现循环引用的地方,就是 block。都知道,出现循环引用的原因,是因为两个变量的相互持有,导致谁也无法释放。断开循环引用链,最常见的方式是:
根据这两种解决方案,block 解决循环引用对应着两种方式:
__weak
或者 __unsafe_unretained
修饰 block 内部要用到的变量。 __weak typeof(self) weakSelf = self; self.block = ^{ NSLog(@"%@", weakSelf); }; self.block();
__block
修饰变量,然后在 block 调用完毕后,在 block 内部对变量置空。 __block typeof(self) blockSelf = self; self.block = ^{ NSLog(@"%@", blockSelf); blockSelf = nil; }; self.block();
使用第二种方式有一个弊端,就是必须要保证 block 会调用,这样才有机会断开循环引用,否则无法解决问题。当然,也有优点,即可以控制另一方的释放时机,保证不调用,就不会释放。
__weak
与 __strong
通常我们能看到以下写法:
__weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(self) strongSelf = weakSelf; NSLog(@"%@", strongSelf); }; self.block();
__weak
的作用我们刚才已经提到了,但是在 block 内部又使用 __strong
标记是为什么?这样会造成循环引用吗?我们来看看 block 实现编译过后的代码:
static void __Test__init_block_func_0(struct __Test__init_block_impl_0 *__cself) { // 这里变成了值拷贝,而不是指针引用 typeof (self) weakSelf = __cself->weakSelf; // bound by copy // 虽然是 strong 的,但是是在 block 调用时,才将 self 的值拷贝赋值给临时变量 weakSelf,之后被 strongSelf 引用 // 根据 ARC 的规则,使用 __strong 修饰的变量,出作用域以后,会插入 release 语句,所以在 block 实现结束后,strongSelf 会释放,并不会造成循环引用 __attribute__((objc_ownership(strong))) Test * strongSelf = weakSelf; NSLog((NSString *)&__NSConstantStringImpl__var_xxx_0, strongSelf); }
也是因为 ARC 对 __strong
修饰的变量,出作用域才插入 release 的机制,我们可以知道,之所以在 block 内部使用 __strong
修饰变量,是因为防止在 block 执行过程中,变量被释放的情况。
再看下面这段代码:
- (void)testBlock { __weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(self) strongSelf = weakSelf; // 在 block 实现中,调用了 run 方法,而 run 方法中,又用到了 self [strongSelf run]; }; self.block(); } - (void)run { NSLog(@"%@", self); }
之前有人问到这个问题,说如果 block 中调用的方法又用到了 self,会造成循环引用吗?想想如果会造成,那岂不是很可怕,好像一直这样写,都没什么问题。那这是为什么呢?
别忘了 OC 是消息机制,发送完消息之后就不管了,所以,并不影响消息的实现。
虽然能将 block 编译出来看到代码,但是还是有很多疑问的,希望大家能解答一下。
__block
标识,对 block 持有对象的影响 其实这里是四种状态:
__block
__block
__block
__block
首先来看问题 1、2:
- (instancetype)init { self = [super init]; if (self) { // 定义一个可变数组 arr NSMutableArray *arr = [NSMutableArray new]; // 输出 retainCount NSLog(@"1. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 // 为减少隐式的 __strong 造成拷贝到堆的影响,所以使用 __unsafe_unretained 修饰 __unsafe_unretained dispatch_block_t block = ^{ NSLog(@"%@", arr); // 输出调用 block 时,arr 的 retainCount NSLog(@"2. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 }; // 在定义完 block 后,arr 的 retainCount NSLog(@"3. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 2; MRC: 1 // 显示拷贝到堆 self.block = block; // 在 block 拷贝到堆以后,arr 的 retainCount NSLog(@"4. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 3; MRC: 2 // 如果是 MRC,则手动 release [arr release]; } return self; } // 调用 block - (void)run { self.block(); }
可以看到,同样没有使用 __block
修饰,ARC 在 block 定义完以后, arr
的 retainCount 要比 MRC 下多 1,这是因为在 block 的结构体中,所定义的 NSMutableArray *arr
,默认的缺省值是 __strong
,而导致的持有,而 MRC 下,缺省值不是 __strong
造成的。
再来看问题 2、4,还是借助上面的例子,只是在 arr
定义时,在前面使用 __block
进行修饰,但这一次的 retainCount 却大为不同:
- (instancetype)init { self = [super init]; if (self) { __block NSMutableArray *arr = [NSMutableArray new]; NSLog(@"1. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 __unsafe_unretained dispatch_block_t block = ^{ NSLog(@"%@", arr); NSLog(@"2. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 }; NSLog(@"3. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 self.block = block; NSLog(@"4. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 [arr release]; } return self; } - (void)run { self.block(); }
这一次,我们发现,无论是 ARC,还是 MRC, arr
的 retainCount 始终为 1,在查阅资料后,找到这样一句话:
在 MRC 下, __block
说明符可被用来避免循环引用,是因为当 block 从栈复制到堆上时,如果变量被 __block
修饰,则不会再次 retain,如果没有被 __block
修饰,则会被 retain。
但是,从上面的代码输出来看,ARC 和 MRC,block 是否拷贝到堆上,都没有再次对变量进行持有,retainCount 始终为 1,所以,到这里我遇到几个不太理解的地方:
__block
修饰符不再持有对象,仅仅是在 MRC 下有效,还是 ARC 与 MRC 下效果是相同的? __block
不能解决 ARC 下的循环引用问题? arr
定义时,缺省值是 __strong
导致的? arr
的 retainCount 是 1,经过一次 release 以后,并未出现问题,而在 MRC 下,在 block 调用的时候,就会出现 crash?