iOS - __block 修饰符底层探索

简介: 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类型的结构体,总结关系图如下:


image.png

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开发
iOS block修饰符用copy还是strong
iOS block修饰符用copy还是strong
167 0
|
iOS开发 Python
iOS小技能:lldb打印block参数签名
iOS逆向时经常会遇到参数为block类型,本文介绍一个lldb script,可快速打印出Objective-C方法中block参数的类型。
203 0
iOS小技能:lldb打印block参数签名
|
存储 Unix 编译器
|
存储 算法 iOS开发
|
存储 缓存 算法
iOS底层学习——对象初始化探索
iOS底层学习——对象初始化探索
|
iOS开发 开发者
iOS开发 - 如何写出漂亮的block
iOS开发 - 如何写出漂亮的block
109 0
|
iOS开发
iOS开发- 关于Block的几种应用
iOS开发- 关于Block的几种应用
120 0
|
自然语言处理 iOS开发
IOS——Block
IOS——Block
80 0
|
iOS开发
iOS代理 通知 block传值的规范写法
iOS代理 通知 block传值的规范写法
146 0
|
存储 API iOS开发
iOS Principle:Block(下)
iOS Principle:Block(下)
134 0
iOS Principle:Block(下)