Block底层实现分析01-block本质/block捕获机制/block类型/block访问对象auto变量引用问题

2018年09月05日

  • 补充:转 C++ 使用的命令

注:分析参考 MJ底层原理班 内容,本着自己学习原则记录

本文使用的源码为objc4-723

转 C++ 使用的命令 :
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

1 block 的定义和类型

1.1 简单 block 定义和调用

1.2 block 的继承链(以__NSGlobalBlock__类型为参考对象)

  • block的继承链式__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
  • 也就说明 block 它实际上是一个 OC 对象

2 block 在 C++ 中的表现

2.1 将 mian.m 文件转化为 C++ 文件

  • 使用xcode工具 xcrun,指定架构模式
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 
// "-o main-arm64.cpp"  可指定输入文件名或者省略

2.2 main.cpp文件内容与 main.m 文件中的 mian 函数内容直观对比

2.2 将main.cpp 中 block 定义表达式和调用简化

  1. block定义
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

// 简化后
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
  1. block调用
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

// 简化后
block->FuncPtr(block);

2.3 block 定义简化解读void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

  1. void (*block)(void)表示 block 是一个函数指针,函数名为block
  2. =右边是指,将函数__main_block_impl_0的返回值,通过地址(&)符号,将函数的结果地址赋值给函数指针变量 block,但是一个是函数指针,一个结构体,不能直接赋值,所以原定义表达式中有将结构体地址值强转为函数指针类型的操作((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

2.4 __main_block_impl_0是一个具有同名构造函数的结构体

  1. 该结构体内有2个结构体成员变量(iml、Desc),和一个与结构体同名的构造函数,函数返回值就是该结构体
  2. 所以(block定义简化表达式)void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);中调用函数__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)时,就会生成一个__main_block_impl_0结构体并返回。

2.5 __main_block_func_0__main_block_desc_0_DATA

  1. __main_block_func_0 是 block 中封装的代码块,这里指的就是{NSLog(@"Hello, World!");}
  2. __main_block_desc_0_DATA是一个__main_block_desc_0类型的结构体变量,在声明时即进行赋值操作,用于存放 block 的大小信息

