在上一篇中了解了 在Block中,外部传入的变量是如何被保存在Block对象中的。通过对其实现本质的了解,可以知道对于Block对象中的值拷贝,改变其值,并不能改变Block外部变量的值。事实上,XCode编译器也不允许我们改变传入Block对象中的变量。
但是在变量前加上__block关键字,变量就可以在Block中改变并影响外部的变量值。这显然不是简单的值拷贝能够实现的。那么,其原理又是什么呢?
__block关键字变量
有如下代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int val = 10;
void(^blk)(void) = ^{ val = 5;};
blk();
}
return 0;
}
很简单,我们用clang -rewrite-objc 命令将其编译为C++代码
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 5;}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}
是不是眼花了?我们先看main函数
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}
可以看到,val变量与Block对象分别转换为类型
__Block_byref_val_0
和
__main_block_impl_0
类型。__main_block_impl_0类型我们之前已经分析过,对应Block对象类型,而此时的val变量,竟然也被转换为了结构体。
再看__Block_byref_val_0结构体定义:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
我们先看下__block变量val是如何被初始化的
__block int val = 10;
对应代码
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
别的还好理解,但这个__forwarding指向自己的指针是干嘛用的呢?别急,我们先看看__block变量是怎么传入到Block中的,而它又是怎么在Block中被赋值的
将__block传入Block中
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
...
void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
...
}
return 0;
}
可以看到在Block的结构体定义中,有一个__block变量指针(为什么不直接使用__block变量而是指针呢?这是为了保证在多个Block内,可以通过操作指向同一个__block变量指针的方式实现对同一个__block变量进行操作。)
__Block_byref_val_0 *val;
它就用来存储传入Block的__block变量的。
它在构造函数中被赋值
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, <span style="color:#ff6666;">__Block_byref_val_0 *_val</span>, int flags=0) : <span style="color:#ff0000;">val(_val->__forwarding) </span>
有意思的是,val并没有被直接被赋值为_val,而是被赋值为_val->__forwarding。
再看block impl中该变量是如何在Block中赋值的
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(<span style="color:#ff0000;">val->__forwarding->val</span>) = 5;}
这给人的感觉就像__block变量在Block中使用时,编译器不会直接使用__block变量指针,而是间接的使用__block->__forwarding指针。这样做的目的是什么呢?且看:
__forwarding存在的理由
在之前我们了解了,Block对象是一种OC对象,其实,Blcok对象按照存储位置的不同,可以分为如下三种
_NSConcreteGlobalBlock //存在于程序数据区
_NSConcreteStackBlock //存在在栈上
_NSConcreteMallocBlock //存在于堆上
我们对于数据区与堆上的Block对象,并不存在Block对象释放的危险,在该对象的作用域外也可以安全的访问这些Block对象。
但是对于Stack Block,由于其存在于栈上,超出其作用域后,内存会被回收,其对应的Block对象,当然就不能够再被正确的访问。但是,现实是当存在于栈上的Block超出其作用域后,仍能够被访问。(比如异步执行的Dispatch,其Block在真正被调用时,很可能已经结束了其作用域。)
这是怎么实现的呢?原来,当需要时,栈上的Block会被自动的复制到堆上来存储,这样Block对象的作用域便获得了延长。如书上的插图。
那么对于存在于Block对象中的__block变量呢?
当Block对象被复制到堆上时,其内部变量__block变量也被复制到堆上(注意这里说的是‘复制’不是‘剪切’)。若多个Block对象同时拥有同一__block变量,则当Block被复制到堆上时,__block变量只会在第一次时被复制到堆,其余只会增加堆__block的引用计数。
如图:
前面说到,__block变量会随着Block对象复制到堆上,那么就可能出现__block变量在内存中的两份拷贝:一份在堆上,另一份在栈上。如下代码所示的情况:
__block int val = 0;
void (^blk)(void) = [^{ ++val;} copy]; //copy方法复制Block到堆上,同时__block变量也被复制到堆
++val; //栈上的val加加
blk(); //堆上的val加加
NSLog(@"%d", val); //输出2
代码分别对栈上的val与堆上的val进行了操作,按说这应当是两个不同的结构体变量,但最终的结果却显示似乎是对同一个val进行了两次++操作?这是怎么实现的呢?
原来是__block变量的_forwarding指针的作用,当__block变量由栈拷贝到堆上时,栈上的_forwarding指针会指向堆上的变量,这样通过操作_forwarding指针,实现了对同一个变量的操作。
总结
1.在Block中可以通过_block变量来改变传入Block中的变量值,_block变量其实是一个_block结构体对象。
2.在适当情况时,Block变量会被拷贝到堆上,使Block变量超出其作用域仍然能够被访问,同时,Block变量中的__block变量也被拷贝到堆上。
3.在栈和堆上的_block变量通过_forwarding指针来保证是对同一个_block变量进行操作。