[转]黑幕背后的__block修饰符
http://www.cocoachina.com/ios/20150106/10850.html
我们知道在Block使用中,Block内部能够读取外部局部变量的值。但我们需要改变这个变量的值时,我们需要给它附加上__block修饰符。
__block另外一个比较多的使用场景是,为了避免某些情况下Block循环引用的问题,我们也可以给相应对象加上__block 修饰符。
为什么不使用__block就不能在Block内部修改外部的局部变量?
我们把以下代码通过 clang -rewrite-objc 源代码文件名重写:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int val = 10;
void (^block)(void) = ^{
NSLog(@"%d", val);
};
block();
}
return 0;
}
得到如下代码:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__val_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_41daf1_mi_0, val);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int val = 10;
void (*block)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val);
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
我们注意到Block实质被转换成了一个__main_block_impl_0的结构体实例,其中__main_block_impl_0结构体的成员包括局部变量val。在__main_block_impl_0结构体的构造方法中,val作为第三个参数传递进入。
但执行我们的Block时,通过block找到Block对应的方法执行部分__main_block_func_0,并把当前block作为参数传递到__main_block_func_0方法中。
__main_block_func_0的第一个参数声明如下:
struct __main_block_impl_0 *__cself
它和Objective-C的self相同,不过它是指向 __main_block_impl_0 结构体的指针。
这个时候我们就可以通过__cself->val对该变量进行访问。
那么,为什么这个时候不能给val进行赋值呢?
因 为main函数中的局部变量val和函数__main_block_func_0不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中, 我们可以通过指针来实现局部变量的修改。不过这是由于在调用__main_block_func_0时,main函数栈还没展开完成,变量val还在栈 中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已 经不在栈中了(block此时在哪里?),再用指针访问就……
所以,对于auto类型的局部变量,不允许block进行修改是合理的。
__block 到底是怎么工作的?
我们把以下代码通过 clang -rewrite-objc 源代码文件名重写:
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSInteger val = 0;
void (^block)(void) = ^{
val = 1;
};
block();
NSLog(@"val = %ld", val);
}
return 0;
}
可得到如下代码:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
NSInteger 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) = 1;
}
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[]) {
{ __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};
void (*block)(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 *)block)->FuncPtr)((__block_impl *)block);
NSLog((NSString *)&__NSConstantStringImpl__val_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_d7fc4b_mi_0, (val.__forwarding->val));
}
return 0;
}
我们发现由__block修饰的变量变成了一个__Block_byref_val_0结构体类型的实例。该结构体的声明如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
NSInteger val;
};
注意到这个结构体中包含了该实例本身的引用 __forwarding。
我们从上述被转化的代码中可以看出 Block 本身也一样被转换成了 __main_block_impl_0 结构体实例,该实例持有__Block_byref_val_0结构体实例的指针。
我们再看一下赋值和执行部分代码被转化后的结果:
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) = 1;
}
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
我们从__cself找到__Block_byref_val_0结构体实例,然后通过该实例的__forwarding访问成员变量val。成员变量val是该实例自身持有的变量,指向的是原来的局部变量。如图所示:

上面部分我们展示了__block变量在Block查看和修改的过程,那么问题来了:
当block作为回调执行时,局部变量val已经出栈了,这个时候代码为什么还能正常工作呢?
我们为什么是通过成员变量__forwarding而不是直接去访问结构体中我们需要修改的变量呢? __forwarding被设计出来的原因又是什么呢?
存储域
通 过上面的描述我们知道Block和__block变量实质就是一个相应结构体的实例。我们在上述转换过的代码中可以发现 __main_block_impl_0 结构体构造函数中, isa指向的是 _NSConcreteStackBlock。Block还有另外两个与之相似的类:
_NSConcreteStackBlock 保存在栈中的block,出栈时会被销毁
_NSConcreteGlobalBlock 全局的静态block,不会访问任何外部变量
_NSConcreteMallocBlock 保存在堆中的block,当引用计数为0时会被销毁
上述示例代码中,Block是被设为_NSConcreteStackBlock,在栈上生成。当我们把Block作为全局变量使用时,对应生成的Block将被设为_NSConcreteGlobalBlock,如:
void (^block)(void) = ^{NSLog(@"This is a Global Block");};
int main(int argc, const char * argv[]) {
@autoreleasepool {
block();
}
return 0;
}
该代码转换后的代码中,Block结构体的成员变量isa的初始化如下:
impl.isa = &_NSConcreteGlobalBlock;
那么_NSConcreteMallocBlock在什么时候被使用呢?
分配在全局变量上的Block,在变量作用域外也可以通过指针安全的访问。但分配在栈上的Block,如果它所属的变量作用域结束,该Block就被废弃。同样地,__block变量也分配在栈上,当超过该变量的作用域时,该__block变量也会被废弃。
这 个时候_NSConcreteMallocBlock就登场了,Blocks提供了将Block和__block变量从栈上复制到堆上的方法来解决这个问 题。将分配到栈上的Block复制到堆上,这样但栈上的Block超过它原本作用域时,堆上的Block还可以继续存在。
复制到堆上的Block,它的结构体成员变量isa将变为:
impl.isa = &_NSConcreteMallocBlock;
而_block变量中结构体成员__forwarding就在此时保证了从栈上复制到堆上能够正确访问__block变量。在这种情况下,只要栈上的_block变量的成员变量__forwarding指向堆上的实例,我们就能够正确访问。
我 们一般可以使用copy方法手动将 Block 或者 __block变量从栈复制到堆上。比如我们把Block做为类的属性访问时,我们一般把该属性设为copy。有些情况下我们可以不用手动复制,比如 Cocoa框架中使用含有usingBlock方法名的方法时,或者GCD的API中传递Block时。
当一个Block被复制到堆上时,与之相关的__block变量也会被复制到堆上,此时堆上的Block持有相应堆上的__block变量。当堆上的__block变量没有持有者时,它才会被废弃。(这里的思考方式和objc引用计数内存管理完全相同。)
而在栈上的__block变量被复制到堆上之后,会将成员变量__forwarding的值替换为堆上的__block变量的地址。这个时候我们可以通过以下代码访问:
val.__forwarding->val
如下面:

