更新日期:2019-07-22
Block的实质
我们先写一个最基础的block
1 | int main(int argc, const char * argv[]) { |
使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
转化成C++代码。其中有关键代码如下:
1 | int main(int argc, char * argv[]) { |
将代码简化一下:
1 | int main(int argc, char * argv[]) { |
可以看出我们定义的testBlock
变成了&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)
,执行block的时候将testBlock()
变成了testBlock->FuncPtr(testBlock)
__main_block_impl_0
是一个结构体,相关定义如下:
1 | struct __block_impl { |
__main_block_impl_0
有两个成员变量,分别是__block_impl impl
和__main_block_desc_0* Desc
,还有一个__main_block_impl_0
的构造函数。__block_impl
和__main_block_desc_0
也是两个结构体,其成员变量作用都写在代码注释中,这里就不再说了。
最重要的是其构造函数:
1 | __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { |
在定义block变量的时候,传入的__main_block_func_0
和&__main_block_desc_0_DATA
分别对应了fp
和* desc
。fp
指向的就是__main_block_func_0
函数,它会传给impl.FuncPtr
,另外的isa
的初始值是_NSConcreteStackBlock
,_NSConcreteStackBlock
就相当于class_t
中的结构体类型的。
接着看调用部分,关于block的调用会被转化成testBlock->FuncPtr(testBlock)
,它指向的是__main_block_func_0
函数的地址。这里要注意testBlock->FuncPtr(testBlock)
是我将强制转化取消后的代码,原本应该是(__block_impl *)testBlock->FuncPtr(testBlock)
,因为__block_impl imp
是__main_block_impl_0
这个结构体的第一个成员变量,所以__block_impl imp
的内存地址就是__main_block_impl_0
结构体的内存地址。
所以说block的本质就是Objective-C对象,block的调用就是函数指针的调用。
截获自动变量值
根据《Objective-C高级编程》一书中提到,所谓的“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量被保存到Block的结构体实例(即Block自身中)。
那不同类型变量之间的截获会有区别吗,使用以下代码:
1 | #include <stdio.h> |
转化成C++代码:
1 | static int globalStaticValue = 1; |
从上面的代码可以看出:
- 局部变量会被block直接截获;
- 局部静态变量会被block直接截获其指针,通过指针进行方法;
- 全局变量和全局静态变量并不会被截获,而是直接使用;
截获对象
block是如何截获对象变量的,使用如下代码:
1 | #import <Foundation/Foundation.h> |
使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
命令,转化成C++代码后:
1 | static NSObject *globalObjc; |
如果截获的变量是对象类型的,会将对象变量及其所有权修饰符一并截获。再看一下__main_block_copy_0
和__main_block_dispose_0
。
- 当block进行一次copy操作的时候,
__main_block_copy_0
函数内部调用_Block_object_assign
函数,它根据对象的类型产生强引用或者弱引用; - 当block从堆中移除的时候,
__main_block_dispose_0
函数内部调用_Block_object_dispose
函数,它会自动释放掉引用的变量。
关于截获对象的实验是在ARC环境下执行的,testBlock是_NSConcreteMallocBlock
类型的。如果在非ARC环境下,testBlock是_NSConcreteStackBlock
类型的即Block在栈上,则不会对截获的对象变量进行强引用。
__block修饰符
当我们需要对被Block截获的局部变量进行赋值操作的话,需要添加一个__block
这个说明符,那__block
到底有什么作用呢?
先看一段代码:
1 | #include <stdio.h> |
由于使用了__block
关键字,可以修改变量a的值,所以输出结果是100。
先说明一下为什么不使用__block
就不能修改值。执行block就是调用__main_block_func_0
函数,a
是main
函数中的局部变量,我们不可能在一个函数中去修改另一个函数的局部变量。
将上面代码转换成C++代码,先看一下__main_block_impl_0
这个结构体
1 | struct __Block_byref_a_0 { |
__main_block_impl_0
结构体中多了一个__Block_byref_a_0 *a
而不是int a
。__Block_byref_a_0
也是一个结构体,里面也有一个isa
指针,因此我们也可以将它当做一个对象。另外还有一个__Block_byref_a_0 *
类型的__forwarding
指针和变量a
。关于__forwarding
指针的作用我们会在后面提到。
接着是主函数:
1 | static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { |
在主函数中我们可以看到__block int a = 1;
转化成了__Block_byref_a_0
的一个结构体,其中__forwarding
指针指向这个结构体自己。
最后是Block执行的时候即调用__main_block_func_0
函数:
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
原来的a = 100;
则被转换成__Block_byref_a_0 *a = __cself->a;
和(a->__forwarding->a) = 100;
。这两句代码的意思是先获取结构体中的a
,通过a
结构体的__forwarding
指针指向成员变量a
赋值为100。
所以__block
的本质就是__block将变量包装成一个对象,将截获到的值存在这个对象中,通过对截获的值进行赋值而更改原有的值。
Block存储域
block有下面三种类型
类 | 设置对象的存储域 |
---|---|
_NSConcreteStackBlock | 栈 |
_NSConcreteGlobalBlock | 程序的数据区域 |
_NSConcreteMallocBlock | 堆 |
那么如何确定block的类型?下面这个实验需要在MRC环境下进行测试,代码如下:
1 | int main(int argc, char * argv[]) { |
结果如下:
通过打印block的类型可以知道
类 | 环境 |
---|---|
_NSConcreteStackBlock | 访问了自动变量值 |
_NSConcreteGlobalBlock | 没有访问自动变量值 |
_NSConcreteMallocBlock | _NSConcreteStackBlock使用了copy |
copy操作对不同类型block的影响:
Block的类 | 副本源的配置存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
block的copy操作能保证block不容易被销毁。但是在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,即我们打印出来的block是_NSConcreteMallocBlock
类型的。
这些情况有:
- block作为返回值的时候;
- block赋值给strong指针的时候;
- block作为Cocoa API中方法名含有usingBlock的方法参数的时候;
- block作为GCD API的参数的时候;
所以如果将上面代码放在ARC环境下执行的话,则block1
是__NSMallocBlock__
类型的。
release操作会在其内部自动执行。
__block变量的存储域
__block变量的存储域 | Block从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
既然__block
对应的结构体或者对象是在Block内部使用,那么Block就需要对这个对象的生命周期进行负责。接着我们从源码里面进行理解
1 | static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { |
我们需要注意两个函数__main_block_copy_0
和__main_block_dispose_0
。
- 当block进行一次copy操作的时候,
__main_block_copy_0
函数内部调用_Block_object_assign
函数,它会对__block
变量生成强引用; - 当block从堆中移除的时候,
__main_block_dispose_0
函数内部调用_Block_object_dispose
函数,它会自动释放掉引用的__block
变量。
上面的代码是在ARC环境下的,如果Block在栈上,并不会对__block
变量产生引用。
这里还要解决一个前面遗留的问题,既然__Block_byref_a_0
结构体中已经有变量a
,为什么还需要使用__forwarding
指针对a
赋值呢或者说__forwarding
存在的意义是什么?
关于__forwarding
的作用《Objective-C高级编程》一书中的第111页、第112页中有很明确的说到:block变量的结构体成员变量forwarding可以实现无论block变量配置在栈上还是堆上都能够正确地访问block变量。
没有进行copy操作的时候:
进行copy操作以后:
这里分为两种情况:
如果我们没有对栈上的Block执⾏copy操作,修改被
__block
修饰的变量实际上是通过其__forwarding
指针指向的自身,对其中的变量进行修改。如果我们执行过copy操作,那么栈上的Block的
__forwarding
指针,实际是指向堆上的__block
修饰的变量,⽽堆上的__forwarding
指针则指向⾃自身的__block
修饰的变量。在变量作用域结束的时候,栈上的__block
变量和Block被废弃掉,但是堆上的__block
变量和Block不受影响。
Block的循环引用
我们在使用Block的时候,如果有一个对象持有了这个Block,而在Block内部又使用了这个对象,就会造成循环引用。如以下写法:
1 | #import <Foundation/Foundation.h> |
可以看到,上面这段代码发生了循环引用,导致AObject对象无法被释放。通常做法是使用__weak
关键字在Block外部声明一个弱引用。重新修改下init
方法:
1 | - (instancetype)init { |
前面在讲述截获自动变量值的时候,我们知道Block在截获对象类型的时候,会连同对象的所有权修饰符一起截获,这里截获的是__weak
修饰的weakSelf
,因此不会发生循环引用。
在为避免循环引用而使用__weak
修饰符的时候,还要注意有可能在Block执行的时候,对象在中途被释放掉了。这个时候需要在Block内部声明一个局部变量强持有对象,这个局部变量会在到Block执行结束时自动释放,不会造成循环引用,而对象也会在Block执行结束后被释放。
是不是所有的Block都需要在外部声明使用__weak
修饰呢?答案是否定的。所谓“引用循环”是指双向的强引用,所以那些“单向的强引用”(block强引用self)没有问题。例如:
1 | [UIView animateWithDuration:duration |
但如果你使用一些参数中可能含有ivar的系统的api,如NSNotificationCenter
就要小心一点。
1 | __weak __typeof__(self) weakSelf = self; |
可以看出self --> _observer --> block --> self
这显然是一个循环引用。总而言之,只有当self直接或间接的持有 Block,并且在Block内部又使用了self的时候,才应该使weakSelf。
总结
- block的本质是一个封装了函数调用以及函数调用环境的OC对象;
- block截获自动变量值的规则:
- 局部变量会被直接截获;
- 局部静态变量会被截获其指针;
- 全局变量并不会被截获,而是直接使用;
- block截获对象的规则:
- block位于栈上,则不会对截获的对象变量进行强引用;
- block从栈上复制到堆上,调用
copy
函数,对截获的变量进行强/弱引用; - block从堆上移除,调用
dispose
函数,自动释放引用的变量;
- block使用copy属性的原因:在MRC下,访问了自动变量的block处于栈上,容易被释放,使用copy可以将其复制到堆上,放在堆上便可以自己去控制Block的生命周期;在ARC下,对Block做了优化;
- block的循环引用:一个对象持有了这个Block,而在Block内部又使用了这个对象,就会造成循环引用;
- __block的作用:解决block内部无法修改自动变量值的问题;
- __block的注意点:不能修饰全局变量和静态变量;
- block的实质:编译器将block变量包装成一个对象,将截获到的值存在这个对象中,通过对截获的值进行赋值而更改原有的值;
- forwarding指针的作用:可以实现无论block变量配置在栈上还是堆上都能够正确地访问__block变量;
Sunnyxx的Block面试题
代码如下:
1 | #import <Foundation/Foundation.h> |
第一题分析
想要替换原有的block实现,我们需要知道Block是如何执行代码的。通过上面的原理分析,我们知道在执行block的代码就相当于调用__main_block_func_0
函数。该函数指针被保存在__block_impl
结构体的FuncPtr
中。我们知道将FuncPtr
指向我们自己的函数就可以完成切换。
第二题分析
知道第一题如何解答之后,第二题相对来说简单一点,其实就是在我们自定义函数的时候,这个函数需要包括我们传入的参数,以及原有实现。
那么通过一个函数指针可以保存原有实现,在自定义函数中通过函数指针调用原有函数即可,而参数是本身__main_block_func_0
函数就有的,我们只要确保我们自定义函数的参数列表与__main_block_func_0
函数的参数列表一致就行。
第三题分析
这道题目说白了就是在一个适当的时机去调用HookBlockToPrintArguments
。通过fishhook这个框架,我们可以动态的修改C语言函数。那如何确定这个C语言函数呢?这个函数需要有这么一个功能,它可以拿到原来的block对象,经过替换再重新返回。
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,它会调用一个copy
函数即_Block_copy
,所以我们需要动态修改这个函数。