本文依旧是对 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 ,就和 block 捕获了一个自动变量 int a 是一个道理,然而 __cself 是结构体本身,使用的时候就是 __cself->self-> _blk,这样 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] 中 ,因为虽然 block 捕获了 self,但是 block 是类对象持有的,所以这种系统库不会造成循环引用。
3、结语
Block 的理论内容就到这里了,是对 iOS与OSX多线程和内存管理 第二章的内容整理,主要也是方便自己翻阅。
如果你发现这些内容对你有用,感谢点个赞。有问题欢迎指出,共同学习,一起进步。