Block介绍
Block是将函数及其执行上下文封装起来的对象。
下面代码的改写过程,大体能够体现出Block是如何实现的
1 | //相当于block中用到的变量 |
改写成block形式如下:
1 | int varId = 1; |
上面block的写法看起开block中用的变量与block的执行方法体的调用是分离的,但其实block中的内部实现是将用到的变量和block执行方法体封装成了对象,看起来像是这样:
1 | @interface CallbackObject:NSObject |
之后这步的改写,就类似Block在内部为我们所做的操作:将函数及上下文封装成对象。
那么Block真正的内部实现是怎样的呢?我来通过clang
来将源码编译成cpp文件后,来窥探一下Block的内部实现。
源码分析
1 | @implementation DMBlock1 |
上面这段源码通过clang -rewrite-objc DMBlock1.m
执行后生成如下cpp文件
1 |
|
分析代码前,先说一下,编译器的命名规则:类名+方法名+block_impl或block_func或block_des+出现的顺序
下面开始逐一分析源码,首先看一下源Block代码块中的
1 | ^(){ |
被编译成了
1 | static void __DMBlock1__dm_method_block_func_0(struct __DMBlock1__dm_method_block_impl_0 *__cself) { |
这个函数有一个参数__cself
,相当于Objective-C中的self,为指向Block值的变量。
1 | - (void)dm_method{} |
相应的被编译成了1
static void _I_DMBlock1_dm_method(DMBlock1 * self, SEL _cmd) {}
block的显示最终变成了在栈上生成的__DMBlock1__dm_method_block_impl_0
结构体实例的指针,这个结构体的构造函数的第一参数,就是上面的那个函数指针,第二参数就是__DMBlock1__dm_method_block_desc_0_DATA
再看block调用部分,被编译成1
(*MyBlock->FuncPtr)(MyBlock);
Block的调用就是直接的函数调用。
最后看一下,直接可以证明block即是对象的结构体
1 | struct __block_impl { |
这里__DMBlock1__dm_method_block_impl_0
结构体相当于基于objc_object
结构体的OC类对象的结构体。另外,对其中的成员变量isa的初始化如下:
1 | isa = &_NSConcreteStackBlock;//栈 |
_NSConcreteStackBlock
相当于class_t
结构体实例。在将Block作为对象处理时,关于该类的信息放置于_NSConcreteStackBlock
中。
总结
Block是将函数及其执行上下文封装起来的对象。
Block的调用就是函数调用。
Block的截获变量
- 局部变量
- 基本数据类型 截获其值
- 对象类型 连同对象所有权修饰符一同截获
- 静态局部变量 以指针形式截获
- 全局变量 不截获
- 全局静态变量 不截获
源码分析
1 | //全局变量 |
ARC -fobjc-arc
MRC -fno-objc-arc
上面这段源码通过clang -rewrite-objc -fobjc-arc DMBlock1.m
执行后生成如下cpp文件
1 | int global_var = 4; |
Block的截获变量只针对Block中使用的局部变量。
被截获的变量,变成了__DMBlock__dm_method_block_impl_0
结构体的成员变量
注意:
C语言数组的无法实现截获变量,如需使用,需要改为指针形式。
__block修饰符
__block修饰符一般使用在对截获变量进行赋值操作时。
- 需要添加__block
- 基本数据类型
- 对象类型
- 不需要需要添加__block
- 静态局部变量
- 全局变量
- 静态全局变量
源码分析:
1 | @implementation DMBlock2 |
编译后:
1 | struct __Block_byref_multiplier_0 { |
__block 修饰的变量变成了对象
Block与__block变量的实质
名称 | 实质 |
---|---|
Block | 栈上Block的结构体实例(对象) |
__block变量 | 栈上__block变量的结构体实例(对象) |
只要Block不截获局部变量,就可以将Block用结构体实例设置在程序的数据区(_NSConcreteGlobalBlock)
虽然通过clang转化的源代码通常是_NSConcreteStackBlock
类对象,但实际上是这样的:
- 声明的全局block变量属于
_NSConcreteGlobalBlock
- Block语法中的表达式中没有截获变量时,该Block属于
_NSConcreteGlobalBlock
在ARC有效的情况下,大多数情形下编译器会恰当的进行判断,自动生成将Block从栈上复制到堆上的代码。
1 | typedef int (^blk_t)(int); |
该源码为返回配置在栈上的block函数。即程序执行中从该函数返回函数调用方时变量作用域结束,因此栈上的block被废弃。但该源码通过对应ARC的编译器转化如下:
1 | blk_t func(int rate) |
其中objc_retainBlock()
中调用了_Block_copy(tmp)
这个方法会将在栈上的block复制到堆上。
编译器不会为我们自动添加的情况:
- 向方法或函数的参数中传递Block时
编译器会为我们自动添加的情况:
- Cocoa框架的方法且方法名中含有usingBlock等时
- GCD的API
会将栈中Block复制到堆上的操作:
- 对block调用copy
- Block作为函数返回值时
- 将Block赋值给__strong修饰的变量时
- 方法名中包括usingBlock的Cocoa框架方法或GCD的API中传递Block时
__forwarding
可以实现无论__block
变量配置在栈上还是堆上都可以正确访问__block
变量。
Block的内存管理
Block的类型
类 | 对象的存储区域 | copy后的结果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 堆 |
_NSConcreteGlobalBlock | 程序的数据区(.data区) | 什么都不做 |
_NSConcreteMallocBlock | 堆 | 增加引用计数 |
block 从栈复制到堆时对__block变量产生的影响
__block变量的配置存储区域 | Block从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
调用copy函数和dispose函数的时机
函数 | 调用时机 |
---|---|
copy函数 | 栈上的block复制到堆上时 |
dispose函数 | 堆上的block被废弃时 |
在ARC无效的情况下,可以使用__block
修饰符用来避免Block中的循环引用。这是由于当Block从栈复制到堆时,若Block中使用的变量为附有__block
修饰符的id类型的或对象类型的局部变量时,不会被retain,若Block中使用的变量为没有__block
修饰符的id类型或对象类型的局部变量,则被retain。
Block的循环引用
1 | - (void)dm_methodF |
Block被self持有,而Block中又持有self
可以通过添加下面的代码解除循环
1 | __weak typeof(self)weakSelf = self; |
1 | - (void)dm_methodG |
上面那段代码在MRC下不会产生循环引用,这是因为在MRC下Block不会对__block强引用
在ARC下会产生循环引用
可以通过以下方法解决循环引用
1 | int(^Block)(int) = ^int(int num){ |
弊端如果block不调用那么循环引用就会一直存在
QA
Q:
1 | - (void)dm_methodA{ |
Q:
1 | - (void)dm_methodC{ |
Q:
1 | - (void)dm_methodE |
Q: 下面代码运行会发生什么?
1 | - (void)dm_methodI |
A:
程序会崩溃。这是因为getBlockArray
方法执行结束后,栈上的block会被释放掉的缘故。
但如果将getBlockArray
方法改成下面的,就不会崩溃,这是因为执行a =
和b =
时,会将block从栈拷贝到堆上了。
1 | - (id)getBlockArray |
Q:
1 | - (void)dm_methodK |
参考
《Objective-C 高级编程 - iOS与OSX多线程和内存管理》