黑幕背后的__block修饰符

我们知道在Block使用中,Block内部能够读取外部局部变量的值。但我们需要改变这个变量的值时,我们需要给它附加上__block修饰符。

__block另外一个比较多的使用场景是,为了避免某些情况下Block循环引用的问题,我们也可以给相应对象加上__block 修饰符。

为什么不使用__block就不能在Block内部修改外部的局部变量?

我们把以下代码通过 clang -rewrite-objc 源代码文件名重写:

 1 int main(int argc, const char * argv[]) {
 2     @autoreleasepool {
 3         int val = 10;
 4         void (^block)(void) = ^{
 5             NSLog(@"%d", val);
 6         };
 7         block();
 8     }
 9     return 0;
10 }

得到如下代码:

 1 struct __main_block_impl_0 {
 2   struct __block_impl impl;
 3   struct __main_block_desc_0* Desc;
 4   int val;
 5   __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
 6     impl.isa = &_NSConcreteStackBlock;
 7     impl.Flags = flags;
 8     impl.FuncPtr = fp;
 9     Desc = desc;
10   }
11 };
12 static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
13   int val = __cself->val; // bound by copy
14   NSLog((NSString *)&__NSConstantStringImpl__val_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_41daf1_mi_0, val);
15 }
16 static struct __main_block_desc_0 {
17   size_t reserved;
18   size_t Block_size;
19 } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
20 int main(int argc, const char * argv[]) {
21     /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
22         int val = 10;
23         void (*block)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val);
24         ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
25     }
26     return 0;
27 }

我们注意到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 源代码文件名重写:

 1 int main(int argc, const char * argv[]) {
 2     @autoreleasepool {
 3         __block NSInteger val = 0;
 4         void (^block)(void) = ^{
 5             val = 1;
 6         };
 7         block();
 8         NSLog(@"val = %ld", val);
 9     }
10     return 0;
11 }

可得到如下代码:

 1 struct __Block_byref_val_0 {
 2      void *__isa;
 3      __Block_byref_val_0 *__forwarding;
 4      int __flags;
 5      int __size;
 6      NSInteger val;
 7 };
 8 struct __main_block_impl_0 {
 9       struct __block_impl impl;
10       struct __main_block_desc_0* Desc;
11       __Block_byref_val_0 *val; // by ref
12       __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
13         impl.isa = &_NSConcreteStackBlock;
14         impl.Flags = flags;
15         impl.FuncPtr = fp;
16         Desc = desc;
17       }
18 };
19 static void __main_block_func_0 (struct __main_block_impl_0 *__cself) {
20   __Block_byref_val_0 *val = __cself->val; // bound by ref
21   (val->__forwarding->val) = 1;
22 }
23 static void __main_block_copy_0 (struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
24     _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
25 }
26 static void __main_block_dispose_0 (struct __main_block_impl_0*src)     {
27     _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
28 }
29 static struct __main_block_desc_0 {
30   size_t reserved;
31   size_t Block_size;
32   void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
33   void (*dispose)(struct __main_block_impl_0*);
34 } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
35 int main(int argc, const char * argv[]) {
36     {   __AtAutoreleasePool __autoreleasepool;
37         __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};
38         void (*block)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344);
39         ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
40         NSLog((NSString *)&__NSConstantStringImpl__val_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_d7fc4b_mi_0, (val.__forwarding->val));
41     }
42     return 0;
43 }

我们发现由__block修饰的变量变成了一个__Block_byref_val_0结构体类型的实例。该结构体的声明如下:

1 struct __Block_byref_val_0 {
2      void *__isa;
3      __Block_byref_val_0 *__forwarding;
4      int __flags;
5      int __size;
6      NSInteger val;
7 };

注意到这个结构体中包含了该实例本身的引用 __forwarding。

我们从上述被转化的代码中可以看出 Block 本身也一样被转换成了 __main_block_impl_0 结构体实例,该实例持有__Block_byref_val_0结构体实例的指针。

我们再看一下赋值和执行部分代码被转化后的结果:

