本文将介绍block的类型,循环引用的解决方法以及block底层分析
Block简介

Block定义:带有自动变量的匿名函数,它是C语言的拓展功能,之所以是扩展,是因为C语言不允许存在这样的匿名函数
- 匿名函数
- 匿名函数式指不带函数名称的函数
- 带有自定变量
- Block拥有捕获外部变量的功能,在Block中访问一个外部的局部变量,Block会持有它的临时状态,自动捕获变量值,外部局部变量的变化不会影响它的状态(这个下面会讲到)。
Block类型
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)
下方图片如果加载失败可以来简书搜索:iOS_asuka
block主要有三种类型
- 1.
__NSGlobalBlock__:全局block,存储在全局区
此时block无参也无返回值,属于全局block
- 2.
__NSMallocBlock__:堆区block,因为block既是函数,也是对象
此时block会访问外界变量,即底层拷贝a,所以是堆区block
- 3.
__NSStackBlock__:栈区block
其中局部变量a在没有处理之前(即没有拷贝之前)是 栈区block, 处理后(即拷贝之后)是堆区block ,所以栈区block越来越少了
这个情况下,可以通过__weak不进行强持有,block就还是栈区block
总结
- 1.block是直接存储在
全局区 - 2.block如果
访问外界变量,并进行block相应copy- 如果此时的
block是强引用,则block存储在堆区,即堆区block - 如果此时的
block通过——weak变成了弱引用,则block存储在栈区,即栈区block
- 如果此时的
Block循环引用
【正常释放】:当A持有B,当A调用dealloc方法,给B发送release信号,B收到release信号,如果此时B的引用计数为0时,则会调用B的dealloc方法,此时A,B都能正常释放 【循环引用】:当A持有B,B同时也持有A时,此时A销毁需要B先销毁,而B销毁同样需要A先销毁,就导致相互等待销毁,此时A,B的引用计数都不为0,所以A,B此时都无法释放。
解决循环引用
举个循环引用的例子:如下图
上面代码发生了
循环引用,因为在block内部使用了self的name变量,导致block持有self,而self本来就持有block,就导致了self和block相互持有。
下面来解决循环引用
- 1.
weak-strong-dance(最常用的方法) - 2.
__block修饰对象,同时置nil - 3.
传递对象self作为block的参数,提供给block内部使用 - 4.
使用NSProxy
weak-strong-dance(弱强共舞)
- 1.
如果block内部并未嵌套block,直接使用__weak修饰self即可
@interface ViewController ()
@property (nonatomic, copy) void (^block)(void);
@property (nonatomic, copy) NSString *name;
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.name = @"man";
__weak typeof(self) weakSelf = self;
self.block = ^(void){
NSLog(@"%@",weakSelf.name);
};
self.block();
}由于此时的weakSelf和self指向同一片内存空间,而且使用__weak不会导致self的引用计数发生变化,可以通过打印weakSelf和self的指针地址,以及self的引用计数来验证 [图片上传失败...(image-74b93d-1619163053376)]
- 2.如果
block内部嵌套block,则需要同时使用__weak和__strong
如果只用weak修饰,则可能出现block内部持有的对象被提前释放,为了防止block内部变量被提前释放,使用strong对引用计数+1,防止提前释放。
其中strongSelf是一个临时变量,在block的作用域内,当block执行完就会释放strongSelf,这种方式属于打破self对block的强引用,依赖于中间者模式,属于自动置为nil,也就是自动释放。
__block修饰变量
这种方式同样依赖于中介者模式,属于手动释放,是通过__block修饰对象,主要是因为__block修饰的对象是可以改变的。
这里的
block必须调用,如果不调用block,vc就不会置空,那么依旧是循环引用,self和block都不会释放。
对象self作用参数
主要是将对象self作用参数,提供给block内部使用,不会有引用计数问题
使用NSProxy虚拟类
OC是只能单继承的语言,但它是基于运行时的机制,所以可以通过NSProxy来实现伪多继承,填补多继承的空白NSProxy和NSObject是同级的类,是个虚拟类,只是实现了NSObject的协议NSProxy其实是一个消息重定向封装的一个抽象类,类似一个代理人,中间件,可以通过继承它,并重新写下面的两个方法来实现消息转发到另一个实例
使用场景
- 1.实现
多继承功能 - 2.解决
NSTimer&CADisplayLink创建时对self强引用问题,这个在YYKit中YYWeakProxy有所使用的
循环引用解决原理
主要是通过自定义的NSProxy类的对象来代替self,并使用方法实现消息转发,下面是NSProxy子类的实现以及使用的场景
@interface LjProxy ()
@property(nonatomic, weak, readonly) NSObject *objc;
@end
@implementation LjProxy
- (id)transformObjc:(NSObject *)objc{
_objc = objc;
return self;
}
+ (instancetype)proxyWithObjc:(id)objc{
return [[self alloc] transformObjc:objc];
}
// 有了方法签名之后就会调用方法实现
- (void)forwardInvocation:(NSInvocation *)invocation{
SEL sel = [invocation selector];
if ([self.objc respondsToSelector:sel]) {
[invocation invokeWithTarget:self.objc];
}
}
// 查询该方法的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
NSMethodSignature *signature;
if (self.objc) {
signature = [self.objc methodSignatureForSelector:sel];
}else{
signature = [super methodSignatureForSelector:sel];
}
return signature;
}
- (BOOL)respondsToSelector:(SEL)aSelector{
return [self.objc respondsToSelector:aSelector];
}
@end自定义Man和Teacher类
@implementation Man
- (void)likeFood {
NSLog(@"%@-->牛肉", self);
}
@end
@implementation Teacher
- (void)likeWork {
NSLog(@"%@->教书育人", self);
}
@end通过LjProxy实现多继承功能
通过LjProxy解决定时器中self的强引用问题
运行打印:
总结
循环引用解决的根本方式:
- 1.
打破self对block的强引用,这需要对block进行声明的时候使用weak修饰,但是这会导致block提前释放,所以这种方式不可行 - 2.
打破block对self的强引用,主要就是self的作用域和block作用域的数据交换问题,我们可以通过代理,通知,传值等几种方式,用于解决循环,我们对上面讲的列一下- weak-strong-dance(弱强共舞)
- __block修饰变量
- 对象self作用参数使用
- 使用NSProxy子类代替self
上面介绍了block的定义,用法以及如何解决循环引用,下面我们来探寻下block的C++实现
Block C++实现
研究底层可以先从C++,断点调试开始
本质
创建block.c文件
通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc block.c -o block.cpp,将block.c编译成block.cpp,其中block在底层被编译成了以下的形式
相当于block等于main_block_impl_0,是一个函数。下面查看main_block_impl_0
通过上图我们可以知道
__main_block_impl_0是一个结构体,同时可以说明block是一个__main_block_impl_0类型的对象,这也是为什么block能够%@打印的原因
我们用一张图来说明他们之间的联系
下面我们来解释几个问题
block为什么需要调用
在底层block的类型__main_block_impl_0结构体,通过其同名构造函数创建,第一个传入的block的内部实现代码块,即__main_block_func_0,用fp表示,然后赋值给impl的FuncPtr属性,然后在main中进行了调用,这也是block为什么需要调用的原因。如果不调用,block内部实现的代码块将无法执行,可以总结为以下两点
- 1.
函数声明:即block内部实现声明成了一个函数__main_block_func_0 - 2.
执行具体的函数实现:通过调用block的FuncPtr指针,调用block执行
block是如何获取外界变量的
我们将上面的代码当中调用block
再将它编译成.cpp文件
__main_block_func_0中的a是值拷贝,如果此时在block内部实现中作 a++操作,是有问题的,会造成编译器的代码歧义,即此时的a是只读的。
【总结】:block捕获外界变量时,在内部会自动生成同一个属性来保存
__block原理
将上面代码的局部变量a使用__block修饰
再将它编译成.cpp文件
通过上面的截图我们可以得出以下结论:
- 1.main中的
a是以__Block_byref_a_0结构体的形式出现的,是封装的对象 - 2.在
结构体__Block_byref_a_0中,a的值存在int a中 - 3.在
__main_block_impl_0中,将对象a的地址&a给构造函数 - 4.在
__main_block_func_0内部对a的处理时指针拷贝,此时创建的对象a与传入对象的a指向的是同一片内存空间
总结
- 1.
外界变量通过__block生成__Block_byref_a_0结构体 - 2.
结构体用来保存原始变量的指针和值 - 3.将变量生成的
结构体对象的指针地址传递给block,然后在block内部就可以对外界变量进行操作了
上面block和非block修饰局部变量产生两种不同的拷贝
- 非block修饰:
值拷贝-浅拷贝,只是拷贝数值,且拷贝的值不可更改,指向不同的内存空间,非block修饰的变量a就是值拷贝 - block修饰:
指针拷贝-深拷贝,生成的对象指向同一片内存空间,通过block修饰的变量a就是指针拷贝
Block底层原理
确定block源码位置
在main函数中写如下代码
通过在block处打断点,运行block
我们发现走到了objc_retainBlock,我们加符号断点objc_retainBlock
打印符号断点后,我们发现执行了_Block_copy,我们再加符号断点_Block_copy
此时我们需要看_Block_copy实现,它在libsystem_blocks.dylib源码中,我们去苹果官方下载下源码libclosure-74,在源码中搜索_Block_copy
通过查看_Block_copy的源码实现,发现block在底层的真正类型是Block_layout
Block真正类型Block_layout
我们查看下Block_layout底层实现
说明:
1.
isa:指向的是block类型的类2.flags:
标识符,按bit位表示一些block的附加信息,类似于isa中的位域,其中flags的种类有以下几种,主要重点关注BLOCK_HAS_COPY_DISPOSE和BLOCK_HAS_SIGNATURE。BLOCK_HAS_COPY_DISPOSE决定是否有Block_descriptor_2。BLOCK_HAS_SIGNATURE决定是否有Block_descriptor_3- 第一位:
BLOCK_DEALLOCATING,释放标记,一般常用BLOCK_NEEDS_FREE做位与操作,一同传入Flags,告知该block可释放 - 第十六位:
BLOCK_REFCOUNT_MASK,存储引用计数的值;是一个可选用参数 - 第二十四位:
BLOCK_NEEDS_FREE,第16是否有有效的标志,程序根据它来决定是否增加或是减少引用计数位的 - 第二十五位:
BLOCK_HAS_COPY_DISPOSE,是否拥有拷贝辅助函数(a copy helper function) - 第二十六位:
BLOCK_HAS_CTOR,是否拥有block析构函数 - 第二十七位:
BLOCK_IS_GC,标志是否有垃圾回收//OS X - 第二十八位:
BLOCK_IS_GLOBAL,标志是否是全局block - 第三十位位:
BLOCK_HAS_SIGNATURE,与BLOCK_USE_STRET相对,判断当前block是否拥有一个签名。用于runtime时动态调用
- 第一位:
3.
reserved:保留信息,可以理解预留位置,猜测是用于存储block内部变量信息4.
invoke:是一个函数指针,指向block的执行代码5.
descriptor:block的附加信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。有三类Block_descriptor_1是必选Block_descriptor_2和Block_descriptor_3是可选的
我们再看下他们底层实现
从上图可以知道:
Block_descriptor_2和Block_descriptor_3都是通过Block_descriptor_1的地址,经过内存平移得到的
Block内存变化
根据符号断点
我们打断点运行,走到objc_retainBlock,我们打印寄存器x0
我们发现此时的
block是全局block,即__NSGlobalBlock__类型
我们增加外部变量a,再次运行,在相同的位置再次打印x0
此时读取
block发现是栈block即__NSStackBlock__
执行到符号断点
objc_retainBlock时,我们发现还是栈区block
我们在增加符号断点_Block_copy,继续往下走,来到_Block_copy断点,此时打印
此时的
x0地址不变,说明此时的block还是栈区block,我们在_Block_copy尾部ret处打断点,执行到断点处,再次打印
发现经过
_Block_copy之后,x0的地址发生了变化,我们打印x0地址后发现block由栈区block变为堆区block,即__NSMallocBlock__
同样上面的结论我们也可以通过读寄存器地址来得出
根据寄存器地址
我们重新运行项目,继续前面的断点,运行前面的断点,打印x0,x8,x9
此时我们看到
x0和x8指向的是同一块内存空间,用于存储__NSStackBlock__,此时的x9存储的是_block_invoke
我们将代码运行到41行,在次打印上面的地址
此时的
x8是_block_invoke,blr就是跳转进入的意思,也就是要进入_block_invoke里
当我们进入
_block_invoke中,可以得出是通过内存平移得到block内部实现的
前面提到的Block_layout的结构体源码中知道其有个属性invoke,即block的执行者,是从isa首地址平移16字节得到invoke,然后进行调用执行的。
Block签名
最开始我们拿到了block的地址,前面底层我们知道block底层是Block_layout的结构体
通过上图我们知道descriptor是附加信息,我们打印下它的内容
找到
block地址,通过内存平移找到descriptor,然后x/8gx查看descriptor内存情况,我们前面说了descriptor会有_Block_descriptor_2或者_Block_descriptor_3,只有_Block_descriptor_3存在签名。
判断是否存在_Block_descriptor_2,即flags的BLOCK_HAS_COPY_DISPOSE(拷贝辅助函数)是否有值
- 1.先通过
p/x 1<<25,即1左移25位得到BLOCK_HAS_COPY_DISPOSE - 2.再拿
flags与上BLOCK_HAS_COPY_DISPOSE(flags是block首地址平移8字节,即:0x00000000c1000002
看到打印结果为0,表示没有Block_descriptor_2。
判断是否存在Block_descriptor_3,即flags的BLOCK_HAS_SIGNATURE(是否有签名)是否有值
- 1.先通过
p/x 1<<30,即1左移30位得到BLOCK_HAS_SIGNATURE - 2.再拿
flags与上BLOCK_HAS_SIGNATURE(flags是block首地址平移8字节,还是:0x00000000c1000002
看到打印的结果不为0,说明有值,说明是Block_descriptor_3,存在签名,看descriptor,其中第三个0x0000000104d63e87表示签名。我们将签名打印出来了
下面我们通过[NSMethodSignature signatureWithObjCTypes:"v8@?0"]看下签名具体内容
下面我们具体来看下签名:
return value: -------- -------- -------- --------
type encoding (v) 'v'
flags {}
modifiers {}
frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
memory {offset = 0, size = 0}
argument 0: -------- -------- -------- --------
type encoding (@) '@?' // 类型是否是@
flags {isObject, isBlock} // @是isObject ,?是isBlock,代表 isBlockObject
modifiers {}
frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
memory {offset = 0, size = 8} // 所在偏移位置是8字节block的签名信息类似于方法的签名信息,主要体现在block返回值,参数以及类型等信息上。
Block的三次copy分析
Block_copy源码分析
- 进入
_Block_copy源码,将block从栈区拷贝至堆区- 如果需要释放,如果需要则直接释放
- 如果是
globalBlock,则不需要copy,直接返回 - 反之,只有两种情况:
栈区blockor堆区block,由于堆区block需要申请空间,前面并没有申请空间的相关代码,所以只能是栈区block- 通过
malloc申请内存空间用于接收block - 通过
memmove将block拷贝至新申请的内存中 - 设置
block对象的类型为堆区block,即result->isa = _NSConcreteMallocBlock
- 通过
_Block_object_assign分析
要分析block的三层copy,首先需要知道外部变量的种类有哪些,在__block的cpp文件中,对block修饰__main_block_desc_0_DATA,而__main_block_desc_0_DATA用的__main_block_copy_0,最后对a的修饰_Block_object_assign。对block修饰其中用的最多的是BLOCK_FIELD_IS_OBJECT和BLOCK_FIELD_IS_BYREF
而_Block_object_assign是在底层编译代码中,外部变量拷贝是调用的方法就是它。看下_Block_object_assign的源码
- 1.如果是
普通对象,则交给ARC处理,并拷贝对象指针,即引用计数+1,所以外界变量不能释放 - 2.如果是
block类型的,则通过_Block_copy操作,将block从栈区拷贝到堆区 - 3.如果是
__block修饰的变量,调用_Block_byref_copy函数,进行内存拷贝以及常规处理
我们看下_Block_byref_copy源码实现
- 1.将传入对象,
强转为Block_byref结构体类型对象,保存一份 - 2.
没有将外界变量拷贝到堆,需要申请内存,进行拷贝 - 3.如果已经
拷贝过了,则进行处理并返回 - 4.其中
copy和src的forwarding指针都指向同一片内存,这也是为什么__block修饰的对象具有修改能力的原因
代码验证
写如下代码:
进行clang编译结果如下
- 1.编译后lj_name比普通变量多了Block_byref_id_object_copy_131和Block_byref_id_object_dispose_131
- 2.Block_byref_lj_name_0结构体中多了Block_byref_id_object_copy和__Block_byref_id_object_dispose
通过上面的分析,我们可以知道这些方法的执行顺序_Block_copy->_Block_byref_copy->_Block_object_assign,正好对应上述的三层copy 综上所述,那么block是如何拿到lj_name的呢?
- 1.通过
__block_copy方法,将block拷贝至堆区 - 2.通过
_Block_object_assign方法正常拷贝,因为__block修饰的外界变量在底层是Block_byref结构体 - 3.发现
外部变量还存在一个对象,从bref中取出相应的对象lj_name,拷贝至block控件,才能使用(相同空间才能使用,不同则不能使用)。最后通过内存平移得到lj_name,此时的lj_name和外界lj_name是同一片内存空间(从_Block_object_assign方法中的*dest = object;看出)
三层copy总结
通过上面我们看出,block的三层拷贝指的是以下三层:
- 【第一层】通过
_Block_copy实现对象的自身拷贝,从栈区拷贝至堆区 - 【第二层】通过
_Block_byref_copy方法,将对象拷贝为Block_byref结构体类型 - 【第三层】调用
_Block_object_assign方法,对__block修饰的当前变量的拷贝
【注意】只有__block修饰的对象才三层copy
拓展
_Block_object_dispose 分析
在__Block_byref_id_object_dispose_131实现中调用的就是_Block_object_dispose,下面我们看下_Block_object_dispose的底层实现:
通过源码我们可以知道
_Block_object_dispose是进行release操作,通过不同分区的block,进行不同的释放操作。而_Block_object_assign是进行retain操作的,
下面看看_Block_byref_release实现
下面我们画图来更容易的了解Block的三层copy的流程
写到最后
写的内容比较多,由于本人能力有限,有些地方可能解释的有问题,请各位能够指出,同时对Block有关的疑问,欢迎大家留言。希望大家能够相互交流、探索,一起进步!

京公网安备 11010502036488号