zoukankan      html  css  js  c++  java
  • [转]黑幕背后的__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是该实例自身持有的变量,指向的是原来的局部变量。如图所示:

    263.jpg

    上面部分我们展示了__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

    如下面:

    29.jpg

    __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才能够解除循环引用问题,否则就会出现问题。

  • 相关阅读:
    使用golang访问kubebernetes
    使用 Rancher 管理现有 Kubernetes 集群
    Running powershell scripts during nuget package installation and removal
    How to Create, Use, and Debug .NET application Crash Dumps in 2019
    寻找写代码感觉(一)之使用 Spring Boot 快速搭建项目
    Selenium+Java之解决org.openqa.selenium.InvalidArgumentException: invalid argument报错问题
    Selenium环境搭建
    关于Xpath定位方法知道这些基本够用
    Web自动化之浏览器启动
    【翻译】编写代码注释的最佳实践
  • 原文地址:https://www.cnblogs.com/Cheetah-yang/p/4898711.html
Copyright © 2011-2022 走看看