【iOS】Block补充
前言
笔者之前寒假有学习过block的相关知识,但当时学得并不是很明白,现在看源码正好回顾了block的相关知识,这篇文章来补充一些之前没有学习到的知识或者博客中没有记录的知识(之前的博客——OC高级编程之Blocks)
block捕获外界变量
在寒假的学习中,我们知道了block捕获自动变量值时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例中,但当时不知道是怎么样保存到结构体中的,也不知道为什么外部对自动变量的改变不会影响到block内部。这里从自动变量、静态局部变量、全局以及全局静态变量四种情况来分析block如何捕获外界变量
捕获自动变量
我们在Block中使用localA局部变量,block会对localA这个变量进行捕获,代码运行结果如下:
可见在外部修改localA并不会影响到Block内部
因为对于自动变量,在block的初始化函数中,localA 通过拷贝与 struct __main_block_impl_0
绑定,也就是说 localA 是拷贝过来的
当
Block
捕获基本数据类型(如 int、float 等)或结构体时,它会通过值捕获的方式复制变量的当前值。这意味着在 Block 内部使用的是变量捕获时刻的快照。由于是值复制,所以之后即使原始变量的值发生改变,Block 内部的值也不会改变。
捕获静态局部变量
捕获静态局部变量时,静态局部变量与Block建立关联的是指针
,也就是说Block捕获的静态局部变量捕获的是变量的指针,因此当我们对静态局部变量进行修改时,Block内部的静态局部变量的值也会随之改变
全局、全局静态变量
全局变量以及全局静态变量不会出现在block底层的结构体中,意味着二者无法被捕获。其实,当block中需要使用全局变量时,直接使用就可以了,从逻辑上来说也确实不必要捕获。
判断block存储在哪里
当block中访问自由变量时,block底层的类对象为_NSStackBlock_或者_NSMallocBlock_。
需要注意这里就算不调用copy方法,创建Block
变量并且捕获自由变量时Block会自动被拷贝到堆上,如果没有copy操作使用完就直接释放了,如果不创建Block
变量就没有copy
操作
是否创建block变量 | 访问变量类型 | Block类型 | 备注 |
---|---|---|---|
否 | 自由变量 | _NSStackBlock_ | |
是 | 自由变量 | _NSMallocBlock_ | NSStackBlock执行了copy 操作 |
Block_copy()源码分析
void *_Block_copy(const void *arg) {return _Block_copy_internal(arg, WANTS_ONE);
}
查看实际调用的函数:
/* 拷贝 Block,或者增加 Block 的引用计数。若需要拷贝,调用拷贝协助方法(如果存在) */
static void *_Block_copy_internal(const void *arg, const int flags) {struct Block_layout *aBlock;const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
///1、若不存在源 Block ,则返回 NULLif (!arg) return NULL;///2、将源 Block 指针转换为 (struct Block_layout *)aBlock = (struct Block_layout *)arg;///3、若源 Block 的 flags 包含 BLOCK_IS_GC,则其为堆块。 \/// 此时增加其引用计数,并返回这个源 Blockif (aBlock->flags & BLOCK_NEEDS_FREE) {// latches on highlatching_incr_int(&aBlock->flags);return aBlock;}///4、源 Block 是全局块,直接返回源 Block(全局 Block 就是一个单例)else if (aBlock->flags & BLOCK_IS_GLOBAL) {return aBlock;}
///5、源 Block 是一个栈 Block,执行拷贝操作。首先申请相同大小的内存struct Block_layout *result = malloc(aBlock->descriptor->size);if (!result) return (void *)0;///6、使用 memmove 方法将栈区里的源 Block 逐位复制到刚申请的堆区 Block 内存中。这样做是为了保证完全复制所有元数据。memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first///7、更新 result 的 flags。result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed ///Junes 确保引用计数为 0。注释表示没这个必要,可能因为此时引用计数早已为 0。但是为了防止 bug 被保留下来。result->flags |= BLOCK_NEEDS_FREE | 1; ///Junes 为 result 的 flags 添加 BLOCK_NEEDS_FREE,并设置其引用计数为 1。表明这是一个堆 Block(一旦引用计数降为 0,则其内存将被回收)///8、将 result 的 isa 指向 _NSConcreteMallocBlock。这意味着 result 是一个堆 Block。result->isa = _NSConcreteMallocBlock;///9、如果 result 存在拷贝协助方法,调用它。/// 如果 block 捕获对象,编译器将会生成这个协助方法。/// 这个协助方法将会 retain 被捕获的对象。if (result->flags & BLOCK_HAS_COPY_DISPOSE) {(*aBlock->descriptor->copy)(result, aBlock); }return result;
}
源码中对copy操作进行了分类
1、如果源Block不存在则返回NULL 2、如果源 Block 是 NSConcreteMallocBlock,增加其引用计数,然后返回源 Block; 3、如果源 Block 是 _NSConcreteGlobalBlock,直接返回源 Block,因为NSConcreteGlobalBlock是一个单例; 4、如果源 Block 是 _NSConcreteStackBlock,那么操作就比较复杂
申请一块相同大小的内存 拷贝栈上的block的所有元数据到新申请的内存空间上,也就是将数据拷贝到堆上,堆上的block我们叫做result 更新 result 的 flags,确保其引用计数为 0; 更新 result 的 flags,添加 BLOCK_NEEDS_FREE,并设置其引用计数为 1; 将 result 的 isa 指向 _NSConcreteMallocBlock。标明 result 是一个堆 Block; 如果 result 捕获了对象,调用编译器生成的拷贝协助方法 retain 被捕获的对象。
block被拷贝时,block中使用的变量也会一起拷贝到堆区,总结如下:
对象类型 | 拷贝方式 |
---|---|
操作对象内存 | 直接指针拷贝*dest = object |
__weak | 直接指针拷贝*dest = object |
Block实例 | 通过_Block_copy()复制 |
OC变量 | 引用计数+1 |
__block结构体 | 通过_Block_byref_assign_copy()复制该结构体 |
ARC
环境下,一旦使用__block
修饰并在block
中修改,就会触发copy
操作,block
就会从栈区copy
到堆区,此时的block
是堆区block
将 block 赋值给 strong
类型的属性或变量也会触发copy
操作
Block循环引用
上图两段代码中,代码一就是一段会造成循环引用的代码,self持有block,而block又使用了self的属性name,因此block会持有self,这样就导致了block和self的互相持有
而代码二中虽然也使用了name属性,但是self不持有block,因此不会发生循环引用
解决循环引用可以使用以下几种方式:
-
方式①:
weak-strong-dance
-- 强弱共舞 -
方式②:
__block
修饰对象(需要注意的是在block
内部需要置空
对象,而且block
必须调用) -
方式③: 传递对象
self
作为block
的参数,提供给block
内部使用 -
方式④: 使用
NSProxy
(这个笔者暂时还没弄明白,就不细说了)
weak-strong-dance
如果block内部并未嵌套block,直接使用__weak修饰self即可
此时weakSelf和self指向用一片内存空间,且使用__weak不会导致self的引用计数发生变化
如果block内部嵌套block,需要同时使用__weak和__strong
其中strongSelf
是一个临时变量,在block
的作用域内,即内部block
执行完就释放strongSelf
这种方式叫做中介者模式,weakSelf作为中介者传递引用,strongSelf代理执行
__block修饰对象
这种方式思路是将self作为__block变量,在block中使用完后再将指针置为空手动释放
需要注意的是这里的block
必须调用,如果不调用block
,vc
就不会置空,那么依旧是循环引用,self
和block
都不会被释放.
传递对象self作为block的参数
这个思路就是把对象self作为参数给block使用,就不会有计数问题