iOS Block 总结
Block源码地址
1. Block实质:
Block从C语言角度实质上是能够捕获上下文变量的匿名函数,在创建的时候会捕获所需要的上下文局部自动变量到闭包内部。Block的底层是作为C语言源代码来处理的,支持Block的编译器会将含有Block语法的源代码转换为C语言编译器能处理的源代码,当作C语言源码来编译。Block和__block 最终都会转换为一个C语言的结构体对象。
Block 在OC中的实现如下
struct Block_layout { |
Block 需要了解如下几个方面:
(1) 我们怎么把带有Block的 Objective C 代码转化为 C 代码
(2) 转换后的代码结构是怎样的
(3) Block对局部自动变量,局部静态变量,全局变量,对象,__block变量的捕获
(4) Block 与 __block 的存储特性
(5) Block的循环引用
(6) Block的内存布局
我们使用的转换block的命令行如下:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp |
一个简单常见的例子:
#import <UIKit/UIKit.h> |
转换后的代码:
int globleIntValue = 2; |
2. Block定义
Block类型变量,一般结合用typedef定义:
typedef int (^blockType)(int,int) |
Block 变量可以作为自动变量,函数参数,函数返回值,静态局部变量,静态全局变量,全局变量
Block 定义:
block定义和普通的C语言函数定义类似,只不过多了一个^省去函数名称
^返回值类型 (参数列表) { |
3. __block_impl 结构体
struct __block_impl { |
void (^blk)(void) = ^ { |
转换成:
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,auto_peram1,auto_peram2); |
blk() 实际上执行的是
(*blk->impl.FuncPtr)(blk); |
一个block的组成:
struct __block_impl impl; |
4. Block的变量捕获
block接受的参数属于值传递,可以在block内修改,不对其进行捕获
局部变量的捕获: 当定义block的时候,Block中所使用的自动变量值被保存到Block的结构体实例中,也就是Block自身中,并且保存后就不能改写该值。但是还是可以调用变更该对象的方法。比如NSMutableArray 的 addObject
static局部变量的捕获: 局部静态变量 作用域在block之外,这种情况下block中存储的是静态局部变量的地址,所以当FuncPtr指向的函数调用的时候会通过取地址中存储的值
全局变量的捕获
全局变量并没有被block所捕获,因为全局变量存放在全局区,随时都可以访问,所以当FuncPtr指向的函数调用的时候就会直接取全局变量的值使用。而局部变量超过作用域就会自动回收所以block需要在自身存放一份,以保证其能准确访问。对象类型的捕获:
在没有调用copy的情况下,还没有调用block()的时候对象就已经释放了,说明在栈上的block并没有对所使用的对象强引用,对block 进行一次copy操作会发现在block没有release之前,所引用的对象没有被释放,所以堆上的block强引用了所使用的对象,对对象执行一次release之后,对象的引用计数依然没有成为0,因为block还引用着它。这是因为当对block进行copy操作的时候,block会执行内部的__main_block_copy_0方法。__main_block_copy_0方法执行_Block_object_assign根据变量的修饰符判断对捕获的对象的引用情况(retain或者弱引用)。而当block从堆中移除的时候,会调用与__main_block_copy_0对应的_Block_object_dispose函数,该函数会自动释放引用的auto变量。(栈上block访问了对象类型的auto变量的时候不会对其发生强引用)
block从栈上copy到堆上的时候,block内部会执行copy操作,_Block_object_assign函数回通过auto变量的修饰符判断发生强弱引用。block从堆中移除的时候,block内部会执行dispose,将引用的对象进行释放。
简而言之:
局部变量在block中使用的时候会被block捕获,auto变量是值捕获,而static变量是地址捕获。全部变量不会被捕获。当Block从栈复制到堆上时,block会对id类型的变量产生强引用
5. __block 说明符
在block中允许修改静态局部变量,静态全局变量和全局变量这几种类型,但是对于局部自动变量如果在block里面修改编译器会发出警告,这时候需要在局部自动变量之前添加__block 说明符,__block 可以指定任何类型的自动变量,通过__block修饰的变量将会变成一个结构体实例,只有这样这个值才能被block共享、并且不受栈帧生命周期的限制、在block被copy后,能够随着block复制到堆上
这时候被捕获的对象会转换为
struct __Block_byref_val_0 { |
__main_block_impl_0中也会多出一个
__Block_byref_val_0 *val; |
也就是说局部自动变量添加了__block后该变量将会以__Block_byref_val_0 形式被捕获
有时候__block变量配置在堆上的情况下,也可以访问栈上的__block变量,在这种情况下只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从堆上的__block变量还是从栈上的__block变量都能正确得访问。简而言之:
__block 变量结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是在堆上都能正确得访问__block变量
__block id __strong obj = [[NSObject alloc] init]; 和 id __strong obj = [[NSObject alloc] init] 一样在block没有copy的时候是不持有的,copy后block会持有对象引用
id array = [[NSMutableArray alloc] init];
id __weak array2 = array;
array2 是弱引用,当变量作用域结束,array 所指向的对象内存被释放,array2 指向 nil
如果 __weak 再改成 __unsafe_unretained ,__unsafe_unretained 修饰的对象变量指针就相当于一个普通指针。使用这个修饰符有点需要注意的地方是,当指针所指向的对象内存被释放时,指针变量不会被置为 nil。所以当使用这个修饰符时,一定要注意不要通过悬挂指针(指向被废弃内存的指针)来访问已经被废弃的对象内存,否则程序就会崩溃。
如果 __unsafe_unretained 再改成 __autoreleasing 会怎样呢?会报错,编译器并不允许你这么干!如果你这么写
__block id __autoreleasing obj = [[NSObject alloc] init];
编译器就会报下面的错误,意思就是 __block 和 __autoreleasing 不能同时使用。
6. Block 存储属性
Block 有如下几种存储类型:
_NSConcreteGlobalBlock 程序的数据区域
- Block当作全局变量使用时(block 字面量写在全局作用域时)
- 当 block 字面量不获取任何外部变量时(只使用全局变量以及block参数的时候)
_NSConcreteGlobalBlock不持有对象
_NSConcreteStackBlock 栈
- 除了上述两中情况下Block配置在程序的数据区中以外(换种说法如果只用到外部局部变量、成员属性变量,且没有强指针引用的block就是StackBlock),Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。配置在栈上的Block,如果其所属的变量作用域结束,该Block就被自动废弃。_NSConcreteStackBlock不持有对象
_NSConcreteMallocBlock 堆
- 有强指针引用或copy修饰的成员属性引用的block,配置在全局变量上的Block,从变量作用域外也可以通过指针访问。但是设置在栈上的Block,如果其所属的作用域结束,该Block就被废弃;并且__block变量的也是配置在栈上的,如果其所属的变量作用域结束,则该__block变量也会被废弃。那么这时需要将Block和__block变量复制到堆上,才能让其不受变量域作用结束的影响。_NSConcreteMallocBlock持有对象。
整个存储区域如下图所示:
通过上述结果我们可以看出当block访问了auto变量的时候会变成__NSStackBlock__类型。而其他情况下是__NSGlobalBlock__类型,比较典型的是Block声明在全局区域,以及Block虽然不声明在全局区域但是Block不截获自动变量。
__NSGlobalBlock__类型存在于数据区,__NSStackBlock__存在于栈区。
而在ARC环境下原本__NSGlobalBlock__的block依然是__NSGlobalBlock__类型,而原本是__NSStackBlock__却变成了__NSMallocBlock__存放在堆区。这是因为当我们定义block的时候ARC默认为我们做了一次copy操作。
在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上,只有当
block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法这时候需要手动调用copy,但是比如Cocoa框架的方法且方法名中包含usingBlock等,或者GCD API的情况下,这些在方法或者函数中对传递过来的参数做了复制操作所以不需要copy
copy 的结果:
如果原先存储域处于_NSConcreteGlobalBlock 那么什么都不做
如果原先存储域处于 _NSConcreteMallocBlock 那么将会导致引用计数增加
如果原先存储域处于_NSConcreteStackBlock 那么会将block从栈复制到堆
下面是Block复制和释放的源码:
Block 复制
void *_Block_copy(const void *arg) { |
Block 释放
void _Block_release(const void *arg) { |
简而言之:
访问了auto变量的block是__NSStackBlock__类型,没有访问auto变量的block是__NSGlobalBlock__类型。而对__NSStackBlock__类型进行copy操作就会变为__NSMallocBlock__类型。
在如下情况下栈上的block会被复制到堆上:
在调用Block 的copy 实例方法的时候
Block 作为函数返回值返回时
将Block赋给带有__strong修饰符id 类型的类或者Block类型成员变量时(block作为强指针引用的时候也会自动调用copy)
在方法名中含有usingBlock的Cocoa框架方法,或者GCD API中传递Block时候
在谁都不持有Block的时候block将会被释放
copy 函数会持有截获的对象,以及所使用的__block变量,dispose函数会释放截获的对象以及__block变量,
所以Block中使用的赋给赋有__strong修饰符的自动变量的对象和复制到堆上的__block变量由于被堆上的Block所持有,因而可以超出其变量作用域而存在。
再简而言之:也就是block 被copy到堆上的时候,它所使用的strong类型的对象以及__block变量都会超出作用域而存在。
- __block 变量存储属性
当Block从栈复制到堆上的时候,它所使用的所有__block变量也会被复制到堆上,并被Block持有。在多个Block中使用__block变量的时候,因为最先会将所有的Block配置在栈上,所以__block变量最初也会配置到栈上,在任何一个Block从栈上复制到堆上的时候,__block变量也会一起从栈复制到堆上,并被该Block 持有,当剩下的Block从栈复制到堆的时候,被复制的Block持有__block变量,并增加__block变量的引用计数。
如果配置在堆上的Block被废弃,那么它所使用的__block变量也会被释放。
- 避免循环引用:
如果用self引用了block,block又捕获了self,这样就会有循环引用。因此,需要用weak来声明self,如果捕获到的是当前对象的成员变量对象,同样也会造成对self的引用,同样也要避免。
使用__weak来声明self |
- (void)configureBlock { |
当struct第一次被创建时,它是存在于该函数的栈帧上的,其Class是固定的_NSConcreteStackBlock。其捕获的变量是会赋值到结构体的成员上,所以当block初始化完成后,捕获到的变量不能更改。
当函数返回时,函数的栈帧被销毁,这个block的内存也会被清除。所以在函数结束后仍然需要这个block时,就必须用Block_copy()方法将它拷贝到堆上。这个方法的核心动作很简单:申请内存,将栈数据复制过去,将Class改一下,最后向捕获到的对象发送retain,增加block的引用计数。