出于需要,本文分析时引用苹果开源的libclosure-74 源码,请点击对比查看苹果libclosure源码
一、Block概念
1.1 名词解释
也成为匿名函数,是将函数及其执行上下文封装起来的对象。有参数和返回值。
Block的结构是:__block_impl
struct __block_impl { |
1.2 分类
根据Block 所属的libclosure 所示,一共有6种。具体操作:进入苹果libclosure源码,选择最新的libclosure-74,下载后打开工程文件,搜索NSBlock
得知具体6种如下:
void * _NSConcreteStackBlock[32] = { 0 }; |
其中:NSConcreteStackBlock、NSConcreteMallocBlock、NSConcreteGlobalBlock 是常用到的栈区、堆区、全局区的block,其余3种为系统级别,实际开发较少会用到,所以先不展开说明。
1.2.1 NSGlobalBlock(全局Block)
当生成的block是独立的,不引用外部变量或者作为参赛参与其他函数,处于全局区,是为全局block
void (^block)(void) = ^{ |
2020-05-25 03:21:33.996335+0800 DDD[12987:411279] hello world! |
1.2.2 NSStackBlock(栈区)
当block 被copy 前,会处于栈区
以下为相关示例:
__block int a = 1; |
结果如下:
2020-05-25 11:36:10.746965+0800 DDD[14013:600887] <__NSStackBlock__: 0x7ffeeb06b188> |
1.2.3 NSMallocBlock(堆区)
当block 引用外部变量,此时block 会从全局区移动到堆区
__block int a = 10; |
2020-05-25 03:22:31.565749+0800 DDD[13022:421048] a 10 |
1.3 Block 的常见使用
作为局部变量
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {
// do sth..
};作为属性
@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);c
作为函数声明中的参数
- (void)someMethodThatTakesABlock:(returnType (^)(parameterTypes))blockName;
作为函数调用中的参数
[someObject someMethodThatTakesABlock:^returnType (parameters) {
// do sth
}];作为typedef
typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {
// DO STH
};
二、循环引用及解决
2.1 循环引用的产生
举一个常见的循环引用的例子
- (void)viewDidLoad { |
当示例代码中①处 self 持有了属性 block,而 ②种 block 代码块又持有了 self,这样会导致相互引用,最终系统在执行 dealloc 方法时,向任何一个变量发送release消息时,两者都因为引用其他变量而无法释放,最终会引起内存泄漏。示意图如下:
2.2 循环引用的解决
此时我们只讨论由block 引起的循环引用
2.2.1 使用weak 修饰
可以考虑对当前持有block 的对象,进行weak 修饰,创建一个弱引用指向它(原理为加入到弱引用计数表,而非引用技术表),但是并不会增加引用计数。
当前案例中,引用block 的是self,所以创建一个weak 对象 weakSelf。
如下所示,即可跳出 self - block - self 的循环引用
__weak typeof(self) weakSelf = self; |
2.2.2 weak-strong-dance(多级block)
在多级block 的场景下,弱引用会造成一些意外的情况。
对上面的例子做一些小改动,对block代码块执行的NSLog
函数,做一个延迟3秒执行操作,并让这个执行在页面消失之后,也就是dealloc 之后,看看是否还能打印 weakSelf.name
。
新的代码如下:
- (void)viewDidLoad { |
打印结果如下:
2020-05-25 12:43:05.876909+0800 DDD[14542:686386] 进入dealloc |
可见,在进入dealloc 之后,添加的弱引用,已经被释放掉了,而设计的3秒后执行 weakSelf.name
也因为 weakSelf 为 nil 了,所以打印为null
解决:在代码块内,添加临时的强引用,将weakSelf
添加到强引用表里,会让dealloc 延迟执行,而当执行完之后block 会对临时的强引用进行释放,避免了循环引用的产生。
代码如下:
- (void)viewDidLoad { |
打印结果及顺序:
2020-05-25 12:49:42.141133+0800 DDD[14610:698670] 初始name hello |
从结果可以看出,12:49:42 秒进入本页面,47秒后 进入打印strongSelf.name 成功,同时立刻执行dealloc
方法。可见修改是成功的。
2.2.3 用临时VC 变量替代
在上文中,都是通过weak 对self修饰,来避免循环引用,实际上,还可以对self 所代替的Controller 引入临时变量,来解决循环引用,话不多说,实战一下。
__block SecViewController *vc = self; |
- 创建一个临时变量vc,使用__block 修饰,因为后面会对他做修改。
- 在block内部进行打印属性,完毕后置空,跳出循环引用。
打印结果如下:
2020-05-25 12:54:28.262738+0800 DDD[14645:705623] 初始name hello |
可见通过ViewController 来替代self 也算可用性的
2.2.4 将VC 作为block 变量替代
如果block 是可以引入参数的,可以将控制器self 传入进去,作为临时变量,打印后block 会自动销毁,无需担心循环引用,具体如下:
创建block ,引入当前VC 的引用,的代码如下
@property (copy, nonatomic) void (^block)(SecViewController *); |
执行函数为:
self.block = ^(SecViewController *vc){ |
此时通过self.block(self)
传入self 的引用,block 内部会自动创建一个新的临时变量指向self,该临时变量使用后会销毁。打印机如果如下;
2020-05-25 12:57:17.929178+0800 DDD[14671:710117] 初始name hello |
三、底层探索
3.1 Block 的本质
Block 是一种匿名函数,它的类型是一种对象,本质是结构体。
为了证明这一点,对main.m 进行cpp 源码分析,现在main.m 生成一个block
void (^block)(void) = ^{ |
进入项目文件下,输入编译命令:xcrun -sdk iphonesimulator clang -rewrite-objc main.m
得到main.cpp
打开查看相关cpp 实现如下:
OC 代码如下:
void (^block)(void) = ^{
};
block();C++ 源码如下:
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);将代码精简一下成为:
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
(block)->FuncPtr)(block);
可见主函数为__main_block_impl_0
,在C++ 源码中的部分为:
struct __main_block_impl_0 { |
可见,我们平常使用的代码块block代码块是一个结构体,有以下几个部分组成
- block 实现 - __block_impl,这个是个指针函数,生成后会返回
- block 描述信息 - __main_block_desc_0
- 内部构造函数 - __main_block_impl_0。主要执行了 生成薪的impl 的函数赋值(如isa,flags,funptr)
继续挖, __block_impl 类型的结构体,实现如下:
struct __block_impl { |
因为有熟悉的isa
的存在,可知其也算个对象类的结构体。
3.2 Block 自动捕获外部变量
接下来看block 是如何捕获外界变量的,一共分为3重大的类型:
- 局部变量
- 局部静态变量
- 全局变量和全局静态变量
3.2.1 局部变量捕获
局部变量的捕获,是指的捕获
此时生成一个变量 int a,在代码块对其进行引用,如下:
int a = 10; |
继续进行cpp 编译,发现int main
函数下,变成了如下
int a = 10; |
通过精简,其内容如下:
int a = 10; |
可见__main_block_impl_0
传入了新的参数a
,接下来看如何实现,新的 main_block_impl_0
结构体如下:
struct __main_block_impl_0 { |
可见这里比起之前的结构体,生成了一个新的属性a
,在构造函数里,发现了__main_block_impl_0
中也多了一句a(_a)
的代码,可以猜测是生成了一个新的a
,将传入的内部属性_a 赋值给 a
继续看main_block_func_0
的实现如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
此时静态函数__main_block_func_0
的内部对传入的block 进行了处理——接受了其内部的变量a
——通过自身新生成一个a
,就是这行代码
int a = __cself->a; // bound by copy |
自动生成的注释很清晰的说明了原理,通过copy
绑定新的值,所以说是值捕获。最终通过NSLog
打印新的变量a
3.2.2 局部静态变量捕获
局部静态变量的捕获,是指针捕获
接下来尝试一下,在block内部生成一个用static
修饰的局部静态变量,看看如何捕获
static NSInteger num = 3; |
进入项目文件下,输入编译命令:xcrun -sdk iphonesimulator clang -rewrite-objc main.m
得到main.cpp
struct __main_block_impl_0 { |
变化:
生成了新的整形指针
NSInteger *num;
将传入的
NSInteger *_num
赋值给num代码块调用有所不同,如下:
// 原函数
NSInteger (*block)(NSInteger) = ((long (*)(NSInteger))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &num));
// 精简后
__main_block_impl_0(&__main_block_func_0, &__main_block_desc_0_DATA, &num)可以清晰的看出来,此时传入的
num
是一个指针,可见,是进行了指针拷贝,而不是值拷贝,所以这里num 得到了改变,打印结果会是2
2020-05-27 00:11:06.899579+0800 DDD[25215:2116517] 2 |
3.3.3 全局变量、全局静态变量的捕获
全局变量、全局静态变量的捕获是直接取值
这次来点刺激的,把所有用的到的都写到block 里面,再验证下cpp 会发生什么
static NSInteger num3 = 300; // 静态变量 |
进入项目文件下,输入编译命令:xcrun -sdk iphonesimulator clang -rewrite-objc main.m
得到main.cpp
不管了,就先一股脑儿的贴上来吧,接下来分析:
static NSInteger num3 = 300; // 全局变量 |
分析:我们看到了,num、num2、num5 在block 的构造函数__main_block_impl_0
中都有了各自的实现,有的是取值,有的是通过forwarding 传递指针。
但是num3,和num4 呢?通篇下来,没有做任何处理,在block 函数内部,通过NSLog 直接取值了……
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_ym1km6hs1_510g123chch8bm0000gp_T_main_8fd68e_mi_2, (long)num3); |
结论:静态变量、全局静态变量,因为他们存在全局存储区,占用静态的单元,所以block 调用时是直接把值取过来用,并不需要指针引用。
3.3.4 小结
捕获变量的流程,除了静态变量和全局变量以外,都是block 在内部自动生成一个新的变量,用来接收外部变量。
3.3 Block 为什么要捕获Block()
block() 这种写法和普通的函数不一样,通过上文可以看到,最终实现的代码为
block->FuncPtr(block); |
分析一下,这里是 block 结构体执行了其内部函数指针指向的函数,即声明一个函数式的属性,在任何想要调用的的地方调用。最终我们的确是调用了。
3.4 __block 的原理
为了让外部变量在block 内部得到更改,我们通常在外部对变量使用__block 的修饰符,究竟发生了什么呢?
那么这次,对外部加上__block
试试
__block int a = 10; |
3.4.1 CPP分析
可以看到,构造函数结构体有了变化;
struct __main_block_impl_0 { |
与之前的变量不一样,生成了一个__Block_byref_a_0 *a; // by ref
的结构体指针对象,点击查看,结构如下:
struct __Block_byref_a_0 { |
可见和类的结构类似,有isa、数据a、内存大小_size、类似链表的__forwarding
指针。结合__main_block_impl_0
的结构,可以清楚的看到,内部创建的一个结构体指针__Block_byref_a_0 *a
,而 __a 的指针指向了 a。即,这里产生了一次指针拷贝。
接下来看看函数的实现:
(a->__forwarding->a)++; |
3.4.2 源码分析
在上文中,我们看到关键函数是_Block_object_assign
,打开libclosure
查看相应的函数实现,得到如下:
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point |
注释简单的翻译是:当block 或者block 引用持有对象时,他们的拷贝常规帮助者会使用它们的入口点来做分配工作。
里面的是现实对持有对象类型的switch,重点关注BLOCK_FIELD_IS_BYREF
,注释已经说了
copy the onstack __block container to the heap
拷贝栈内__block 容器到堆
太棒了, 这就是我们需要的实现函数,即对__block 的方法做出相应,拷贝到堆区,再来看函数 _Block_byref_copy(object)
具体的实现:
static struct Block_byref *_Block_byref_copy(const void *arg) { |
在这里可以看到,具体的实现了:
拷贝新的对象到堆区,并置空
isa
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;将新的拷贝对象的flag 与之前栈区对象的flags的相等
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;将新的对象指向原对象,即进行指针拷贝——这个是__block 的关键,新的指向原对象,与原地址指向同一个内存空间,故具备修改能力!
// 问题 - __block 修饰变量 block具有修改能力
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy新对象大小与原对象大小相等
copy->size = src->size;
总结:可见生成的a 是一个__Block_byref_a_0
类型结构体引用,而不是值拷贝的引用。**__block 在作用于内部生成了变量的指针,通过改变指针指向的地址,来即改变原变量。**
原理图如下:
四、签名
4.1 block 结构分析
block 的结构如下:
struct Block_layout { |
分析如下:
isa- 指向block父类的指针
用来描述block 对象的flags
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};预留数值
函数调用invoke
描述信息 struct Block_descriptor_1 *descriptor; //
此处的描述信息,是动态的,如果flags 中
BLOCK_HAS_COPY_DISPOSE
值存在,则存在Block_descriptor_2
,其结构如下;// 可选
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};包含的是拷贝函数copy以及销毁函数 dispose
如果flags 中
BLOCK_HAS_SIGNATURE
存在,即block 包含有签名存在,则存在Block_descriptor_3
方法签名就在3里面:
- 方法签名
- 方法布局
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};// 引入的变量等
4.2 block签名的访问
上文中提到的Block_descriptor_2
、Block_descriptor_3
,都可以通过 Block_descriptor_1
的内存偏移进行访问到,block 的签名为@?
4.2.1 代码部署:
先设定一个最简单的代码块块用来检查:
void(^block)(void) = ^{ |
4.2.2 汇编断点配置
连接真机。配置汇编监控打开:Xcode——Debug——Debug Workflow——Always show Dissembly
对block 代码块打断点,运行程序
会跳转到汇编的main 步骤,通过
Step Over
键进入到objc_retainBlock
函数下打印当前x0寄存器:
register read x0
得到当前运行的block 函数的位置为0x0000000104afc028
打印地址为
0x0000000104afc028
,得到block的类型、签名和内部实现地址:<__NSGlobalBlock__: 0x104afc028>
signature: "v8@?0"
invoke : 0x104afa01c (/private/var/containers/Bundle/Application/3F9F1F87-526B-499C-93CE-784F88CE7DB4/DDD.app/DDD`__main_block_invoke)具体的细节请见下图
4.2.1 访问 Block_descriptor_2
当block 的block_layout
结构中flag 参数BLOCK_HAS_COPY_DISPOSE
值为1 即可通过内存偏移访问到,相关原理可以见下方代码:
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock) |
函数实现中,第3行desc += sizeof(struct Block_descriptor_1);
即内存地址偏移Block_descriptor_1
的内存位,即可访问到Block_descriptor_2
。
4.2.2 访问 Block_descriptor_3
同理,在该block
的flag 值 BLOCK_HAS_SIGNATURE
为正时,亦可以通过Block_descriptor_1
的内存偏移而访问到Block_descriptor_3
,当然,如果此时BLOCK_HAS_COPY_DISPOSE
值也为1,必须叠加Block_descriptor_2
的偏移位
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock) |
五、block的copy 分析
5.1 源码
在libclosure
源码中,runtime.cpp
这一页,block_copy
的函数实现如下所示:
void *_Block_copy(const void *arg) { |
5.2 分析
我们知道,基于block
的性质,在block 的使用中,当block
被copy 时,会从堆区到栈区进行copy
,具体发生了什么,不妨读一读源码,看看苹果工程师是如何设计block
的copy
实现:
先判断block 的类型:先强转
arg
获取block_layout ,判断其flagsaBlock = (struct Block_layout *)arg;
如果是堆
block
,即BLOCK_NEEDS_FREE:创建的对象需要程序员手动释放——由于此时block 空间已经初始化,所以不做拷贝,只增加引用计数即可,如下;if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}如果是全局
block
–BLOCK_IS_GLOBAL,直接返回else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}如果是栈区block:进行一次拷贝操作,拷贝到——>堆区。
else {
// Its a stack block. Make a copy.
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}创建一个新的
Block_layout
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);如果有函数指针调用,将新的Block_layout 指针指向原函数实现,即替换原方法实现:
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;把原来
aBlock
的数据通过内存拷贝到堆区memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
重置引用计数
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);重置isa 指向到一个新的堆block
result->isa = _NSConcreteMallocBlock;
5.3 小结
5.3.1 操作copy 结果
- 对栈block进行copy:拷贝一份到堆区
- 对全局block进行copy:仍是全局block
- 对堆block进行copy:增加引用计数
5.3.2 总结图
俗话说的好,有图有真相,将上述流程汇总成一张图,是这样的
六、release 与dispose
当block 需要释放时,系统内做了哪些工作呢?先看看_Block_object_dispose
函数如下:
void _Block_object_dispose(const void *object, const int flags) { |
可以看到,除了BLOCK_FIELD_IS_BYREF
类型的情况,此种情况为通过__block修饰变量进入,其他情况直接调用release 或者不用操作即可。
BLOCK_FIELD_IS_BYREF
的情况下,需要调用release 方法,即_Block_byref_release(object)
,继续查看如下
static void _Block_byref_release(const void *arg) { |
释放的过程比较简单:
isa 指回原对象
byref = byref->forwarding;
生成一个新的
Block_byref_2
指针``byref2,指向
byref`下个内存位struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
通过
byref2
向原blog引用发送摧毁消息(*byref2->byref_destroy)(byref);
释放原block(byref)
free(byref);
七、总结
本片篇幅较多,主要从block 的结构、block 循环引用的原理解决、block捕获外部变量,__block 究竟做了什么,以及源码层面上做了大量的分析。总的来说block是很巧妙的一项涉及,合理地用好,可以帮我们极大地提高工作效率。