Block 原来你是这样的(二)
作者: 稀土掘金 更新时间:2021-04-30 18:30:53 原文链接
本文依旧是对 iOS与OSX多线程和内存管理 的整理。
上一篇 Block 原来你是这样的(一) 介绍了什么是 Block
、分析了 Block
如何截获自动变量、更改 Block
存在的变量值的方法,上篇遗留了三个问题:
-
__block
说明符修饰的变量Clang
后是什么样;(第一大节已说明) - 为什么截获的自动变量可以超出其作用域;(2.4 节说明了)
-
self
什么时候会引起循环引用。(2.7 节说明了)
本篇内容将对这些内容进行说明。
1、__block 说明符
先看代码,再 Clang
。
int main(int argc, const char * argv[]) { // 这里添加了 __block __block int a = 10; void(^blk)(void) = ^{ a = 11; }; blk(); return 0; }
Clang
后:
struct __Block_byref_a_0 { void *__isa; __Block_byref_a_0 *__forwarding; int __flags; int __size; int a; }; struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_a_0 *a; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_a_0 *a = __cself->a; // bound by ref (a->__forwarding->a) = 11; } static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);} static struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; int main(int argc, const char * argv[]) { __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10}; void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344)); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); return 0; }
Clang
后的代码有点多了,分解看。
1、分析 __block 带来的变化
对比之前的代码发现:
- 增加了
__Block_byref_a_0
结构体; - 增加了
__main_block_copy_0
函数; - 增加了
__main_block_dispose_0
函数; -
__main_block_impl_0
截获的成员变量由int a;
→__Block_byref_a_0 *a
;
2、__Block_byref_a_0 结构体
查看 __Block_byref_a_0
结构体,分析初始化代码:
__Block_byref_a_0 a = { (void*)0, (__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10 }; struct __Block_byref_a_0 { void *__isa; __Block_byref_a_0 *__forwarding; int __flags; int __size; int a; };
-
isa
:赋值为0
,这里没有给类的地址; -
__forwarding
:将结构体自己的指针地址保存; -
__flags
:标志位; -
__size
:结构体大小; -
a
:保存int a
原本的值。
3、__block 后函数调用区别
分析调用和没有 __block
说明符的代码有什么区别:
/// 没有 __block 说明符 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { /// 直接使用__cself 获取 a 的值 int a = __cself->a; } /// 有 __block 说明符 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { /// 取出 __Block_byref_a_0 结构体 __Block_byref_a_0 *a = __cself->a; /// 结构体 a 取 __forwarding 再取 a (a->__forwarding->a) = 11; }
由上方结构体代码看到,没有 __block
说明符的 int a
是由 __main_block_impl_0
截获了,但是添加了 __block
说明符后, __main_block_impl_0
截获 __Block_byref_a_0
结构体指针,再由 __Block_byref_a_0
结构体保持 int a
的值,那为什么这么做呢? (1.4小节说明)
再看 int a
的取值,就会还有疑问,如果正常取值,我们应该 a->a
(结构体 __Block_byref_a_0 a
直接取 int a
的值),但是这里却是 int a = a->__forwarding->a;
,这是为什么呢? (2大节说明)
4、__Block_byref_a_0 存在的原因
由 __Block_byref_a_0
结构体保持 int a
的值是因为 int a
可能由多个 Block
改动,看一段代码:
int main(int argc, const char * argv[]) { // 这里添加了 __block __block int a = 10; void(^blk)(void) = ^{ a = 11; }; void(^blk1)(void) = ^{ a = 12; }; blk(); blk1(); return 0; }
Clang
,主要代码如下:
__Block_byref_a_0 a = {0, &a, 0, sizeof(__Block_byref_a_0), 10}; blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a, 570425344)); blk1 = &__main_block_impl_1(__main_block_func_1, &__main_block_desc_1_DATA, &a, 570425344));
Block blk
和 Block blk1
都使用了 __Block_byref_a_0
结构体实例指针,这样一来就可以多个 Block
使用同一个 __block
变量,方便了值的改动,也节省了内存。
2、Block 的存储域
通过之前的说明可知 Block
也是 Objective-C
对象。将 Block
当作 Objective-C
对象来看时,,该 Block
的 isa
赋值为 __NSConcreteStackBlock
(栈),也就是说 Block
的类为 __NSConcreteStackBlock
,但是 Block
不仅仅只有栈上的。
1、Block 类型
Block
总共有3种类型:
类 | 设置对象的存储域 |
---|---|
__NSConcreteStackBlock | 栈 |
__NSConcreteGlobalBlock | 程序的数据区域(.data区) |
__NSConcreteMallocBlock | 堆 |
应用程序内存分配如下图:
到现在为止出现的 Block
例子使用的都是 __NSConcreteStackBlock
类,且都设置在栈上。但实际上并非全是这样,在声明全局变量的地方使用 Block
时,生成的 Block
为 __NSConcreteGlobalBlock
类对象。例如:
void(^blk)(void) = ^{ printf("Global Block\n"); }; int main(int argc, const char * argv[]) { }
Clang
后:
__main_block_impl_1 -> impl -> isa = &__NSConcreteGlobalBlock;
该 Block
的类为 __NSConcreteGlobalBlock
类。即此 Block
的结构体实例设置在程序的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。由此 Block
的结构体的实例不依赖于执行时的状态,所以正是个程序中只需一个实例。因此将 Block
的结构体实例设置在与全局变量相同的数据区域即可。
在使用 Block
的时候只要 Block
不截获自动变量,无论是否在使用全局变量的地方使用 Block
都会将 Block
的结构体实例设置在程序的数据区域。
虽然通过 Clange 转换的源代码通常是 __NSConcreteStackBlock
类对象,但是实际上却有不同。总结如下:
Block Block
以上这些情况, Block
为 __NSConcreteGlobalBlock
类对象。即 Block
配置在程序的数据区域中。除此之外的 Block
为 __NSConcreteStackBlock
类对象,且设置在栈上。
但是 __NSConcreteMallocBlock
是何时使用的呢?这个就和 Block
的作用域有关了。
2、Block 作用域
配置在全局变量上的 Block
在变量作用域外也可以通过指针安全的使用。但是设置在栈上的 Block
,如果其所属的变量作用域结束,该 Block
也就被废弃。由于 __block
变量也配置在栈上,同样地,如果其所属的变量作用域结束,则该 __block
变量也会被废弃。如下图所示:
为了解决这个问题, Block
提供了将 Block
和 __block
变量从栈上复制到堆上的方法来解决这个问题。将栈上的 Block
复制到堆上,这样即使 Block
语法记述的变量作用域记述,堆上的 Block
还可以继续存在。如下图所示:
复制到堆上的 Block
的成员变量 isa
将会设置为 __NSConcreteMallocBlock
。那么 Block
是如何复制的呢?
3、Block 复制到堆上
实际上当 ARC 有效的时候,大多情况下编译器会恰当的判断,自动生成将 Block
从栈上赋值到堆上的代码。看一下下面的代码:
typedef int (^blk_t)(int); blk_t func(int rate); int main(int argc, const char * argv[]) { blk_t blk = func(10); int result = blk(3); printf("%d",result); } blk_t func(int rate) { return ^(int count){ return rate * count; }; }
上述代码在 C 语言下,编译器会报错的,说不能返回一个栈上的 Block
。
但是在 OC
语言 ARC
模式下是没有问题的,验证了当前情况下编译器会自动生成将 Block
从栈上赋值到堆上的代码。 Clang
一下 (记得加上 ARC ,不然会报错的 clang -fobjc-arc -rewrite-objc main.m -o main.cpp
):
blk_t func(int rate) { return ((int (*)(int))&__func_block_impl_0((void *)__func_block_func_0, &__func_block_desc_0_DATA, rate)); }
这里书上说通过 ARC 编译器就可以转换成下方代码,但是我试过了,还是上方代码,所以个人猜测是因为书籍过时了。
/// 书上的代码,仅做参考 blk_t func(int rate) { blk_t tmp = &_func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate); tmp = objc_retainBlock(tmp); return objc_autoreleaseReturnValue(tmp); }
后来问了一下别人,别人说: return
的 Block
在被赋值变量的时候 copy
。
上方 Clang
的结果显示,并没有在编译的时候调用 copy
,由以下2点论证 return
的 Block
在被赋值变量的时候 copy
:
- 不调用
func
函数,直接运行:结果控制台并没有打印Block 拷贝
。 - 调用
func
函数,执行return Block
,结果如下:
上方程序执行调用了 objc_retainBlock
也就是 Block_copy
方法,此时就会把栈上的 Block
拷贝到堆上。
"大多情况下编译器会恰当的判断,并自动拷贝"
,那什么时候编译器不能进行判断的?
目前根据这位大佬的示例测试如下:
- 作为变量:
- 赋值给一个普通变量之后就会被 copy 到堆上
- 赋值给一个 weak 变量不会被 copy
- 作为属性:
- 用 strong 和 copy 修饰的属性会被 copy 到堆上
- 用 weak 和 assign 修饰的属性不会被 copy
- 函数传参:
- 作为参数传入函数会被 copy 到堆上 (这里和之前测试结果不同了。他的测试结果为不 copy ,我的测试结果为 copy,可能他的博客太早了)
- 作为函数的返回值会被 copy 到堆上
copy
方法进行复制的动作总结如下表:
Block 的类 | 副本源的配置存储域 | copy 效果 |
---|---|---|
__NSConcreteStackBlock | 栈 | 从栈复制到堆 |
__NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
__NSConcreteMallocBlock | 堆 | 引用计数增加 |
4、自动变量复制到堆上
Block
从栈上复制到堆上, 那么 Block
截获的自动变量肯定也需要复制到堆上,否则就会出现 BAD_ADDRESS
或者取值不对情况。接下来看一段代码:
typedef void (^blk_t)(id obj); int main(int argc, const char * argv[]) { blk_t blk; /// 作用域编号1(方便下方描述) { NSMutableArray *array = [NSMutableArray array]; blk = ^(id obj) { [array addObject:obj]; NSLog(@"%d\n",array.count); }; } blk([NSObject alloc]); blk([NSObject alloc]); blk([NSObject alloc]); blk([NSObject alloc]); }
我们知道,一个变量的生命周期是根据其所属的作用域而定的,那么不使用 Block
的情况下 NSMutableArray *array
的作用域为上方代码中标识的 作用域编号1
,出了 作用域编号1
NSMutableArray *array
对象就会被销毁,但是上方代码却能正常执行。
Clang
一下看看:(简化了一些代码)
typedef void (*blk_t)(id obj); struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; /// __strong 指向了 NSMutableArray *__strong array; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *__strong _array, int flags=0) : array(_array) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself, __strong id obj) { NSMutableArray *__strong array = __cself->array; // bound by copy [array addObject:obj]; NSLog(@"%d\n",array.count); } static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/); } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; int main(int argc, const char * argv[]) { blk_t blk; { NSMutableArray *array = [NSMutableArray array]; blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344); } (*blk->FuncPtr)(blk, [NSObject alloc]); (*blk->FuncPtr)(blk, [NSObject alloc]); (*blk->FuncPtr)(blk, [NSObject alloc]); (*blk->FuncPtr)(blk, [NSObject alloc]); return 0; }
在 OC 中,C语言结构体不能含有 __strong
修饰符的变量。因为编译器不知道应何时进行 C 语言结构体的初始化和废弃操作,不能很好的进行内存管理。
但是 OC 运行时库能够准确的把握 Block 从栈复制到堆以及堆上的 Block 被废弃的时机,因此 Block 用结构体中即使含有 __strong
或 __weak
修饰符的变量,也可以恰当地进行初始化和废弃。
所以需要在 __main_block_desc_0
机构体中增加成员变量 copy
和 dispose
,以及作为指针赋值给改成员变量的 __main_block_copy_0
函数和 __main_block_dispose_0
函数。
由于源代码中,含有 __strong
修饰符的对象类型变量 array
,所以需要恰当管理赋值给变量 array
的对象。
因此需要 __main_block_copy_0
函数使用 _Block_object_assign
函数将对象类型的对象赋值给 Block 的结构体的成员变量 array
并持有该对象。 _Block_object_assign
函数相当于 retain
,将对象赋值在对象类型的结构体成员变量中。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/); }
另外, __main_block_dispose_0
函数使用 _Block_object_dispose
函数,释放赋值在 Block 的结构体成员变量 array
中的对象。 _Block_object_dispose
函数相当于 release
,释放赋值在 Block 的结构体成员变量 array
中的对象。
static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/); }
但是发现 copy
和 dispose
函数并没有在 Clang
后的代码调用。其实它们实在 Block
从栈复制到堆上以及 Block
从堆上销毁的时候调用的。
函数 | 调用时机 |
---|---|
copy 函数 | 栈上的 Block 复制到堆上时 |
dispose 函数 | 堆上的 Block 被废弃时 |
有了这两个方法截获的自动变量就可以超出其作用域使用了,对于 __block
的变量也是一样的。
_Block_object_assign
、 _Block_object_dispose
最后一个参数:
-
BLOCK_FIELD_IS_OBJECT
:自动变量为对象时; -
BLOCK_FIELD_IS_BYREF
:自动变量为__block
时;
注: int a = 10
:不经过 __block
修饰的常量在常量区,随时可用。
5、__block 变量存储域
上述已经对 Block
和不含有 __block
修饰符的自动变量进行了说明,本小节就对 __block
变量存储域说明:
若在 1 个 Block
中使用 __block
变量,则当该 Block
从栈复制到堆时,使用的所有 __block
变量也必定配置在栈上。这些 __block
变量也全部被从栈复制到堆。此时,Block 持有 __block
变量。即使在该 Block
已复制到堆的情形下,复制 Block
也对所使用的 __block
变量没有任何影响。如下图所示。
在多个 Block
中使用 __block
变量时,因为最先会将所有的 Block
配置在栈上,所以 __block
变量也会配置在栈上。在任何一个 Block
从栈复制到堆时, __block
变量也会一并从栈复制到堆,并被该 Block
所持有。当剩下的 Block
从栈复制到堆时,被复制的 Block
持有 __block
变量, 并增加 __block
变量的引用计数。如下图所示。
如果配置再堆上的 Block
被废弃,那么它所使用的 __block
变量也就会被释放。如下图所示。
到这里, Block
的方式和 OC
的引用计数的内存管理完全相同。
6、__forwarding 取值
明白了 __block
变量的存储域之后,现在再看 __forwarding
变量存在的原因。
“不管上block 变量配置在栈 上还是在堆上,都能够 正确地访问该变量”。正如这句话所述,通过 Block
的复制, __block
变量也从栈复制到堆。此时可以同时访问栈上的 __block
变量和堆上的 __block
变量。
看一段代码:
__block int val = 0; void (^blk)(void) = ^{ ++val; }; ++val; blk(); NSLog(@"%d", val);
利用 copy
方法复制使用了 __block
变量的 Block
语法。 Block
和 __blok
变量两者均是从栈 复制到堆。此代码中在 Block
语法的表达式中使用初始化后的 __block
变量。
^{++val;}
然后在 Block
语法之后使用与 Block
无关的变量。
++val;
以上两种源代码均可转换为如下形式:
++(val.__forwarding->val);
在变换 Block
语法的函数中,该变量 val
为复制到堆上的 __block
变量用结构体实例,而使用的与 Block
无关的变量 val
,为复制前栈上的 __block
变量用结构体实例。
但是栈上的 __block
变量用结构体实例在 __block
变量从栈复制到堆上时,会将成员变量 __forwarding
的值替换为复制目标堆上的 __block
变量用结构体实例的地址。如图下图所示。
通过该功能,无论是在 Block
语法中、 Block
语法外使用 __block
变量, 还是 __block
变量配置在栈上或堆上,都可以顺利地访问同一个 _block
变量。
7、Block 循环引用
先看代码再分析:
typedef void (^blk_t)(); @interface TestClass : NSObject { blk_t _blk; id _obj; } @end @implementation TestClass - (instancetype)init { self = [super init]; if (self) { _blk = ^{ /// 标注点 1 NSLog(@"_obj = %@",_obj); /// 上面代码使用编译器 fix 后会添上 self // NSLog(@"_obj = %@",self->_obj); }; } return self; } @end
上方的代码,在 标注点 1
的位置会有 2 个警告:
-
Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior
:提示开发者有隐式self
; -
Capturing 'self' strongly in this block is likely to lead to a retain cycle
:提示开发者会造成循环引用而无法释放。
经过之前的分析知道, __main_block_impl_0 *__cself
会捕获 self
, __cself
是结构体本身, self
又持有 _blk
,所以造成循环引用了。
再看一个例子:
typedef void (^blk_t)(); @interface TestClass : NSObject { blk_t _blk; } @end @implementation TestClass - (instancetype)init { self = [super init]; if (self) { __block id tmp = self; _blk = ^{ NSLog(@"self = %@",tmp); tmp = nil; }; } return self; } - (void)execBlock { _blk(); } - (void)dealloc { NSLog(@"我想销毁"); } @end int main() { id o = [TestClass alloc]; [o execBlock]; return 0; }
上方代码正常调用就不会引起循环引用,但是如果不执行 [o execBlock]
代码就会发生循环引用。
-
TestClass
的对象o
持有Block _blk
; -
Block _blk
持有__block
变量。 -
__block
变量持有TestClass
的对象o
。
如果正常调用,就因为 tmp = nil;
断开循环引用。
所以到这里,我相信你就想明白为什么 [UIView animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations]
这种系统库不会造成循环引用了吧。
3、结语
Block
的理论内容就到这里了,是对 iOS与OSX多线程和内存管理 第二章的内容整理,主要也是方便自己翻阅。
如果你发现这些内容对你有用,感谢点个赞。有问题欢迎指出,共同学习,一起进步。