美文网首页
iOS - __block 修饰符底层探索

iOS - __block 修饰符底层探索

作者: 码代码的小马 | 来源:发表于2021-05-02 18:50 被阅读0次

    Block技术合集

    iOS - Block变量截获
    Block的写法及使用

    阅读本文前,请先思考如下问题

    • 为什么Block可以截获变量
    • 为什么Block外定义的基本数据类型,在Block内部不能修改
    • 为什么用__block修饰后,在Block内部可以修改
      本文将对Block底层探索并解答如上三个问题

    什么是Block

    带有自动变量值的匿名函数

    Block截获变量

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void(^blk)(void) = ^{
                printf("Block\n");
            };
            blk();
        }
        return 0;
    }
    

    编译成成cpp代码, 代码非常多,我们精简如下

    //1. 结构体
    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    //2. 
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    //3. 
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
                printf("Block\n");
            }
    
    //4. 
    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)};
    
    //5. main函数代码块
    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));
            ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        }
        return 0;
    }
    

    总共4个结构体和一个main函数代码块
    查看5. main函数代码块 可见,block对象被编译成了__main_block_impl_0类型的结构体, 这个结构体由两个成员结构体和一个构造函数组成,两个结构体分别是__block_impl__main_block_desc_0类型的,其中__block_impl结构体中有一个函数指针, 指针指向__main_block_func_0类型的结构体,总结关系图如下:

    Block在定义的时候:

    ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))
    

    Block在调用的时候:

    ((__block_impl *)blk)->FuncPtr
    

    Block内部的函数打印,很显然放在了__main_block_func_0,那么block内部截获的数据存放在哪呢?同样 我们对如下代码进行编译

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int a = 10;
            void(^blk)(void) = ^{
                printf(" Block\n a = %d\n", a);
            };
            blk();
        }
        return 0;
    }
    

    编译成cpp

    //1. 
    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int a;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
    
                printf(" Block\n a = %d\n", a);
            }
    
    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 a = 10;
            void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
            ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        }
        return 0;
    }
    
    

    很显然的是,__main_block_impl_0结构体增加了成员变量int a;并且在结构体的构造函数__main_block_func_0中对变量进行赋值int a = __cself->a,而这一赋值操作,在Block定义的时候就已完成(并非在Block调用的时候),这也是Block截获变量的原理(文章开头问题1:为什么Block可以截获变量)。Block对不同数据类型截获方式请查看我之前写的iOS - Block变量截获

    为什么Block中不能修改变量值

    我们先把代码做微小的修改,即对 block外定义的变量'int a = 10', 分别在block定义前后及block内部打印其地址

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int a = 10;
            printf("before block &a = %p \n", &a);
            void(^blk)(void) = ^{
                printf(" Block\n a = %d\n in block &a = %p \n ", a, &a);
            };
            printf("after  block &a = %p \n\n", &a);
            blk();
        }
        return 0;
    }
    

    打印如下:

    before block &a = 0x7ffeefbff4ec 
    after  block &a = 0x7ffeefbff4ec 
    
     Block
     a = 10
     in block &a = 0x1004385f0 
    

    很明显的是,外block外部打印的int a地址一致,但在block内部却不一样了,即block内部的a并不是我们外部定义的int a(此时作者想起了一首歌:你说的黑不是黑,你说的白是神魔TM的白...)

    这里问题二的答案已经很明显了,为什么block内部无法修改外部的变量,因为就不是同一个变量啊,只是长的一样而已
    有人就问了,那block内部的那个a究竟是谁从哪里来?请看前边编译的cpp代码中block方法的结构体__main_block_func_0

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
    
                printf(" Block\n a = %d\n", a);
            }
    

    此时你应该恍然大悟,这个a是block内部重新定义的a,取值自block外部定义的int a = 10,至此,block内部无法修改外部变量的问题显而易见:

    为什么无法修改:因为不是同一个值,地址不一样
    内部的a变量哪来的:block底层重新定义的,取值自外部(相当于副本)

    为什么用__block修饰后,在Block内部可以修改

    先附上__block修饰前编译的main函数源码(用于下文做比较)

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            int a = 10;
            printf("before block &a = %p \n", &a);
            void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
            printf("after  block &a = %p \n\n", &a);
            ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        }
        return 0;
    }
    

    不废话,改代码加__block修饰,先打印看看

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block int a = 10;
            printf("before block &a = %p \n", &a);
            void(^blk)(void) = ^{
                printf(" Block\n a = %d\n in block &a = %p \n ", a, &a);
            };
            printf("after  block &a = %p \n\n", &a);
            blk();
        }
        return 0;
    }
    
    before block &a = 0x7ffeefbff4e8 
    after  block &a = 0x103009f98 
     Block
     a = 10
     in block &a = 0x103009f98 
    

    根据打印,很明显的能看到,a在__block修饰定义时的地址,与block内部及block定义后的地址不一致,此处大胆猜测,__block修饰的变量,在block定义时,会生成新的对象(下文得知是结构体),在block外部获取、更改该变量时,获取的是这个新生成的对象

    我们编译一下

    //1. 
    struct __Block_byref_a_0 {
      void *__isa;
    __Block_byref_a_0 *__forwarding;
     int __flags;
     int __size;
     int a;
    };
    
    //2. 
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_a_0 *a; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    //3. 
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_a_0 *a = __cself->a; // bound by ref
    
                printf(" Block\n a = %d\n in block &a = %p \n ", (a->__forwarding->a), &(a->__forwarding->a));
            }
    
    //4. 
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
            printf("before block &a = %p \n", &(a.__forwarding->a));
            void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
            printf("after  block &a = %p \n\n", &(a.__forwarding->a));
            ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        }
        return 0;
    }
    

    编译后源码先找不同

    • 定义int a = 10变成了__Block_byref_a_0 a = 10(精简)
    • 多了各结构体__Block_byref_a_0,而此结构体内部有int a
    • __main_block_impl_0 结构体中的int a不见了,多了个__Block_byref_a_0 *a
    • __main_block_func_0结构体中的int a = __cself->a变成了__Block_byref_a_0 *a = __cself->a
    • block外部的printf("after block &a = %p \n\n", &a)变成了printf("after block &a = %p \n\n", &(a.__forwarding->a))

    上文不同翻译总结一下就是答案:
    变量添加__block修饰后,变量会被封装称结构体,结构体内部包含变量,
    在block内部修改变量时,修改的是结构体__Block_byref_a_0内部的变量数据(a->__forwarding->a)(所以可以修改)
    出了block作用域后,修改数据修改的仍然是__Block_byref_a_0内部的变量数据(a.__forwarding->a)

    疑问:

    printf("before block &a = %p \n", &(a.__forwarding->a));
    printf("after  block &a = %p \n\n", &(a.__forwarding->a));
    before block &a = 0x7ffeefbff4e8 
    after  block &a = 0x100474798 `
    

    查看编译后底层代码,打印地址查找都是&(a.__forwarding->a)),为什么打印出来的地址不同

    相关文章

      网友评论

          本文标题:iOS - __block 修饰符底层探索

          本文链接:https://www.haomeiwen.com/subject/kmbtdltx.html