1 static void __main_block_func_0 (struct __main_block_impl_0 *__cself) {
2   __Block_byref_val_0 *val = __cself->val; // bound by ref
3   (val->__forwarding->val) = 1;
4 }
5 ((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,如:

1 void (^block)(void) = ^{NSLog(@"This is a Global Block");};
2 int main(int argc, const char * argv[]) {
3     @autoreleasepool {
4         block();
5     }
6     return 0;
7 }

该代码转换后的代码中,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修饰符可以指定任何类型的局部变量,上面的转换代码中,有如下代码:

1 static void __main_block_copy_0 (struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
2     _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
3 }
4 static void __main_block_dispose_0 (struct __main_block_impl_0*src)     {
5     _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
6 }

当Block从栈复制到堆时,会使用_Block_object_assign函数持有该变量(相当于retain)。当堆上的Block被废弃时,会使用_Block_object_dispose函数释放该变量(相当于release)。

由上文描述可知,我们可以使用下述代码解除Block循环引用的问题:

1 __block id tmp = self;
2 void(^block)(void) = ^{
3     tmp = nil;
4 };
5 block();

通过执行block方法,nil被赋值到_block变量tmp中。这个时候_block变量对 self 的强引用失效,从而避免循环引用的问题。使用__block变量的优点是:

  • 通过__block变量可以控制对象的生命周期
  • 在不能使用__weak修饰符的环境中,我们可以避免使用__unsafe_unretained修饰符
  • 在执行Block时可动态地决定是否将nil或者其它对象赋值给__block变量

但是这种方法有一个明显的缺点就是,我们必须去执行Block才能够解除循环引用问题,否则就会出现问题。

时间: 2024-11-08 03:27:16

黑幕背后的__block修饰符的相关文章

[转]黑幕背后的__block修饰符

http://www.cocoachina.com/ios/20150106/10850.html 我们知道在Block使用中,Block内部能够读取外部局部变量的值.但我们需要改变这个变量的值时,我们需要给它附加上__block修饰符. __block另外一个比较多的使用场景是,为了避免某些情况下Block循环引用的问题,我们也可以给相应对象加上__block 修饰符. 为什么不使用__block就不能在Block内部修改外部的局部变量? 我们把以下代码通过 clang -rewrite-ob

你真的理解__block修饰符的原理么?

开篇自测 在本文的开头,提出两个简单的问题,如果你不能从根本上弄懂这两个问题,那么希望你阅读完本文后能有所收获. 为什么block中不能修改普通变量的值? __block的作用就是让变量的值在block中可以修改么? 如果有的读者认为,问题太简单了,而且你的答案是: 因为编译器会有警告,各种教程也都说了不能修改. 应该是的吧. 那么我也建议你,抽出宝贵的几分钟时间阅读完本文吧.在开始揭开__block的神秘面纱之前,很不幸的是我们需要重新思考一下block的本质和它的实现. block是什么?

__weak与__block修饰符区别

API Reference对__block变量修饰符的解释,大概意思: 1.__block对象在block中是可以被修改.重新赋值的. 2.__block对象在block中不会被block强引用一次,从而不会出现循环引用问题. API Reference对__weak变量修饰符的解释,大概意思: 使用了__weak修饰符的对象,作用等同于定义为weak的property.自然不会导致循环引用问题,因为苹果文档已经说的很清楚,当原对象没有任何强引用的时候,弱引用指针也会被设置为nil. 因此,__

__weak与__block修饰符到底有什么区别

API Reference对__block变量修饰符有如下几处解释: //A powerful feature of blocks is that they can modify variables in the same lexical scope. You signal that a block can modify a variable using the __block storage type modifier. //At function level are __block vari

Java中final修饰符深入研究

一.开篇 本博客来自:http://www.cnblogs.com/yuananyun/ final修饰符是Java中比较简单常用的修饰符,同时也是一个被"误解"较多的修饰符.对很多Java程序员来说,他们大都只是草草看了一下各种书本上的介绍,然后背下来,什么时候想起 来有这东西就用一下.对于何时使用final修饰符.使用final修饰符对程序有什么影响,这些其实他们并不知道,当然在这篇文章之前,我也是一知半解的. 我们书本上对final的描述大概有三种用法: final可以修饰变量,

静态修饰符static,类中的常量定义修饰符

static可以用来区分成员变量.方法是属于类本身还是属于类实例化后的对象.有static修饰的成员属于类本身,没有static修饰的成员属于类的实例. 静态变量仅在局部函数域中存在,但当程序执行离开此作用域时,其值并不丢失static是一个修饰符,用于修饰成员(成员变量和成员函数)静态成员随着类的加载而加载.静态成员优先于对象存在.静态成员被所有对象所共享静态成员多了一个中调用方式,可以被类名直接调用.静态的优缺点优点: 静态成员多了一种调用方式.可以直接被类名调用 格式 :类名.静态成员.也

有趣的修饰符之??、?:、?

良好的程序修养在于灵活运用各类语法(修饰符).本文介绍下,在C#语言中的??(双问号操作符).?:(三元表达式).?(单问好操作符) 1,?? 为了实现Nullable数据类型转换成non-Nullable型数据,就有了一个这样的操作符"??(两个问号)",双问号操作符意思是取所赋值??左边的,如果左边为null,取所赋值??右边的. 比如int y = x ?? -1 如果x为空,那么y的值为-1. 于是这时候就可以把最上面第二段代码改成:string strParam= Reque

修饰符-包-内部类-代码块执行顺序

1.访问权限修饰符     从大到小的顺序为:public--protected--default--private     private--只能在同一类中使用;     default--不用写出来,默认不加.可以被同一包中的类使用     protected--可以被不同包的子类使用     public--可以被不同包的其它类使用 2.各种修饰符的修饰对象(可修饰哪些:类/接口/方法/属性)(多个修饰符连用是可以没有顺序的!)     1)访问权限修饰符:public/default--

访问修饰符

访问修饰符:public.protected.Default.private Public:是公共的,公有的.在任何一个地方都可以访问,一个类里面只有一个public的类,对外的公共类只有一个.成员变量可以用public.局部变量不可使用访问修饰符. Public void getName(){ } Protected:受保护的,在同一个包中可以访问,或者是不同包中的子类可以访问. Protected void getName(){} Default:默认的访问修饰符,只能在同一个包中访问. (