本文依旧是对 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多线程和内存管理 第二章的内容整理,主要也是方便自己翻阅。
如果你发现这些内容对你有用,感谢点个赞。有问题欢迎指出,共同学习,一起进步。