什么是 Block, 为什么引入 Block
Block 是 Objective-C 对 Closure(闭包)的实现, 本质上是一个带有自动变量(局部变量)的匿名函数。
比如说 void (^blk)(void) = ^{};
block
对象就是一个struct
实例,更进一步的是一个OC对象。blk
指针就是一个函数指针,指向了一个__main_block_impl_0
对象。block
调用就是把block
对象中的函数指针取出来,然后把自己当参数调用一下。
Block 简单使用如下:
1 | int (^blk)(int a) = ^(int a) { |
也可以用 typedef
定义 Block
1 | // 定义 |
Block实现原理
比如如下一个 main.m,
1 |
|
执行
1 | clang -rewrite-objc main.m |
将其从oc转换成c++, 截取一段
1 | struct __block_impl { |
__main_block_impl_0
结构体
1 | struct __main_block_impl_0 { |
__main_block_impl_0
结构体中有两个数据成员,一个构造函数:
第一个成员:__block_impl
结构体, __block_impl
可以认为是__main_block_impl_0
的基类。
__block_impl
而__block_impl
其第一个数据是isa
指针,OC对象的第一个成员也是isa
指针,指向所在类的类对象,实质上,__block_impl
是一个OC对象。成员 FuncPtr
是一个函数指针,指向block实现函数。
1 | struct __block_impl { |
__main_block_desc_0
第二个成员:__main_block_desc_0
可以认为是子类中的数据。有两个成员,reserved 预留它用,暂时不表;Block_size 用来表示 __main_block_impl_0 的大小,通过 sizeof(struct __main_block_impl_0) 获取.
1 | static struct __main_block_desc_0 { |
构造函数:__main_block_impl_0
构造函数:__main_block_impl_0
,它有三个参数:函数指针fp,__main_block_desc_0
指针和一个flags。
1、函数指针fp,用于构造__main_block_impl_0
时,指向block的函数。
2、对象参数__main_block_desc_0
保存对象私有信息。
Tips:
__main_block_impl_0
结构体名称怎么来的? 是Clang根据所在函数、函数中block定义的顺序自动生成的。
构造函数的定义:
1 | __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { |
impl
对象的isa
指针指向一个外部的全局对象:impl.isa = &_NSConcreteStackBlock;
说明这个block是一个栈Block。
1 | extern "C" __declspec(dllexport) void *_NSConcreteGlobalBlock[32]; |
Block对象的创建
接下来就是block对象的生成与赋值:
1 | void (^blk)(void) = ^{}; |
生成 C 之后是
1 | void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); |
这句代码是通过构造函数 __main_block_impl_0
创建一个实例,第一个参数是block实现代码的函数指针,第二个参数是一个__main_block_desc_0
实例:__main_block_desc_0_DATA
。
blk
是一个函数指针,指向了__main_block_impl_0
实例的地址。
Block的执行
1 | ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); |
block对象的FuncPtr
指向的是block的实现函数:
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
这个函数只有一个参数,就是block自己。
block调用的时候就把函数指针FuncPtr
取出来并执行,参数传自己blk
。
Block对象大小
block对象就是一个__main_block_impl_0
实例,在64位机器上,这个实例大小32字节,其中__block_impl
有24字节,其中void*
8字节,int
4字节,__main_block_desc_0*
是一个指针8字节,所以一个block对象是32字节。
如果block自动截获变量,__main_block_impl_0
结构体中会增加自动截获的变量,相应的会增加 block 对象的大小。也就是 block 对象大小最小是32字节,如果截获了自动变量,block 对象会变大。
Block 捕获外部变量
C语言中变量一般可以分为以下 5 种:
- 自动变量
- 函数参数
- 静态变量
- 静态全局变量
- 全局变量
函数参数先不管, 自动变量我们知道需要用__block
修饰才能修改值, 我们看看另外三个
1 | int global_i = 1; |
翻译下
1 | clang -rewrite-objc main.m |
__main_block_impl_0
结构定义变成了
1 | struct __main_block_impl_0 { |
结构中捕获了 static_k
的指针
再看下Block 执行代码
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
可以看到全局变量是直接使用的, 静态变量是通过指针操作修改的.
构造函数
1 | __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val_l, int flags=0) : static_k(_static_k), val_l(_val_l) { |
这个构造函数中,自动变量和静态变量被捕获为成员变量追加到了构造函数中.
Block 定义如下
1 | static int static_k = 3; |
val_l 的值直接传到了 __main_block_impl_0
中, 所以修改 val_l 无意义, OC 的编译器直接禁止了对 Block 捕获的自动变量做修改.
__block
使用 __block 修饰 var_l 后, 定义变为了
1 | struct __main_block_impl_0 { |
Block 执行变成了
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
val_l 是一个__Block_byref_val_l_0
结构体(这个结构体就是代码中的int val_l = 4;
中的 val_l 变量, 只是包了一层), 定义如下
1 | struct __Block_byref_val_l_0 { |
再看看结构体赋值的地方, 跟Block 定义地方在一起, 先看看 Block 定义
Block 的定义变成了
1 | static int static_k = 3; |
其中__block int val_l = 4;
变成了__Block_byref_val_l_0
结构体
1 | __Block_byref_val_l_0 val_l = { |
__forwarding
就是(__Block_byref_val_l_0 *)&val_l,
即结构体自己的内存地址, val_l->__forwarding->val_l
先是访问结构体, 然后访问自己, 再访问结构体中的val_l
, 然后对结构体中的val_l
的执行修改操作, 达到了修改自动变量的目的
为什么要引用 __forwarding
引入 __forwarding
目的是为了将 Block 从栈复制到堆上的时候, 会一起复制 __Block_byref_val_l_0
这个结构体(也是一个对象), 通过上面的分析, 我们知道
val_l
变量实际是存在__Block_byref_val_l_0
结构体中的, 栈上的我们简称结构体 A
,- 如果Block 被复制了, 堆上也会复制出
结构体 B
, - 栈上面的 Block 修改
结构体 A
的的数据, 堆上的 Block 修改的是结构体 B
的数据, - 最终导致 val_l 的修改对象不一致, 逻辑上有 bug.
解决办法就是 引入了 __forwarding
Block 从栈复制到堆上后, 栈上的结构体 A
的__forwarding
指向了堆上的 Block, 对栈上的val_l
的修改实际改的是堆上的val_l
, 即 val_l(栈)->__forwarding(指向堆)->val_l(堆上的val_l).
同时使用结构体代替 var_l
还有一个好处是, 栈的作用域结束, val_l
就被销毁了, 但是 结构体可以一起被复制到堆上, 保证作用域结束后, 堆上的 Block 仍然能访问到 var_l
.
Block 种类
OC中,一般Block就分为以下3种,_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock。先来说明一下3者的区别。
从捕获外部变量的角度上来看
_NSConcreteStackBlock:
- 只用到外部局部变量、成员属性变量,且没有强指针引用的block都是StackBlock。
- StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了。
_NSConcreteMallocBlock:
- 有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制
_NSConcreteGlobalBlock:
- 没有用到外界变量或只用到全局变量、静态变量的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束。
从持有对象的角度上来看
- _NSConcreteStackBlock是不持有对象的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//以下是在MRC下执行的
NSObject * obj = [[NSObject alloc]init];
NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);
void (^myBlock)(void) = ^{
NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
};
NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);
myBlock();
// 输出
1.Block外 obj = 1
2.Block外 obj = 1
Block中 obj = 1 - _NSConcreteMallocBlock是持有对象的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//以下是在MRC下执行的
NSObject * obj = [[NSObject alloc]init];
NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);
void (^myBlock)(void) = [^{
NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
} copy]; // 这里对 Block copy 了一次
NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);
myBlock();
[myBlock release];
NSLog(@"3.Block外 obj = %lu",(unsigned long)obj.retainCount);
// 输出
1.Block外 obj = 1
2.Block外 obj = 2
Block中 obj = 2
3.Block外 obj = 1 - _NSConcreteGlobalBlock也不持有对象
1
2
3
4
5
6
7
8
9
10
11//以下是在MRC下执行的
void (^myBlock)(void) = ^{
NSObject * obj = [[NSObject alloc]init];
NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
};
myBlock();
// 输出
Block 中 obj = 1
在ARC环境下,Block也是存在__NSStackBlock的时候的,平时见到最多的是_NSConcreteMallocBlock,是因为我们会对Block有赋值操作,所以ARC下,block 类型通过 = 进行传递时,会导致调用objc_retainBlock->_Block_copy->_Block_copy_internal方法链。并导致 NSStackBlock 类型的 block 转换为 NSMallocBlock 类型。
1 |
|
这种情况就是ARC环境下Block是__NSStackBlock的类型。
以下4种情况,系统都会默认调用copy方法把Block复制
- 手动调用copy
- Block是函数的返回值
- Block被强引用,Block被赋值给__strong或者id类型
- 调用系统API入参中含有usingBlcok的方法
但是当Block为函数参数的时候,就需要我们手动的copy一份到堆上了。这里除去系统的API我们不需要管,比如GCD等方法中本身带usingBlock的方法,其他我们自定义的方法传递Block为参数的时候都需要手动copy一份到堆上。
对 Block 执行 copy
- 对_NSConcreteStackBlock进行copy,会深拷贝到堆区,创建一个新的_NSConcreteMallocBlock。
- 对_NSConcreteMallocBlock进行copy,只会增加引用计数,不会分配新内存。
Block 带来的问题
Block 的出现方便了程序逻辑, 避免 delegate 导致各种逻辑被分在了代码不同的位置(比如 viewDidAppear 触发请求, 在 delegate 回调方法才能得到请求的回调, 如果这样的逻辑一多, 代码会很复杂), 但是 Block 也带来的一个问题循环引用.
什么是循环引用
对象A持有对象B,调用B的block参数方法,在里面使用了self。在使用block我们都会默认在里面使用weakself,网上搜了很多解释都是为了防止循环引用,以防self被持有导致内存泄露。
什么情况会循环引用
1 | // 内存泄露 |
在这个例子, A -> b -> Block -> A, 才会循环引用(method2 是类方法, Class B 保存了 Block 也会导致循环).
我们一般会将 A 设成 weak, 来打破这个循环链,
还有个比较好的习惯, Block 执行完之后, 解除 b 对 Block的引用(如果只用一次的话), 也能避免循环引用.
Block 内调用 super 引发的循环引用
不仅 self 会触发循环引用, super 也会触发
1 | @interface TestObj : NSObject |
Q1:
这个有点经验的都知道泄漏了,因为隐式包含调用了 self->_xxx ,还是通过 self 再进行偏移量获取的。这里就不再进行分析了,看完分析 Q2 之后你也可以尝试分析 Q1 的问题。
解法也很简单,注意因为是读取偏移量,如果不对 self 判空的话,当 self 为 null 时,进行 self->_capture 的访问是会出现 MACH 类型的崩溃的。
1 | @weakify(self); |
Q2:
实际是会内存泄露, 并且编译器会提示 @strongify(self);
未使用.
但是我们知道 super 会被替换为 objc_msgSendSuper(self), 既然会被换成 self ,那就会被 weak/strong dance 所替换,然后就万事大吉了. 难道 宏替换 与 objc_msgSendSuper 不是在同一个步骤中进行的嘛?
结论: 宏的替换在 Preprocessed 阶段,super 的变更在 compile 阶段,因此对 self 进行 weak/strong dance 并解决不了 super 导致的循环引用问题 (Rewrite C++ 有问题, 实际没有把 super 换成 self)。关于 objc_msgSend Stub 可以参考 【WWDC22 110363】App 包大小优化和 Runtime 上的性能提升
怎么发现循环引用
对于内存泄漏,苹果提供了两种常用的检测方式:
- 静态检测,即使用XCode分析功能,Product -> Analyze。
- 动态检测,即使用Instruments工具中的Allocation。
苹果提供的检测方式需要人工观察,功能很强大,就是比较麻烦。通常对于一般的工程来说,内存泄漏的场景中还是以UI居多,第三方工具MLeaksFinder针对这种场景就孕育而生,在运行过程自动检测UIViewController和其子UIView对象的内存泄漏,然后通过控制台以及弹框告知开发者并去修改。
MLeaksFinder
MLeaksFinder工具检测的出发点是从UIViewController入手,利用UIViewController的生命周期来判断UIViewController以及view等内存有没有被释放,UIViewController的生命周期如下图:
退出UIViewController,要么被pop,要么就是被dismiss的,退出后包括它的view,view的subviews等都将会很快被释放(除非设计成单例,或者强引用持有,一般很少这样做)。所以只需在UIViewController被pop或dismiss一段时间后,判断UIViewController,它的view,view的subviews等是否还存在即可。
判断这些对象是否还存在,可以为基类NSObject添加方法-(BOOL)willDealloc,该方法的作用是,先用一个弱指针指向self,并在一段时间(2秒)后,通过这个弱指针调用-(void)assertNotDealloc方法,而assertNotDealloc这方法的作用就是提示开发者哪个对象泄漏了,如果对象泄漏了,弱指针调用assertNotDealloc方法就会执行相应逻辑,对应的源码如下:
1 | - (BOOL)willDealloc { |
UIViewController显示时会调用viewWillAppear,不显示时会调用viewDidDisappear,不显示包括退出,被移出,以及被覆盖的。所以就可以在viewWillAppear方法处做一个标记kHasBeenPopedKey,表示这个UIViewController没有被退出,值为NO,然后在viewDidDisappear方法处判断这个标记kHasBeenPopedKey,如果标记值还是为NO,就不处理内存是否还存在的逻辑,如果是YES的,就要过两秒判断下这些对象还存不存在,如果存在即泄漏。然后在UIViewController的pop或dismiss时设置标记kHasBeenPopedKey为YES,代码实现上,使用了AOP技术,通过hook掉UIViewController以下三个方法,加入自己的标记逻辑:
1 | [self swizzleSEL:@selector(viewDidDisappear:) withSEL:@selector(swizzled_viewDidDisappear:)]; |
对于UIViewCtroller的pop和dismiss就要hood掉UINavigationController以下几个方法:
1 | [self swizzleSEL:@selector(pushViewController:animated:) withSEL:@selector(swizzled_pushViewController:animated:)]; |
在这几个方法中,会将标记kHasBeenPopedKey设置为YES,然后在viewDidDisappear就会触发willDealloc逻辑判断内存存在性