Block存储
Block存储区域
在之前我们看到了block的isa
指向的为_NSConcreteStackBlock
其实 除此之外还还有其他两种:
_NSConcreteStackBlock
:该类对象被设置到栈上_NSConcreteGlobalBlock
:与全局变量类似,被设置在城区的数据区域.data区
中_NSConcreteMallocBlock
:设置在由malloc函数分配的内存块(堆)中
block为_NSConcreteGlobalBlock
的情况:
记述全局变量的地方有Block语法时
block写在全局变量区,此时转换源代码可以看到impl.isa=&_NSConcreteGloabalBlock
。因为在全局变量区中不能使用自动变量,所以不存在对自动变量的截获,因此整个程序只需要一个实例即可,故将Block用结构体实例设置在全局变量相同的数据区Block语法的表示式中不使用截获的自动变量时
typedef int(^blk_t)(int) for(int rate=0; rate<10; ++rate) {
blk_t blk = ^(int count){
return count
}
}即使在函数内部,不在全局变量区域使用Block语法,只要Block不截获自动变量,就可以将Block用结构体实例设置在数据区域
Block在超出变量作用域还能存在的原因
对于配置在全局数据区的block 超出变量作用区可以安全使用。
设置在栈上的block和__block变量,是在栈上,当作用域结束,会被废弃,可以通过将block
和__block
变量复制到堆上解决此问题
当将block复制到堆上后,其isa=&_NSConceteMallocBlock
在ARC情况下,编译器大多会自动判断,生成将block从栈到堆上的代码
typedef int(^blk_t)(int)
blk_t func(int rate) {
return ^(int count) {
return rate*count
}
}
//转换为源代码
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) //等效于 tmp = Block_Copy(tmp)
return objc_autoreleaseReturnValue(tmp)
}
tmp = objc_retainBlock(tmp)
其实就是执行了Block_Copy
函数
虽然大多时候编译器会自动判断,但是特殊情况,我们需要手动copy
,将其从栈复制到堆上
当向方法或者函数的参数中传递Block时,需要手动copy
但是如果在方法或者函数中对复制了传递过来的参数,就不需要在调用函数之前手动复制了
以下为不需要手动复制情况:- Cocoa框架中的方法,并且方法名字中含有
usingBlock
时 - GCD的API中
//例如 - (id)getBlockArray {
int val = 10
return [[NSArray alloc] initWithObjects: [^{NSLog(@"blk0:%d", val);} copy], nil];
}
//作为参数传递的block需要copy 否则block在出了该函数作用区域就被废弃- Cocoa框架中的方法,并且方法名字中含有
不论block'在何处,对其执行copy方法都不会有任何问题
- 对栈上的block copy 将其从栈上复制到堆上
- 对数据区的block copy 什么也不做
- 对堆上的block copy 引用计数增加
注意
对block多次调用 copy 也是不会有任何问题的 [[[[blk copy] copy] copy] copy]
__block变量存储区域
当Block从栈复制到堆上时,其使用的__block变量也会复制到堆上,此时Block会持有__block变量
当多个block使用__block变量时,当第一个block从栈复制到堆上时,__block变量就复制到堆上,并被改block持有,而其他block再从栈复制到堆上时,只是对该__block的引用计数增加了而已
注意
在栈上的block只是对__block变量使用 而并不会持有
block使用forwarding原因
这样设计的主要原因是: 为了不论__block变量是在栈还是堆上都能够正确访问
因为在将__block从栈复制到堆上之后,我们就有两个可访问:栈上的__block变量和堆上的__block变量
此时访问该变量的方法均为++(val->__forwarding->val)
将变量从栈复制到堆上时,就将成员变量__forwarding
的值替换为了指向了堆上的__block
变量结构体实例的地址
在将block从栈上复制到堆上后,此时会同时存在栈上和堆上的Block和__block变量,栈上的block保持不变,而堆上的block的isa设置为&_NSConcreteMallocBlock
。栈上的__block变量的结构体指针__forwarding
会被修改指向堆上的__block变量
,而堆上的__block
变量的__forwarding
指针不变, 依然指向堆上的__block变量
截获对象
当block内部使用的是 id类型或者对象类型变量时,
blk_t blk;
{
id array = [[NSMutableArray alloc] init]
blk = [^(id objc){
[array addObject: obj]
NSLog("%ld", [array count])
} copy]
}
blk([[NSObject alloc] init]) //1
blk([[NSObject alloc] init]) //2
blk([[NSObject alloc] init]) //3
此时array在变量作用域结束或 依旧存在
注意
必须在block后执行copy方法,因为只有调用了_Block_Copy方法才能持有截获的__strong类型对象。否则不调用
_Block_Copy
函数的话,即使截获了对象,也会随着变量作用域的结束而被废弃
//转换源码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
id __strong array; //id __strong修饰符的成员变量
//结构体初始化函数
__main_block_impl_0(void *fp, strct __main_block_desc_0, id __strong _array, int flags=0): array(_array) {
impl.isa = & NSConcreatStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
}
可以看到 array被截获成为block结构体的成员变量
内存管理中说C语言的结构体中不能含有附有__strong
修饰符的成员变量。因为编译器不知道何时能进行C语言结构体的初始化和废弃操作,不能很好的管理内存。但是Object-C
的运行时 能帮助准确掌握Block从栈复制到堆上以及堆上的block被废弃的时机,因此在Block的结构体中即使有__strong或者__weak的修饰符的变量,也可以恰当的进行初始化和废弃。为此需要在_main_block_des_0
中增加成员变量copy和dispose,以及作为指针赋值给该成员变量的_main_block_copy_0
和_main_block_dispose_0
函数
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct _main_block_impl_0 *src) {
_Block_object_assign(&det->array, src->array, BLOCK_FIFLD_IS_OBJECT)
}
static void __main_block_dispose_0(struct _main_block_impl_0 *src) {
_Block_object_dispose(src->array, BLOCK_FIFLD_IS_OBJECT)
}
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
void (*copy)(struct __main_block_impl_0 *)
void (*dispose)(struct __main_block_impl_0 *)
}
使用_Block_object_assign
将对象类型赋值给block结构体的成员变量并持有该对象。相当于调用了retain
方法
使用_Block_object_dispose
来释放在block中的结构体变量中的成员变量,相当于release
实例方法
当block复制到堆上时调用copy函数,当堆上的block被废弃时调用dispose
函数
Block复制到堆上的时机?
- 调用Block的copy实例方法时
- Block作为函数返回值返回时
- 将Block赋值给附有__strong修饰符id类型的类或者Block类型的成员变量时
- 在方法名中含有
usingBlock
的Cocoa框架方法或者GCD的API中传递Block时
根据block存储区域的学习可知,其实就是当__Block_copy
方法被调用时,Block从栈复制到堆上
当释放block谁都不持有时,调用dispose
函数,相当于调用dealloc实例方法
__block <==> 对象
我们在使用__block
变量时 与 截获对象类型相似
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct _main_block_impl_0 *src) {
_Block_object_assign(&det->val, src->val, BLOCK_FIFLD_IS_BYREF)
}
static void __main_block_dispose_0(struct _main_block_impl_0 *src) {
_Block_object_dispose(src->val, BLOCK_FIFLD_IS_BYREF)
}
通过BLOCK_FIFLD_IS_BYREF/BLOCK_FIFLD_IS_OBJECT
区分是__block变量还是对象类型。
与对象相同 copy函数持有__block变量 dispose释放持有的__block变量,因为都被block持有,因此与block类似可以在超出其变量作用域而存在
__block对象变量
__block id obj = [[NSObject alloc] init]
__block id __strong obj = [[NSObject alloc] init]
源代码转换如下:
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
void (* __Block_byref_id_object_copy)(void *, void*)
void (* __Object_object_dispose)(void *)
__strong id obj;
}
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Object_object_assign((char *)det + 40, *(void **) ((char *)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Object_object_dispose( *(void **) ((char *)src + 40), 131);
}
__block声明
__Block_byref_obj_0 obj = {
0,
&obj,
0x20000000,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
[[NSObject alloc] init],
}
与之前Block截获__strong或者id类型的过程相同,当__block变量从栈复制到堆上的时候,使用_Object_object_assign
函数,持有赋值给__block
变量的对象,当__block
变量被废弃时,使用_Object_object_dispose
函数,释放赋值给__block变量的对象。因此,只要__block变量在堆上继续存在,那么该变量就会处于被持有状态
对于__weak修饰符,即使附加了__block修饰符 当变量被释放时,nil
也会被附加到__weak
修饰符的变量中
注意
因为不存在__autorelasing修饰符和__block修饰符同时存在的必要,因此 同时指定会引起编译错误
Block循环引用
如果在block中使用__strong类型修饰符的变量,因为会随着从栈复制到堆上,该对象被block持有而引起循环引用
typedef blk_t blk;
@interface MyObject: NSObject
{
blk_t blk_;
id obj_;
}
@end
@implementation MyObject
- (id)init {
self = [super init]
blk_ = ^{
NSLog(@"%@", obj_);
};
return self;
}
@end
实际上
blk_=^{NSLog(@"obj_=%@", self->obj_);}
block内部捕获了self
对于该实例 将不会dealloc
,因为 blk_是该实例成员成员变量,而且blk_因为赋值给成员变量也会从栈复制到堆,blk_持有其附有的__strong修饰符的id类型变量self
使用__weak避免循环引用
- (id)init {
self = [super init]
id __weak obj = obj_;
blk_ = ^{
NSLog(@"%@", self);
};
return self;
}
注意
因为此时此时Block存在 持有其值的对象必定存在 因此也不需要进行判断是否为nil
使用__block避免循环引用
- (id)init {
self = [super init]
__block id tmp = self
blk_ = {
NSLog(@"%@", tmp)
tmp = nil;
};
}
...
int main() {
id o = [[NSObject alloc] init]
[o execBlock];
}
此时代码并没有引起循环引用,但是如果不调用execBlock
即执行成员变量blk_
就会循环引用,造成内存泄漏
当执行了该block后,__Block
变量不再持有该对象,而block虽然持有__block变量但是也就不再造成循环引用了
注意
使用__block避免循环引用必须执行block