通过前面的知识, Block语法执行时, Block底层会转化成Block的结构体类型
的自动变量存储在栈上, 在__block变量
初始化时, 会转化成__block变量的结构体类型
的自动变量存储在栈上. 所谓结构体自动变量, 就是在栈上生成的结构体的实例, 如下表:
名称 | 实质 |
---|---|
Block | 栈上Block的结构体实例 |
__block变量 | 栈上__block变量的结构体的实例 |
同时,我们知道Block也是Objective-C的对象, 通过isa
指针, 上面出现的Block基本是_NSConcreteStackBlock
, 表示该对象存储在栈上.
实际上,Block对象有如下几种类型, 不同的类型, 存储在应用程序的不同的内存区域:
名称 | 设置对象的存储域 |
---|---|
_NSConcreteStackBlock | 栈 |
_NSConcreteGlobalBlock | 数据域(.data域) |
_NSConcreteMallocBlock | 堆 |
关于应用程序的内存布局, 可以参考以下文章 linux系统进程的内存布局
isa = &_NSConcreteGlobalBlock
对象的Block, 会存储在数据域, 因为它创建在全局域, 因此不会截获自动变量, 全局只需要一个实例即可, 因此将该Block存储在数据域. 有以下两种情况创建的Block是_NSConcreteGlobalBlock
:
- 记述全局变量的地方有Block语法时
- Block语法的表达式中不使用截获的自动变量时候(只截获静态变量, 或者不截获变量)
通过以下Demo可以支持上面的结论, 前4个Block都是_NSConcreteGlobalBlock
:
void (^global_block)(void) = ^{};
static int static_global_val = 1;
int global_val = 2;
int main(int argc, const char * argv[]) {
@autoreleasepool {
static int static_val = 3;
int num = 4;
void (^global_static_val_block)(void) = ^{ static_global_val = 0; };
void (^local_static_val_block)(void) = ^{ static_val = 0; };
void (^normal_block)(void) = ^{};
void (^stack_block)(void) = ^{int a = num;};
NSLog(@"Global Block: %@\n", global_block);
NSLog(@"Global Static val Block: %@\n", global_static_val_block);
NSLog(@"Local Static val Block: %@\n", local_static_val_block);
NSLog(@"Normal Block: %@\n", normal_block);
NSLog(@"Stack Block: %@\n", stack_block);
NSLog(@"Stack Block: %@\n", ^{int a = num;});
// Global Block: <__NSGlobalBlock__: 0x100001060>
// Global Static val Block: <__NSGlobalBlock__: 0x1000010a0>
// Local Static val Block: <__NSGlobalBlock__: 0x1000010e0>
// Normal Block: <__NSGlobalBlock__: 0x100001120>
// Stack Block: <__NSMallocBlock__: 0x1007078b0>
// Stack Block: <__NSStackBlock__: 0x7ffeefbff548>
}
return 0;
}
最后两个一个是_NSConcreteMallocBlock
, 一个是_NSConcreteStackBlock
, 两者实现一样,为何不是同一类Block?
我们知道栈上的自动变量, 在超出其作用域以后,其内存会被废弃回收. Block自动变量也是如此, 当栈上的Block所属的变量作用域结束, 那么该Block就会被废弃, 同时__block变量也存储在栈上,超出其作用域时, 也会被废弃.
让我们思考一下以下场景,__block int val
是__block变量结构体在栈上, 而创建的Block结构体实例也是在栈上, 但是我们在超出其作用域时, 也能够修改val的值, 同时此时Block
变成了__NSMallocBlock__
:
// 全局blk变量
void (^blk)(void);
int main(int argc, const char * argv[]) {
{
// val在作用域内有效
__block int val = 10;
blk = ^{
NSLog(@"val = %d", val);
val = 0;
NSLog(@"val = %d", val);
};
}
// 此时 val不在作用域内, val应该失效
blk();
NSLog(@"blk: %@", blk);
return 0;
}
//2018-11-06 10:51:15.758231+0800 Block-Demo[40051:1639002] val = 10
//2018-11-06 10:51:15.758633+0800 Block-Demo[40051:1639002] val = 0
//2018-11-06 10:52:30.711561+0800 Block-Demo[40088:1643359] blk: <__NSMallocBlock__: 0x10053d360>
"Blocks"提供了底层将栈上的Block和__block变量copy到堆上的方法
, 这样即使栈上的Block和__block变量失效, 堆上的Block和__block变量依然能够使用.
在从栈上将Block变量复制到堆上以后, 会同时存在栈上和堆上的Block和__block变量,一共两份, 并且会进行如下操作:
- 在Block结构体数据copy到堆上以后, 栈上的Block保持不变, 将堆上Block的
isa
设置成&_NSConcreteMallocBlock
. - 在
__block变量
的结构体数据调用impl->Desc->copy
copy到堆上以后, 栈上的__block变量
的结构体的__forwarding
指针会被修改指向堆上的__block变量
, 而堆上的__block变量
的__forwarding
指针不变, 依然指向堆上的__block变量
, 具体栈上和堆上的内存结构如下图:
此时, 不论Block/__block变量在栈上还是堆上, 通过__forwarding
会访问同一个变量.
Block从栈上copy到堆上
既然有些情况下Block会自动从栈上copy到堆上, 那么编译器是如何实现的呢?
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count) { return rate * count; };
}
实际该源码会返回一个创建在栈上的Block, 即在该方法返回时, 栈上的Block会被废弃, 我们通过ARC编译器源码可得:
blk_t func(int rate){
//1. 通过Block语法生成的Block, 即配置在栈上的Block用结构体实例赋值给相当于Block类型的变量tmp中.
blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
//2. _Block_copy函数将栈上的Block复制到堆上,复制后,将堆上的地址作为指针赋值给变量tmp.
tmp = objc_retainBlock(tmp);// _Block_copy(tmp)
//3. 将注册在堆上的Block作为Objective-C对象注册到autoreleasePool中, 然后返回该对象
return objc_autoreleaseReturnValue(tmp);
//4. 函数调用结束,在栈上的Block会被自动销毁.
}
ARC中的大多数情况,编译器都会自动帮我们调用copy函数,将栈上的Block copy到堆上.但是向方法或者函数中的参数传递Block时
, 需要我们手动调用copy
方法.
-(id) getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects: ^{NSLog(@"blk: %d", val);}, nil];// Block没有调用copy方法
}
typedef void (^blk_t)(void);
id obj = getBlockArray();
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk(); // 这里执行时会报异常, getBlockArray函数执行完成以后, 栈上的Block被废弃,当执行blk(),时导致坏内存访问
因此, 要手动调用copy方法:
-(id) getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects: [^{NSLog(@"blk: %d", val);} copy], nil];// 手动调用copy方法
}
typedef void (^blk_t)(void);
id obj = getBlockArray();
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk(); // 这里可以正常执行
ps: 实际上, 我在iOS12上Xcode10默认配置验证的时候,发现第一种情况即使不添加copy也能够正常运行, 在数组中的Block是
__NSMallocBlock__
, 可能LLVM某个版本以后这些地方都做了优化.
以下两种情况无需手动调用copy方法:
- Cocoa框架的方法且方法名中含有
usingBlock
等时, 例如NSArray - enumerateObjectsUsingBlock:
- GCD的API, 例如
dispatch_async
前面是配置在栈上的Block进行copy操作, 没有问题, 如果原本Block就在堆上,调用copy操作也不会域问题, 具体的总结如下:
Block类 | 原存储域 | 调用copy效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈copy到堆 |
_NSConcreteGlobalBlock | 数据域(.data域) | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数+1 |
对__block变量
来说, 当Block被从栈copy到堆上, __block变量
也会受到影响, 具体来说还是看下图:
在栈上的情况, Block中的指针只是指向栈上的__block变量
, 而当Block/__block变量被copy到堆上以后, 堆上Block会持有堆上__block变量. 而堆上的Block再次被调用copy时, 只是Block的引用计数+1而已, 而__block变量如果被多个堆上Block持有也只涉及到引用记数的变化. 一旦Block/__block变量的引用计数为0, 就会自动从堆上释放内存.这里Block/__block变量在堆上的内存管理与Objective-C对象完全一致.
参考资料
- <<Objective-C 高级编程: iOS与OSX多线程和内存管理>>
- https://blog.csdn.net/deft_mkjing/article/details/53149629