__block变量和循环引用问题
__block修饰符可以指定任何类型的局部变量,上面的转换代码中,有如下代码:
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*/);
}
当Block从栈复制到堆时,会使用_Block_object_assign函数持有该变量(相当于retain)。当堆上的Block被废弃时,会使用_Block_object_dispose函数释放该变量(相当于release)。
由上文描述可知,我们可以使用下述代码解除Block循环引用的问题:
__block id tmp = self;
void(^block)(void) = ^{
tmp = nil;
};
block();
通过执行block方法,nil被赋值到_block变量tmp中。这个时候_block变量对 self 的强引用失效,从而避免循环引用的问题。使用__block变量的优点是:
通过__block变量可以控制对象的生命周期
在不能使用__weak修饰符的环境中,我们可以避免使用__unsafe_unretained修饰符
在执行Block时可动态地决定是否将nil或者其它对象赋值给__block变量
但是这种方法有一个明显的缺点就是,我们必须去执行Block才能够解除循环引用问题,否则就会出现问题。
[转]黑幕背后的__block修饰符的更多相关文章
- __weak与__block修饰符区别
API Reference对__block变量修饰符的解释,大概意思: 1.__block对象在block中是可以被修改.重新赋值的. 2.__block对象在block中不会被block强引用一次, ...
- IOS开发 __weak与__block修饰符到底有什么区别
API Reference对__block变量修饰符有如下几处解释: //A powerful feature of blocks is that they can modify variables ...
- __block和__weak修饰符的区别
block下循环引用的问题 __block本身并不能避免循环引用,避免循环引用需要在block内部把__block修饰的obj置为nil __weak可以避免循环引用,但是其会导致外部对象释放了之后, ...
- Swift lazy 修饰符和方法
LAZY 修饰符和 LAZY 方法 由 王巍 (@ONEVCAT) 发布于 2015/10/07 延时加载或者说延时初始化是很常用的优化方法,在构建和生成新的对象的时候,内存分配会在运行时耗费不少时间 ...
- 【iOS】Swift LAZY 修饰符和 LAZY 方法
延时加载或者说延时初始化是很常用的优化方法,在构建和生成新的对象的时候,内存分配会在运行时耗费不少时间,如果有一些对象的属性和内容非常复杂的话,这个时间更是不可忽略.另外,有些情况下我们并不会立即用到 ...
- 环境变量、block、修饰符:block对环境变量的引用和修改需要通过修饰符来限定
环境变量.block.修饰符:block对环境变量的引用和修改需要通过修饰符来限定. http://www.cnblogs.com/fengmin/p/5816580.html - (NSUInteg ...
- Java中final修饰符深入研究
一.开篇 本博客来自:http://www.cnblogs.com/yuananyun/ final修饰符是Java中比较简单常用的修饰符,同时也是一个被"误解"较多的修饰符.对很 ...
- 01 (OC)* @property 后面可以有哪些修饰符?
一:@property 后面可以有哪些修饰符? 1:线程安全的: atomic,nonatomic 2:访问权限的 readonly,readwrite 3:内存管理(ARC) assign, cop ...
- JAVA语言中的修饰符
JAVA语言中的修饰符 -----------------------------------------------01--------------------------------------- ...
随机推荐
- GeoServer基础教程(四):空间数据互操作的接口规范WMS、WFS和WCS
转载:https://ethanblog.com/tech/all-about-wms-wfs-and-wcs.html 前面几节介绍了GeoServer基础教程的一些基本操作,相信大家对GeoSer ...
- JavaUtil_09_通用工具类-01_Hutool
一.重要的官方资料 1. Hutool 官网 2. Hutool 参考文档 3. Hutool API文档
- Metasploit的使用下篇——漏洞攻击
一.上文总结 上篇当中主要通过开启的端口对目标主机进行攻击,最后成功做到了连接不过却不能查看到telnet的登陆用户名和密码,目前来看有两个原因:第一.我对于靶机telnet的设置有问题,没有设置相应 ...
- AngularJS directive简述
转自:http://segmentfault.com/q/1010000002400734 官方API:http://docs.angularjs.cn/api/ng/service/$compile ...
- 洛谷【P2201】数列编辑器
我对模拟的理解:http://www.cnblogs.com/AKMer/p/9064018.html 题目传送门:https://www.luogu.org/problemnew/show/P220 ...
- 【转】 Pro Android学习笔记(六七):HTTP服务(1):HTTP GET
目录(?)[-] HTTP GET小例子 简单小例子 出现异常NetworkOnMainThreadException 通过StrictMode进行处理 URL带键值对 Andriod应用可利用ser ...
- Ruby 局部变量做block参数
Ruby中使用yield语句调用block时可以带有参数,参数值见传送个相关联的block.如果传给block的参数是已经存在的局部变量,那么这些变量即为block的参数,他们的值可能会因block的 ...
- java基础知识(4)---设计模式
设计模式:解决问题最行之有效的思想.是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可靠性. java中有23种设计模 ...
- sql server 2008 开启1433端口,开启远程连接
通常情况下只需要设置两处
- js 函数定义的两种方式以及事件绑定(扫盲)
一.事件(例如:onclick)绑定的函数定义放在jsp前面和放后面没影响 二. $(function() { function func(){}; }) onclick通过如下方式绑定事件到jsp中 ...