在前面的几篇中,我们已经做了充足的准备了,本篇中我们将尝试分析下 Block 的源码,了解下 Block 是怎样实现的,以及前几篇中我们提到的 Block 的几个特性是怎样设计并实现的。
1 Block 的实现 1.1 Block 源码分析 分析 Block 源码,有两种方式,一种是直接分析 libclosure
(苹果官方 block 源码,可在网上搜索最新版);另一种是使用 clang
将 OC 代码转为 C++ 源码分析。我个人较喜欢 《iOS-iOS 与 OS X 多线程和内存管理》 一书中的分析方式,基于自己代码改写的 C++ 源码较之直接看 libclosure
更容易理解与入手,所以我们的学习也是主要以此方法入手,适时对比下 block 中的源码,对照着理解。
实际上这里也引出一种我推荐的学习方式,就是 “兼听则明”。大家自己在学习的过程中也可以这样,就是不要光看一个人的著作或偏执于一种方法。比如学习 iOS 某项技术,到网上或书中找多些相关的材料,有条件的将它们打印下来,对比着看对比着学习,最重要是自己手动操作一下,这样理解会更深。
好了,废话不多说,我们下面来进入 Block 源码分析。
我们先写一个简单的示例,然后将之用 clang
命令重写成 C++ 源码,再慢慢进行分析。
简单的 block 示例 - testblock.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #import <Foundation/Foundation.h> int main(int argc, const char * argv[]){ void (^myBlock)(void ) = ^{ NSLog (@"Hello world!" ); }; myBlock(); return 0 ; }
然后使用下面的 clang
命令将其改写成 C++ 源码
将 OC 源码其改写成 C++ 源码 1 2 3 4 clang -rewrite-objc -fobjc-arc -w testblock.m clang -rewrite-objc -fobjc-arc -w -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations xx.m
重写出来的文件我们可以自己细看下感受一下,可以看到,生成出来的源码编译器帮我们自动生成了很多基于 Runtime 的辅助代码。我们在 《面向对象》 一文中说过了 OC Runtime 中面向对象的实现,有此文的基础你再看这些源码会显得亲切一些。
我们看看生成的 main 方法:
生成源码中的 main 方法 1 2 3 4 5 6 7 8 9 10 11 int main (int argc, const char * argv[]) { void (*myBlock)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock); return 0 ; }
这段源码还是挺好看的嘛~与我们 OC 代码中做的一样,就是简单的翻译过来,先定义 Block,再调用 Block。我们不妨先看 block 的定义。
1.2 Block 的定义 Block 的定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void (*myBlock)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));static void __main_block_func_0(struct __main_block_impl_0 *__cself){ NSLog((NSString *)&__NSConstantStringImpl__var_folders__9_98znkr191gzfg1hg7lq_0n8c0000gn_T_test_1b945a_mi_0); } static __NSConstantStringImpl __NSConstantStringImpl__var_folders__9_98znkr191gzfg1hg7lq_0n8c0000gn_T_test_1b945a_mi_0 __attribute__ ((section ("__DATA, __cfstring" ))) = {__CFConstantStringClassReference,0x000007c8 ,"Hello world!" ,12 };
block 定义部分的代码可以这样看,myBlock 是一个标准的指向结构体的指针,我们知道,OC 面向对象的实现中,对象和类都是基于结构体实现的,所以 myBlock 就可以理解为是我们定义的这个 block 对象的引用!这样一来,定义一个 Block 看起来就像是定义了一个 Block 类,并生成了一个此 Block 的对象一般。我们从下面的简单过程可以看得更清楚:
简化定义代码 1 2 3 4 5 6 7 8 9 10 void (*myBlock)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); │ │ │ ▼ ▼ ▼ ------------------------------------------------------------------------------------------ __main_block_impl_0 ( __main_block_func_0 , __main_block_desc_0_DATA ); ------------------------------------------------------------------------------------------ │ │ │ ▼ ▼ ▼ 生成 Block 结构体构造函数 block 方法体 Block 的描述信息 (block 大小等信息)
再看后面的具体定义的代码:
定义 block 结构体实例 1 ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
此处有个 __main_block_impl_0
,我们在源码中搜索,发现其是一个结构体,上面这行语句就是调用此结构体的构造函数生成了一个 __main_block_impl_0
结构体实例。我们看看其命名 __main_block_impl_0
– main
是生成此 block 的外层方法,impl
这个我们熟悉,不就是 implement 吗,后面的 0 我们可以猜测,如果在 main 中定义多个 block,就会以此规则用数字序号递增下去 (后面感兴趣的童鞋可以自行生成多个 block 验证下~)。
block 结构体定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 struct __main_block_impl_0 { struct __block_impl impl ; struct __main_block_desc_0 * Desc ; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0 ) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; 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)};
此构造函数第一个参数 void *fp
,一个方法指针,正是我们定义 Block 中的方法体。第二个参数 __main_block_desc_0_DATA
我们在其后看到定义,如其名是 block 结构体的描述信息 (主要是保存 block 占内存大小)。
我们再看看 __main_block_impl_0
的成员 __block_impl
,其中有一个 isa
– 这不禁让我们想到我们之前说过的 类对象
或 元类
!这也可以让我们猜想,Block 的很多公共行为将会由此 impl 定义,我们在 block 调用中也可以看到,当调用 block 时,实际上就是调用 impl.FuncPtr
:
回顾 Block 调用语法 1 2 3 4 5 6 ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock); ───────────┬──────────── │ │ ▼ │ ▼ myBlock 之所以可向上转型成__block_impl │ 调用实例方法时传入实例 self! 因为其第一个成员就是 impl。 ▼ 调用 Block 方法体
我们从 __main_block_impl_0
的结构函数中可以看出端倪:
__main_block_impl_0 结构函数 1 2 3 4 5 6 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0 ) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }
在构造 __main_block_impl_0
时,同时也初始化了其主要成员 __block_impl
,将方法指针赋给其 FuncPtr
成员,同时指定其 isa
为 _NSConcreteStackBlock
(这个_NSConcreteStackBlock 后面还会细说).
基于 OC 中类与对象的实现 (《面向对象》 ), 我们可以猜测,__main_block_impl_0
结构体相当于一个 OC 对象结构体 (objc_object),而其 isa
指向相当于 objc_class
。即当我们定义一个 Block 时,底层自动为每个 Block 生成了一个包装类如 __main_block_impl_0
, 其继承自 _NSConcreteStackBlock
。所以我们也可以说 Block 的实质也是一个 Objective-C 对象 。(这真有点像 Java 中的匿名内部类~:) )
啰嗦一点我再通俗解释下,也方便自己日后复看些文章快速回忆。相当于我们每次定义一个 Block,编译器其实自动帮我们生成了一个特定的 Block 类,此 Block 类继承自 _NSConcreteStackBlock
或其它预定义好的 Block 基类,然后将 Block 方法体和传入的变量 (后面将要介绍) 赋给经 Block 类。当我们调用 block 时,会自动转成 (__block_impl *) blockObj)->FuncPtr (blockObj)
调用。
我们现在搞清楚了 Block 定义相关的内容,下面我们接着看看 Block 特性之一的访问外层变量是怎样实现的
2 Block 访问外层变量的实现 我们首先看看截取自动变量的实现。
2.1 截取自动变量的实现 与研究 Block 定义的实验方法一样,我们也是先按我们设想写个 Demo,然后用 C++ 将之重写来研究:
截取自动变量示例程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #import <Foundation/Foundation.h> int main (int argc, const char * argv[]) { int a = 10 ; id obj = [[NSObject alloc] init]; void (^myBlock)(void ) = ^{ NSLog(@"Hello world! %d, %@" , a, obj); }; myBlock(); return 0 ; }
我看先看看 rewrite 后源码的 main 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main (int argc, const char * argv[]) { int a = 10 ; id obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject" ), sel_registerName("alloc" )), sel_registerName("init" )); void (*myBlock)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, obj, 570425344 )); ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock); return 0 ; }
第一部分的定义局部变量我们没有问题,生成 obj 对象也没有问题。
看了定义 Block 这里我们终于真像大白了!就如我在上篇中说的一样,虽然在 Block 中我们可以自由地使用外层变量,但实际上,编译器帮我们做了传值!!如下示例:
调用 Block 这里我们上节已经学习过了。
自动变量传值示意: 1 2 3 4 5 void (*myBlock)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, obj, 570425344 )); │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ __main_block_impl_0 ( __main_block_func_0 , &__main_block_desc_0_DATA, a, obj , flags)
既然在我们定义 Block 之时编译器帮我们做了传值,那 Block 内部是怎样对待它们的呢?知道了 Block 定义之后,我们可以直接看 Block 包装类 __main_block_impl_0
:
Block 包装类__main_block_impl_0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 struct __main_block_impl_0 { struct __block_impl impl ; struct __main_block_desc_0 * Desc ; int a; id obj; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, id _obj, int flags=0 ) : a(_a), obj(_obj) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself){ int a = __cself->a; id obj = __cself->obj; NSLog((NSString *)&__NSConstantStringImpl__var_folders__9_98znkr191gzfg1hg7lq_0n8c0000gn_T_testWithBasicVariable_c33981_mi_0, a, obj); } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; 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}; static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src){ _Block_object_assign((void *)&dst->obj, (void *)src->obj, 3 ); } static void __main_block_dispose_0(struct __main_block_impl_0*src){ _Block_object_dispose((void *)src->obj, 3 ); }
从上面源码中看出,Block 包装类 __main_block_impl_0
自动构造了 2 个成员变量,分别对应着我们传入 block 的自动变量!然后在其构造函数中在定义处传了进去。这里我们就彻底明白自动截取变量的实现玄机了!所以我们说截取的是瞬时值,因为其实现就是在 block 定义之时复制到 block 内部的 ~ 那么,在 Block 中不可修改截取的自动变量,当然就是编译器做的限制咯。
然后我们可以留意下 Block 方法体中,先是对传入的 cself (也就是 Block 对象本身) 取出其中生成的截取变量值,然后再继承函数体操作的。
而此时 __main_block_desc_0
这个结构体,多出了两个方法
下面小测试我留了道思考题,大家看看对传进来的 OC 对象会有什么遭遇,为什么?
截取自动变量测试题 1 2 3 4 5 6 7 NSMutableArray *array = [@[@"1" , @"2" , @"3" ] mutableCopy];void (^myBlock)(void ) = ^{ NSLog (@"array: %@" , array); }; [array removeLastObject]; myBlock ();
这里截取的变量会变吗 —————————————— 答案是会的,因为对于对象而言,截取的只是其引用。
2.2 __block 变量的实现 截取的自动变量实现虽然简单,但是其缺点为不能对之进行修改。我们说过,__block 变量可以解决这种问题,老规矩,我们写 Demo 实验看看:
__block 变量 Demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #import <Foundation/Foundation.h> int main(int argc, const char * argv[]){ __block id obj = [[NSObject alloc] init]; __block int a = 10 ; void (^myBlock)(void ) = ^{ obj = [[NSObject alloc] init]; a = 20 ; }; myBlock(); return 0 ; }
我们看看 rewrite 之后的 main 方法,我们惊奇地发现,增加了 __block
修饰符后变量的定义都变了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 int main (int argc, const char * argv[]) { __attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = { (void *)0 , (__Block_byref_obj_0 *)&obj, 33554432 , sizeof (__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject" ), sel_registerName("alloc" )), sel_registerName("init" )) }; __attribute__((__blocks__(byref))) __Block_byref_a_1 a = { (void *)0 , (__Block_byref_a_1 *)&a, 0 , sizeof (__Block_byref_a_1), 10 }; void (*myBlock)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_obj_0 *)&obj, (__Block_byref_a_1 *)&a, 570425344 )); ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock); return 0 ; }
我们发现,生成的代码对 __block
修饰的变量进行了封装,比如使用 __Block_byref_obj_0
这个类型来封装 __block id obj
, 使用 __Block_byref_a_1
来封装 __block int a
. 我把这个封装结构体代码列出来,从其 isa 看出它也是一个类型。然后我们对照 main 里的该结构体的生成代码就可以发现其中的关系:
__block 变量封装类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 struct __Block_byref_obj_0 { void *__isa; __Block_byref_obj_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void *, void *); void (*__Block_byref_id_object_dispose)(void *); id obj; }; struct __Block_byref_a_1 { void *__isa; __Block_byref_a_1 *__forwarding; int __flags; int __size; int a; }; __attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = { (void *)0 , (__Block_byref_obj_0 *)&obj, 33554432 , sizeof (__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject" ), sel_registerName("alloc" )), sel_registerName("init" )) }; ┌──────────────┐ │ │ │ │ ┌────────────────────────▼──────┐ │ │__isa │ │ │__forwarding ──────────────────┼───────┘ │__flags │ │__size │ │__Block_byref_id_object_copy │ │__Block_byref_id_object_dispose│ │id obj │ └───────────────────────────────┘ ( __Block_byref_obj_0 图解 )
乍一看这个自动生成的变量封装类有点奇怪,里面有个指向结构体本身的 __Block_byref_a_1 *__forwarding
指针,还有两个函数 copy、dispose, 它们各自有什么用呢?我们下面就来说变量封装类的作用,其实正因为有了这种机制,才使用__block 变量的特性得以实现:
使得外部变量可以在 Block 内修改;
变量超出作用域后
2.2.1 修改外部变量的实现 我们看 Block 方法体源码:
Block 方法体源码 1 2 3 4 5 6 7 8 9 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_obj_0 *obj = __cself->obj; __Block_byref_a_1 *a = __cself->a; (obj->__forwarding->obj) = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject" ), sel_registerName("alloc" )), sel_registerName("init" )); (a->__forwarding->a) = 20 ; }
可见,正因为将变量封装成__Block_byref_xx 结构,使其不必像截取自动变量一样在 block 定义时复制到 block 内部。而是类似 (__Block_byref_obj_0 *)&obj
这样传递__Block_byref_xx 结构的引用。
这样,在方法体中对外部__block 变量的修改就变成了对传入的__Block_byref_xx 结构体中相应的变量成员的修改。在 Block 外访问__block 变量时,也是访问此同一结构体,因此,就达到了修改外部变量的作用。
至此,我们已经了解了 Block、自动变量截取、__block 变量的底层是怎样实现的了。不过现在还有一些疑问,比如
__forwarding
指针有什么用?
block 结构体中的 copy、dispose 方法又有何用?
我们平时说的 block 循环引用是为什么会产生?
下面我们分析 Block 的存储域问题后,这些问题将一一解开。
3 Block 存储域与堆拷贝 我们前面分析过,Block 其实也是一个 Objetive-C 对象,我们每定义一个 Block 其实对应生成了一个 Block 结构体,而其 isa 指向一个叫 NSConcreteStackBlock
的结构。这个结构说明了 block 类的类型!从字面上可以看出,这是一个分配在栈 (stack) 上的 block。
3.1 Block 存储域 其实,Block 共有 3 种类型,分别代表其存储在不同的存储区中:
类 | 对应的存储域 | ———————-|—————|– NSConcreteStackBlock | 栈区 | NSConcreteGlobalBlock | 数据区 (.data) | NSConcreteMallocBlock | 堆区 |
存储在不同区域的 Block 有什么区别呢?我们不妨先构造场景看一看
block 存储域示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 void (^globalBlock)(void ) = ^{ printf ("globalBlock\n" , ); };int main (int argc, const char * argv[]) { int a = 1 ; static int b = 2 ; void (^localBlock)(void ) = ^(void ) { NSLog (@"访问局部变量 localBlock % d" , a); }; void (^staticBlock)(void ) = ^(void ) { NSLog (@"访问静态变量 staticBlock % d" , b); }; void (^paramBlock)(NSString *) = ^(NSString *inputStr) { NSLog (@"只访问入参 paramBlock %@" , inputStr); }; globalBlock(); localBlock(); staticBlock(); paramBlock(@"c" ); NSLog(@"globalBlock:%@" , globalBlock); NSLog(@"localBlock:%@" , localBlock); NSLog(@"staticBlock:%@" , staticBlock); NSLog(@"paramBlock:%@" , paramBlock); NSLog(@"stackBlock:%@" , ^{ NSLog (@"访问局部变量 stackBlock % d" , a); }); } ==> output: globalBlock 访问局部变量 localBlock 1 访问静态变量 staticBlock 2 只访问入参 paramBlock hello globalBlock:<__NSGlobalBlock__: 0x10b7f9138 > localBlock:<__NSMallocBlock__: 0x60000270e100 > staticBlock:<__NSGlobalBlock__: 0x10b7f9198 > paramBlock:<__NSGlobalBlock__: 0x10b7f91d8 > stackBlock:<__NSStackBlock__: 0x7ffee4406520 >
我们看到,全局定义的 Block
、访问静态变量的 Block
和 没有访问外部变量
的 Block 都是 __NSGlobalBlock__
类型!也就是说,当 Block 不需要捕获外部变量 (除了静态变量与局部变量) 时,Block 会被定义为全局 Block。这是编译器的一个优化,因为全局 Block 不存在对自动变量的截取、封装等操作,肯定是机制最常用也最简单的 Block,这种 Block 在整个程序中只需要一个实例,因此直接将其置于数据区最合适 (因为全局变量也在这个区域)。
虽然这些 Block 除了全局定义的 Block 外都是在函数中定义的,但是我们看,只有最后一个在 NSLog 方法内定义并直接访问的 Block 为我们最熟悉的 __NSStackBlock__
。此类 Block 存储在栈区中,上一篇我们对栈区有了深入的了解,我们知道,当函数调用结束,它就会随着栈帧的销毁而销毁。
示例中的 localBlock
让我们大跌眼镜,我们并没有对 block 做任何 copy 的动作,也没有指定在堆上为之分配内存,为什么这个 Block 会被分配到堆上呢?原来在 ARC 中,赋值操作会自动对指向的 Block 调一次 copy
方法。我们看下图:
对不同的 block 调用 copy 方法 1 2 3 __NSStackBlock__ --> 执行 copy 后 --> 从栈复制到堆并变成__NSMallocBlock__ __NSGlobalBlock__ --> 执行 copy 后 --> 什么也不做 __NSMallocBlock__ --> 执行 copy 后 --> 引用计数增加
也就是说,本来 localBlock
在我们源码分析时应该是 stackBlock,但因为自动执行了 copy
,将之从栈复制到堆并变成 __NSMallocBlock__
。
这个自动调用 copy 的示例也解释了为什么我们平时将 block 作为 property 时用 strong 和 copy 其实是一样的。
知道了 Block 的存储域后我们进一步发问,为什么要如此设计呢?将 Block 拷贝到堆上有什么好处?下面我们就来深入 Block 的堆拷贝。
3.2 Block 的堆拷贝 Block 堆拷贝的第一个作用就是使得 Block 超出变量作用域后仍然可以使用 。
3.2.1 Block 超出变量作用域后仍然可以使用 我们来看一下这个例子:
Block 的堆拷贝示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 AddBlock generateAddBlock (int inc) { return ^(int a, int b) { printf ("%d" , a + b + inc); }; } int main (int argc, const char * argv[]) { AddBlock myAdd = generateAddBlock(2 ); myAdd(1 , 2 ); return 0 ; }
在上面的例子中,block 被作为函数的返回值,我们在学习内存管理时了解过,这是一个经典的返回内部对象问题 (因为 Block 本质上也是 OC 对象嘛)。因为这个成生的 Block 是个 __NSStackBlock__
,联系我们上一篇学的函数栈管理我们知道如果 Block 分配在栈上的函数一返回其就被回收了,那我们为什么在 main
中仍然可以调用之呢?
答案就是 Block 的堆拷贝!将 Block 从栈上拷贝到堆上变成 __NSMallocBlock__
后,就能解决问题,也就是说,函数返回的是被拷贝在堆上的 Block。对应于 ARC 的转换代码应该如下:
参照《iOS-iOS 与 OSX 多线程和内存管理》一书中对应于 ARC 的转换代码 1 2 3 4 5 6 7 8 9 AddBlock generateAddBlock(int inc) { AddBlock tmp = ((void (*)(int , int ))&__generateAddBlock_block_impl_0((void *)__generateAddBlock_block_func_0, &__generateAddBlock_block_desc_0_DATA, inc)); tmp = _Block_copy(tmp); return objc_autoreleaseReturnValue(tmp); }
其中 tmp = _Block_copy (tmp);
将 Block 复制到堆上,复制到堆上后,block 终于更像一个 OC 对象了,可以将其注册到 autoReleasePool
中并返回。实际上,上面我们说过,在 ARC 中,只要对 Block 有赋值,就会发生堆拷贝。我们不妨深入 _Block_copy
源码去一窥究竟:
libclosure-73 中_Block_copy 源码,为便于阅读稍有改动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 enum { BLOCK_DEALLOCATING = (0x0001 ), BLOCK_REFCOUNT_MASK = (0xfffe ), BLOCK_NEEDS_FREE = (1 << 24 ), BLOCK_HAS_COPY_DISPOSE = (1 << 25 ), BLOCK_IS_GLOBAL = (1 << 28 ), }; void *_Block_copy(const void *arg) { struct Block_layout *aBlock ; if (!arg) return NULL ; aBlock = (struct Block_layout *)arg; if (aBlock->flags & BLOCK_NEEDS_FREE) { latching_incr_int(&aBlock->flags); return aBlock; } else if (aBlock->flags & BLOCK_IS_GLOBAL) { return aBlock; } else { struct Block_layout *result = (struct Block_layout *)malloc (aBlock->descriptor->size); if (!result) return NULL ; memmove(result, aBlock, aBlock->descriptor->size); result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); result->flags |= BLOCK_NEEDS_FREE | 2 ; _Block_call_copy_helper(result, aBlock); result->isa = _NSConcreteMallocBlock; return result; } } static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock){ struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock); if (!desc) return ; (*desc->copy)(result, aBlock); }
其实上面的 _Block_copy
就正对着我们之前列出的这张表:
1 2 3 __NSStackBlock__ --> 执行 copy 后 --> 从栈复制到堆并变成__NSMallocBlock__ __NSGlobalBlock__ --> 执行 copy 后 --> 什么也不做 __NSMallocBlock__ --> 执行 copy 后 --> 引用计数增加
3.2.2 __block 变量随着 block 一块堆拷贝 上面 _Block_copy
源码中我们注意到还有一点没有说,就是里面的 _Block_call_copy_helper (result, aBlock);
。它调用了 Block 类的 Desc 中的 copy 方法,也是我们之前遗留的一个问题,这个 copy 是用来干嘛的呢?我们把他找出来看看(在 rewrite 的 C++ 源码中):
我们以之前介绍__block 变量定义的示例来说,其中定义了两个__block 变量 int a
和 id obj
:
深入 DESC 的 copy 方法 1 2 3 4 5 static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src){ _Block_object_assign((void *)&dst->obj, (void *)src->obj, 8 ); _Block_object_assign((void *)&dst->a, (void *)src->a, 8 ); }
其中调用了 _Block_object_assign
方法,此方法在 rewrite 源码中被定义为外部符号,主要作用就是当 block 发生拷贝时对其内部成员的或拷贝或持有的操作,所幸我们可以在 libclosure
中找到源码:
libclosure-73 中_Block_object_assign 方法追踪 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 void _Block_object_assign(void *destArg, const void *object, const int flags) { const void **dest = (const void **)destArg; switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) { case BLOCK_FIELD_IS_OBJECT: _Block_retain_object(object); *dest = object; break ; case BLOCK_FIELD_IS_BLOCK: *dest = _Block_copy(object); break ; case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK: case BLOCK_FIELD_IS_BYREF: *dest = _Block_byref_copy(object); break ; case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT: case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK: *dest = object; break ; case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK: case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK: *dest = object; break ; default : break ; } }
从上面源码我们知道,编译器在对 Block 进行堆拷贝时,会将其包含的__block 变量一并也进行堆拷贝!
libclosure-73 中_Block_byref_copy 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 static struct Block_byref *_Block_byref_copy (const void *arg ) { struct Block_byref *src = (struct Block_byref *)arg; if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0 ) { struct Block_byref *copy = (struct Block_byref *)malloc (src->size); copy->isa = NULL ; copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4 ; copy->forwarding = copy; src->forwarding = copy; copy->size = src->size; if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) { struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1 ); struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1 ); copy2->byref_keep = src2->byref_keep; copy2->byref_destroy = src2->byref_destroy; if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) { struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1 ); struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1 ); copy3->layout = src3->layout; } (*src2->byref_keep)(copy, src); } else { memmove(copy+1 , src+1 , src->size - sizeof (*src)); } } else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) { latching_incr_int(&src->forwarding->flags); } return src->forwarding; }
那么这个 forwarding 有什么用呢?我们需要画图解释!
栈拷贝图示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ┌────────────────────────┐ ┌─────────────────────────┐ │ │ forwarding 拷贝后指堆上 ┌────────┐ │ │ ┌─┼────────────┼─────────────▶│ │ │ │ ┌───────────────┐ │ │ │ ┌────────▼──────┐ │ │ │ │ __block var │ │ │ │ │ __block var │ │ │ ┌──┼───▶│ │ │■■■■■ 堆拷贝 ■■■■▶ │ │◀┼─┼───┐ │ │ │ forwarding ──┼─┘ │ │ │ forwarding ──┼─┘ │ │ │ │ └───────────────┘ │ │ └───────────────┘ │ │ │ │ │ │ │ │ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │ │ │ │ │ │ │ │ │ │ ├──┼────│ Block1 │ ■■■■■ 堆拷贝 ■■■■▶ │ Block1 ├────┼───┤ 持有 (block 销毁后将释放) │ │ │ │ │ │ │ │ │ │ │ │ └──────────────┘ │ │ └──────────────┘ │ │ │ │ │ │ │ │ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │ │ │ Block2 │ │ │ │ Block2 │ │ │ └──┼────│ │ ■■■■■ 堆拷贝 ■■■■▶ │ │────┼───┘ │ │ │ │ │ │ │ │ │ └──────────────┘ │ │ └──────────────┘ │ │ │ │ │ └────────────────────────┘ └─────────────────────────┘
当 Block 堆拷贝后,其__block 变量随着一起拷贝 (Block_byref 结构
),这样做当然也是为了避免栈上的变量随着栈帧销毁释放的设计。现在我们就知道 forwarding 的设计多巧妙了,它使得__block 拷贝到堆上后原来栈上的 Block 与堆上的 Block 仍可使用,且指向的__block 变量是同一个,这样的小设计可以使代码变得更简单。
3.3 堆拷贝相关问题 堆拷贝可以优雅地解决 Block 与__block 变量的作用域问题,也会带来一些新的要注意的问题,了解这些点有助于我们对 Block 的运用。
3.3.1 Block 持有对象 我们前面就提到过,当截取的自动变量是对象时,会在 block_impl
中生成一个指向此对象的引用。在 “libclosure-73 中_Block_object_assign 方法追踪” 中我们看到,发生堆拷贝时,又对其持有的对象做了 retain 操作。
我们看看下面这个示例:
Block 持有对象示例 1 2 3 4 5 6 7 8 9 10 11 12 13 typedef void (^blk_t ) (id) ;blk_t blk;{ NSMutableArray *array = [NSMutableArray arrayWithCapacity:10 ]; blk = ^(id obj){ [array addObject:obj]; NSLog(@"After add object, array count is %ld" , array .count); }; } blk([NSObject new]); blk([NSObject new]); blk([NSObject new]);
这里我们用了一个局部代码块,在代码块中定义了一个局部的 array 对象,正常来说,当代码块执行完毕,array 出了其作用域后,ARC 会将其引用计数减至 0,释放此数组对象。但是从 block 的运行结果来看,此 array 并没有销毁。
我们 rewrite 上面代码观察,发现在 block_impl 中增加了引用此对象的成员:
block 结构体定义 1 2 3 4 5 6 7 8 9 10 struct __main_block_impl_0 { struct __block_impl impl ; struct __main_block_desc_0 * Desc ; NSMutableArray *__strong array ; };
我这里就不把全部代码罗列出来了,和我们之前已经看过的差不多,就是在 block 定义处,将 array 引用传给了 block 的 NSMutableArray *__strong array;
成员。
因此,我们发现,当往 Block 中 “传” 对象的时候 (这里说的传,其实就是访问外部对象!),实际上 Block 将其转化成了持有该对象 !这就是我们要注意的地方了,也就是在 Block 内访问外部对象时要格外注意,因为这将导致 Block 持有该对象。
现在我们终于知道为什么 Block 会持有外部对象了,那这里要注意什么问题呢?主要就是我们常说的 Block 循环引用问题。
3.3.2 Block 循环引用问题 其实有了上一小节的示例,常见的循环引用问题就很好解了,比如下面这个常见的例子
Block 循环引用示例 1 2 3 4 [self .button onButtonPress:^{ [self loadData]; }];
这会导致一个怎样的循环引用出现呢?
我们先分析下,首先,self 持有 button,这个没问题。 然后给 Button 设置了这个 Block 后,发生 Block 的堆拷贝,然后 Button 持有此 Block。最后我们看下 Block 内访问到了 self 对象,顾 Block 也会持有 self。我们作出持有关系图:
循环引用示意图 1 2 3 4 5 6 ┌────┐ ┌─────────┐ ┌───────┐ │self│─retain─▶│ button ├─retain──▶│ block │ └────┘ └─────────┘ └───────┘ ▲ │ │ │ └───────────retain──────────────────┘
如何打破此循环引用呢?众所周知,就是使用__weak 引用:
使用__weak 解决循环引用 1 2 3 4 5 6 7 8 9 10 11 __weak typeof (self) weakSelf = self; [self.button onButtonPress:^{ [weakSelf loadData]; }];
3.3.3 Block 中的__weak 引用 我们最后看看 Block 中的__weak 引用是啥样:
Block 中使用引部__weak 引用的 OC 源码 1 2 3 4 5 6 7 8 9 NSArray* array = [[NSArray alloc] init]; __weak NSArray* w_array = array ; __block __weak NSArray* bw_array = array ; void (^myBlock)(void ) = ^{ NSLog(@"%@" , w_array); NSLog(@"%@" , bw_array); };
我们将之 rewrite, 注意因为这里用到的 weak,所以要用命令:
1 clang -rewrite-objc -fobjc-arc -w -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations xx.m
我们单看生成了 Block 类结构体就知道了:
Block 中使用引部__weak 引用的 C++ 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 struct __main_block_impl_0 { struct __block_impl impl ; struct __main_block_desc_0 * Desc ; NSArray *__weak w_array; __Block_byref_bw_array_0 *bw_array; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSArray *__weak _w_array, __Block_byref_bw_array_0 *_bw_array, int flags=0 ) : w_array(_w_array), bw_array(_bw_array->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; struct __Block_byref_bw_array_0 { void *__isa; __Block_byref_bw_array_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void *, void *); void (*__Block_byref_id_object_dispose)(void *); NSArray *__weak bw_array; };
从上面的源码我们也可以看出,__block __weak
与 __weak
的效果其实是一样的,而加上 __block
后还会增加额外的结构体与堆拷贝,所以为了解决循环引用问题,使用 __weak
就可以了。
4 小结 至此,我们对 Objective-C 的 Block 的探索就暂告一段落了。
在这一系列中,我们了解了 Block 的由来,它是 OC 中对于闭包的一种实现。而闭包又是一种优秀的技术,主要来说其语法简单高效,可以轻松地实现方法的传递,而且闭包还可以方便地访问外部变量,这种灵活高效的技术也提高了我们平时工作的效率。
然后我们还了学习了关于函数栈的知识,巩固了我们知识面的同时,也为了解 Block 时涉及的各种栈区啊、堆区啊等作好了知识储备。
最后,也就是本篇中,我们深入探索了 Block 的实现。知道了 Block 在底层中是怎样定义的,它其实是一个 Objective-C 对象!接着我们又了解到 Block 是怎样访问外部变量的,截取自动变量是怎样实现的,__block 变量是怎么被封装成__block_byref 结构。最后,我们共同学习了 Block 的堆拷贝技术,终于明白了为什么 Block 可以自由传递而不会随着栈区销毁、Block 为什么可以持有其内部访问的对象、以及搞明白了 Block 循环引用是怎样造成的又是如果解决的。
Block 作为多线程技术中重要的结构,可说是重中之重,全面地认识 Block 为后面我们继续深入学习 iOS 多线程编程做了良好的铺垫。
5 引用
《iOS-iOS 与 OS X 多线程和内存管理》 我学习本系列的主要参考书,写得非常好,真正的深入浅出。
《谈 Objective-C block 的实现 - 唐巧》 唐巧一篇早期的文章,写得不错的。
源码:libclosure-73,objc4-709
《iOS逆向-从汇编代码理解函数调用栈》 - 傻傻木 ,分析函数调用栈比较详细的一篇文章