iOS - __block 修饰符底层探索

724 阅读6分钟

##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)),为什么打印出来的地址不同