2.6 block 调用简化解读block->FuncPtr(block);

  • 源码((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
  1. 经过上述解析,我们知道 block 此时指向的是一个__main_block_impl_0结构体,结构体中的 impl结构体成员变量内有 Funcptr 成员。
  2. 为什么不是通过接构体一层层调用block->impl->FuncPtr呢?而是直接block->FuncPtr(block);调用?
  3. 通过前面的调用源码((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);可知,里面调用 FuncPtr 前会进行1层转换,即(__block_impl *)block后,block 从类型(void (*)()转为__block_impl类型,但我们知道,block 变量实际类型是__main_block_impl_0,所以从底层上说,block 的类型转换实际上应该是从__main_block_impl_0转到__block_impl
  4. 为甚么可以从__main_block_impl_0转到__block_impl呢?那时应为结构体的变量地址值实际等于该结构体第一个成员变量的地址值,而__main_block_impl_0结构体第一个成员变量就是__block_impl结构体,所以当要调用 FuncPtr 成员变量时,就是从地址值开始寻址,而从__main_block_impl_0开始寻址,和从__block_impl开始寻址,它们的起始地址值就是同一个。
  5. FuncPtr 指向的是函数__main_block_func_0,在调用是需要将 block 作为参数传入。

2.7 综上,block本质上也是一个OC对象

  • block 内部的isa指针继承自 NSObject,即可证明 block 即 OC 对象
  • block 是封装了函数调用以及函数调用环境的OC对象

3 block 的带参数及值捕获-底层代码展示

3.1 带参数 block


相对于无参 block 来说,变化如下

  1. block 的定义中,其类型转为void(*)(int, int)
  2. block 的调用中,FuncPtr 传入的参数除了 block 本身外同样多了两个实参

3.2 auto变量的值捕获-底层代码展示

  1. block 定义时,将 value 变量的传入值__main_block_impl_0结构体构造函数中
  2. __main_block_impl_0结构体多了一个成员变量 value,从其构造函数看,是用于来保存外面传进来的 value 的值的,这种情况就是值捕获
  3. __main_block_impl_0构造函数中的: value(_value)表示,将int _value的值赋值给到结构体的 value 成员变量中去,这是 c++的语法
  4. 在 block 的调用时,int value = __cself->value;是指,从 block 的结构体获取其 value 成员变量的值,赋值给 value 变量。

3.3 static 变量的地址捕获-底层代码展示

  1. static 基本情况与 auto 差不多
  2. static 变量被 block 捕获是通过地址捕获
  3. 为什么 static 变量是地址捕获,而 auto 变量是值捕获呢?因为auto 变量的生命周期只有在变量声明作用域有效,超出作用域时候就会自动销毁,所以为了保证后续block 能够继续使用 auto 变量,则需要将 auto 变量的值捕获

3.4 全局变量没有捕获行为-底层代码展示

  1. 没有进行捕获行为,因为全局变量一直存在内存中,随处可以访问

3.5 block 对 self 的捕获

// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)test;
@end
//Person.m
#import "Person.h"
@implementation Person
- (void)test {
    void (^block)(void) = ^{
        NSLog(@"%@",self);
    };
    
    block();
}
@end

转为 Person.cpp后


  1. OC方法的底层实现都会默认带上两个参数:Person *selfSEL _cmd

  2. 所以 block 中访问的 self 是 test 方法的形参 self 参数,这个 self 是一个局部变量,所以 block 会对 self 进行捕获

  3. 显然,self 不是自动变量(auto),所以它在 block 中的访问方式是通过指针访问

  4. 拓展:无论上述 test 方法内的 block 通过以下哪种方式访问,都是会对 self 进行捕获的

    - (void)test {
    void (^block)(void) = ^{
        NSLog(@"%@",_name); // 等同于 self->_name
    };
    
    block();
    }
    
    - (void)test {
    void (^block)(void) = ^{
        NSLog(@"%@",[self name]); // 等同于 objc_msgSend(self, @selector("name"))
    };
    
    block();
    }
    

3.6 block 对变量的捕获总结

摘自 MJ 底层课课件

4 block的类型

4.1 block 有3中类型

  1. 可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
  1. 通过定义打印 block 类型获取对应的 block 类型


4.2 不同类型的 block 在内存中的存放位置

摘自 MJ 底层课课件

4.3 不同环境(ARC、MRC)下 block 的类型判断

4.3.1 在 MRC 环境下(~将 Xcode 中默认的 ARC 环境改为 MRC~)

摘自 MJ 底层课课件

测试代码如下:


不同类型的 block 进行 copy 操作效果总结


摘自 MJ 底层课课件

4.3.2 在 ARC 环境下

4.3.2.1 在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况

  1. block作为函数返回值时


  2. 将block赋值给__strong指针时


  3. block作为Cocoa API中方法名含有usingBlock的方法参数时


  4. block作为GCD API的方法参数时


4.3.2.2 不同环境下 block 属性的内存语义写法区别

  • MRC下block属性的建议写法
    @property (copy, nonatomic) void (^block)(void);

  • ARC下block属性的建议写法
    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);

统一使用 copy 语义更直接

5 对象类型的 auto 变量

5.1 block对 对象类型的 auto 变量捕获-底层代码展示

5.2 block对 对象类型的 static 变量捕获-底层代码展示

5.3 超出作用域的对象类型的 auto 变量会自动销毁(ARC环境)

5.4 堆 block 会对 对象类型的 auto 变量 进行强引用(ARC环境)


栈 block 不会对 对象类型的 auto 变量 进行强引用

5.5 使用__weak修饰对象类型的 auto 变量

  • 让堆 block 不再强引用对象类型的 auto 变量

6 栈block 和 堆block 对对象类型的 auto 变量强弱引用总结

在使用clang转换OC为C++代码时,可能会遇到以下问题
cannot create __weak reference in file using manual reference
解决:通过添加运行时参数至转码C++命令,获取 ARC 下 block 对对对象类型的 auto 变量强弱引用提示
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

6.1 __strong修饰的`对象类型的 auto 变量

  • 添加强修饰符__strong对象类型的 auto 变量 OC 代码
  • 通过带运行时信息的命令转为 C++后


  1. 注意__main_block_impl_0中捕获的person变量,默认都带上__strong

6.2 __weak修饰的`对象类型的 auto 变量

6.3 stackBlock 和 mallocBlock 的对对象类型的 auto 变量的处理

6.3.1 对比访问基本 auto 变量对象类型的 auto 变量的 block 转 C++后源码区别

重点区别在struct __main_block_desc_0的结构体成员上

  • 访问基本 auto 变量 block 转 C++后源码

  • 访问对象类型的 auto 变量的 block 转 C++后源码

访问对象类型的 auto 变量struct __main_block_desc_0的结构体成员比访问`基本 auto 变量的多了两个成员

void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);

对应实现


它们的作用就是用来处理对象类型的 auto 变量引用计数问题

6.3.2 当block内部访问了对象类型的auto变量时

  1. 如果block是在栈上(即 stackBlock),将不会对auto变量产生强引用

  2. 如果block被拷贝到堆上(即 mallocBlock)

  • 会调用block内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

6.3.3 如果block从堆上移除

  • 会调用block内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放引用的auto变量(相当于release操作)

文/Jacob_LJ(简书作者)
PS:如非特别说明,所有文章均为原创作品,著作权归作者所有,转载需联系作者获得授权,并注明出处,所有打赏均归本人所有!

推荐阅读更多精彩内容