前言
本文内容主要来自 Pro Multithreading and Memory Management for iOS and OS X with ARC, Grand Central Dispatch, and Blocks 这本书,参考这篇文章。
Block 是语言级别的语法,是 C 语言的扩展。Block 可以解释为“包含了局部变量的匿名函数(anonymous functions together with automatic (local) variables)”。本文不多说 Block 的使用方法,着重讨论 Block 的实现机制。
一、Block 的基本实现
可以使用指令 clang -rewrite-objc file_name_of_the_source_code
,将 OC 源代码转换成对应的 C++ 实现,从而探究 Block 的实现原理。
原始代码:
1 2 3 4 5 6
| int main() { void (^blk)(void) = ^{printf("Block\n");}; blk(); return 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
| struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; };
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; } };
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Block\n"); }
static struct __main_block_desc_0 { unsigned long reserved; unsigned long Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) };
int main() { void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA); ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk); return 0; }
|
对比原始代码,转换后的代码增加了三个结构体和一个函数的定义。结构体:__block_impl、__main_block_impl_0、__main_block_desc_0;函数:__main_block_func_0。__main_block_func_0 对应原始代码中的 Block 实现,函数命名的规则是取原始方法名(main)和该 Block 在原始方法中的次序(第0个),结构体的命名规则也是如此。
二、isa 和 _NSConcreteStackBlock
上小节 __main_block_impl_0 的构造函数中有赋值语句 impl.isa = &_NSConcreteStackBlock
,本小节简述 isa 和 _NSConcreteStackBlock 的含义。
OC 中的对象实例和类编译后都有其对应的结构体定义,如下所示。
对象实例对应的结构体:
1 2 3 4
| struct objc_object { Class isa; }
|
类对应的结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct objc_class { Class isa; };
typedef struct objc_class *Class;
struct class_t { struct class_t *isa; struct class_t *superclass; Cache cache; IMP *vtable; uintptr_t data_NEVER_USE; };
|
OC 中的类使用 class_t 构造(class_t 本身基于 objc_class),也就是说 OC 中的每个类都使用 class_t 创建了实例。比如,NSObject 有其对应的 class_t 实例,NSMutableArray 有其对应的 class_t 实例。class_t 实例保存了类的信息,如方法名、方法实现、指向父类的指针等,提供给 OC 运行时库使用。
基于上面描述,下图描述 isa 值的含义。
图 OC 对象和类中 isa 指针的指向
__main_block_impl_0 结构体基于 objc_object,表明 Block 本身即为 OC 对象。创建 Block 时执行语句 impl.isa = &_NSConcreteStackBlock
。根据上文描述,_NSConcreteStackBlock 是 class_t 实例,保存了该 Block 对应的类的信息。
三、Block 捕获自动变量
Block 能够捕获自动变量。下面使用 clang -rewrite-objc file_name_of_the_source_code
指令转换代码,描述了在这种情况下 Block 实现方式的变化。(__block_impl、__main_block_desc_0、__main_block_desc_0_DATA的声明和定义与上文相同,不再描述)
原始代码:
1 2 3 4 5 6 7
| int main() { int dmy = 256; int val = 10; const char *fmt = "val = %d\n"; void (^blk)(void) = ^{printf(fmt, val);}; return 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
| struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; const char *fmt; int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { const char *fmt = __cself->fmt; int val = __cself->val; printf(fmt, val); }
int main() { int dmy = 256; int val = 10; const char *fmt = "val = %d\n"; void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, fmt, val); return 0; }
|
__main_block_impl_0 为 Block 对象的定义,在这种情况下,其中增加了两个成员变量(fmt 和 val)用来存储捕获的自动变量。Block 不会捕获未使用的自动变量(dmy)。
四、Block 中修改静态变量、静态全局变量和全局变量
Block 中能够修改静态变量、静态全局变量和全局变量的值,但是底层实现机制存在差异。下面给出转换前后的代码,并给出说明。
原始代码:
1 2 3 4 5 6 7 8 9 10 11 12
| int global_val = 1; static int static_global_val = 2; int main() { static int static_val = 3; void (^blk)(void) = ^{ global_val *= 1; static_global_val *= 2; static_val *= 3; }; return 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
| int global_val = 1; static int static_global_val = 2;
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 *Desc; int *static_val; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int *static_val = __cself->static_val;
global_val *= 1; static_global_val *= 2; (*static_val) *= 3; }
int main() { static int static_val = 3;
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val); return 0; }
|
静态全局变量和全局变量的处理方式是一致的,Block 能够直接对其进行读写。
静态变量的可见性只是当前函数内,转换后的代码中 __main_block_func_0 无法访问。在 __main_block_impl_0 中增加指向静态变量指针的成员变量,通过该成员变量实现读写静态变量。
前文说到 Block 能够捕获自动变量,但是不能修改其值。自动变量的生命周期跟随其所在的作用域,离开作用域即销毁。Block 的生命周期可能会长于自动变量的生命周期,所以无法采用读写静态变量的实现方案。
五、Block 中修改 __block 变量
原始代码:
1 2 3 4 5
| int main() { __block int val = 10; void (^blk)(void) = ^{val = 1;}; return 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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; };
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 *Desc; __Block_byref_val_0 *val; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_val_0 *val = __cself->val; (val->__forwarding->val) = 1; }
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) { _Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF); }
static void __main_block_dispose_0(struct __main_block_impl_0 *src) { _Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF); }
static struct __main_block_desc_0 { unsigned long reserved; unsigned long 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 };
int main() { __Block_byref_val_0 val = { 0, &val, 0, sizeof(__Block_byref_val_0), 10 }; blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000); return 0; }
|
转换后的代码中,原始 __block 变量转换成了 __Block_byref_val_0 结构体类型。__Block_byref_val_0 的成员变量 val 存储原始值。
__Block_byref_val_0 实例和 __main_block_impl_0 实例是多对多的关系,即一个 __Block_byref_val_0 实例可以在多个 __main_block_impl_0 实例中使用,一个 __main_block_impl_0 实例也可以使用多个 __Block_byref_val_0 实例。
六、Block 的存储类型
上文可知 Block 本身也是 OC 对象,其在内存中的存储方式有三种:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,分别对应:栈、全局/静态存储区、堆。内存区域划分方式大致可用下图表示,下图同时描述了不同存储方式的 Block 对应的内存区域。
图 Block 不同存储方式对应的内存区域
Block 字面定义在全局作用域生成 _NSConcreteGlobalBlock 类型 Block 对象。
存储类型为 _NSConcreteStackBlock 的 Block 以及 __block 修饰的变量的生命周期与普通自动变量相同,离开作用域后即销毁。
七、Block 的存储类型 -- 堆上的 Block
堆上的 Block 即存储类型为 _NSConcreteMallocBlock 的 Block。_NSConcreteStackBlock 存储类型的 Block 可以从栈拷贝到堆上。下图为 Block 从栈拷贝到堆的示意图,同样的,__block 变量也可以从栈拷贝到堆。
图 Block 和 __block 变量从栈拷贝到堆
在下列情况下,栈中的 Block 会拷贝到堆:
a. 调用 Block 对象的 copy 方法;
b. 函数返回值为 Block 对象;
c. Block 对象赋给 __strong 所有权描述符修饰的变量;
d. Block 对象被 Cocoa 框架中的 “usingBlock” 方法使用,或者被 GCD 中的函数使用。
不同存储类型的 Block 对象调用 copy 方法的效果不同:
a. _NSConcreteStackBlock 类型的 Block 对象,从栈拷贝到堆;
b. _NSConcreteGlobalBlock 类型的 Block 对象,不发生作用;
c. _NSConcreteMallocBlock 类型的 Block 对象,引用计数加一。(在 ARC 开启的情况下,多次调用 copy 方法也没有问题。)
八、__block 变量
当 Block 使用了 __block 变量并且 Block 从栈拷贝到堆时:如果 __block 变量存储在栈上,__block 变量会被拷贝到堆上,并且 Block 对象拥有 __block 变量的所有权;如果 __block 变量本来即存储在堆上,Block 也会拥有 __block 变量的所有权。
__block 变量编译后也是普通的结构体实例,其中有个特别的成员变量 __forwarding。通过 __forwarding 成员变量保证访问 __block 变量的一致性。如下代码片段,__block 变量随着 Block 从栈拷贝到堆上。
1 2 3 4
| __block int val = 0; void (^blk)(void) = [^{++val;} copy]; ++val; blk();
|
Block 内部会修改堆上的 __block 变量,Block 外部会修改栈上的 __block 变量。转换后,这两种行为是一致的,即++(val.__forwarding->val)
。栈和堆上 __block 变量中的 __forwarding 指针都指向堆上的 __block 变量,下图描述了这种机制。
图 __block 变量拷贝到堆
九、Block 捕获对象实例
前文讲述的 Block 捕获自动变量,自动变量的类型为整型变量。当 Block 捕获的自动变量为 OC 对象时:
原始代码:
1 2 3 4 5 6 7 8 9 10 11
| blk_t blk; { id array = [[NSMutableArray alloc] init]; blk = [^(id obj) { [array addObject:obj]; NSLog(@"array count = %ld", [array count]); } copy]; } blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); blk([[NSObject alloc] init]);
|
转换后代码:
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
| struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 *Desc; id __strong array; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong _array, int flags=0) : array(_array) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) { id __strong array = __cself->array; [array addObject:obj]; NSLog(@"array count = %ld", [array count]); }
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) { _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT); }
static void __main_block_dispose_0(struct __main_block_impl_0 *src) { _Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT); }
static struct __main_block_desc_0 { unsigned long reserved; unsigned long 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 };
blk_t blk; { id __strong array = [[NSMutableArray alloc] init]; blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000); blk = [blk copy]; } (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]); (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]); (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
|
十、内存管理
前文讲述到 Block 内存管理时提到对象之间的所有权关系,但是转换后的 C 代码是无法利用 ARC 机制的。本小节说明相关的内存管理是如何实现的。
10.1 Block 内的内存管理实现
在前文中,当 Block 对象需要引用对象时,比如捕获 __block 变量、捕获 OC 对象实例,__main_block_desc_0 结构体中多了两个成员变量 copy 和 dispose,他们都为函数指针,函数实现如下代码所示。copy 和 dispose 分别对应对象的初始化和销毁,OC 运行时检测到 Block 从栈拷贝到堆或者 Block 对象被销毁时,能够适时调用 copy 和 dispose 实现 Block 内的内存管理。
Block 捕获 __block 变量内存管理相关代码:
1 2 3 4 5 6 7 8 9
| static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) { _Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF); }
static void __main_block_dispose_0(struct __main_block_impl_0 *src) { _Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF); }
|
Block 捕获对象实例内存管理相关代码:
1 2 3 4 5 6 7 8 9
| static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) { _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT); }
static void __main_block_dispose_0(struct __main_block_impl_0 *src) { _Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT); }
|
10.2 __block 变量内的内存管理实现
当 __block 修饰的变量为 OC 对象实例时,__block 内部需要负责该对象实例的内存管理。如下代码所示。__block 变量内的内存管理实现和 Block 内的内存管理类似。该情况下,__Block_byref_obj_0 结构体中多了两个成员变量 __Block_byref_id_object_copy 和 __Block_byref_id_object_dispose,都为函数指针,作用与上小节的 copy 和 dispose 相同,OC 运行时检测到 __block 变量从栈拷贝到堆或者 __block 变量被销毁时,适时调用这对方法,实现 __block 变量内的内存管理。
原始代码:
1
| __block id obj = [[NSObject alloc] init];
|
转换后代码:
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 __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*); __strong id obj; };
static void __Block_byref_id_object_copy_131(void *dst, void *src) { _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131); }
static void __Block_byref_id_object_dispose_131(void *src) { _Block_object_dispose(*(void * *) ((char*)src + 40), 131); }
__Block_byref_obj_0 obj = { 0, &obj, 0x2000000, sizeof(__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, [[NSObject alloc] init] };
|
十一、ARC 下 Block 得存储类型变化
_NSConcreteGlobalBlock:这种类型的 block 定义在全局存储区,它没有捕获任何上下文,在编译时就能完全定义。
_NSConcreteStackBlock:这种类型的 block 定义在栈上。block 在被拷贝到堆上之前,都是存储在栈中。
_NSConcreteMallocBlock:这种类型的 block 存储在堆上。和普通 OC 对象遵循同样的内存管理规则(引用计数)。
MRC 和 ARC 的 block 存储有差异,“在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。”
十二、案例分析
案例
下面的代码显然存在内存泄漏,有 self -> blk -> self 这样的循环引用。那内存泄漏是哪个环节导致的,是位置 A,还是位置 B?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @interface TestObject : NSObject @property (nonatomic, copy) dispatch_block_t blk; @end @implementation TestObject - (instancetype)init { self = [super init]; if (self) { self.blk = ^{ [self emptyMethod]; }; self.blk(); } return self; } - (void)emptyMethod { NSLog(@"hello"); } @end
|
使用 Instruments 能够分析出位置 A 便引入了循环引用。参考 九、Block 捕获对象实例,Block 实例化的时候,外部对象实例在其构造函数中就被强引用。所以 Block 定义的时候就已经产生了循环引用,而不用等到执行的时候。
参考文献:
Sakamoto, Kazuki, and Tomohiko Furumoto. Pro Multithreading and Memory Management for IOS and OS X. Apress, 2012.
A look inside blocks: Episode 2
唐巧 . 谈Objective-C block的实现 – ARC 对 block 类型的影响