iOS - block

作者: valentizx | 来源:发表于2019-04-16 00:18 被阅读51次
    image

    三年前,第一次写关于 block 的东西,就是初识 block,了解了些皮毛,但发现,那么仅仅是 block 的冰山一角,关于 block 还有很多需要参透和理解。

    block 本质

    block 的本质是一个 Objective-C 对象,其内部也有 isa 指针,block 中封装了函数的调用以及函数调用环境的 Objective-C 对象。它的结构如下:

    image
    1. 函数的调用相当于函数的调用地址
    2. 函数调用环境指参数,访问 block 外部的值等

    一段下面的 block:

    void(^block)(int a, int b) = ^(int a, int b) {
                NSLog(@"a + b = %d", a + b);
    };
    block(1, 2);
    

    用命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m(下同) 重写后 C++ 代码是这样的:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
    };
    

    __block_impl 的声明:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    

    __main_block_desc_0 的声明:

    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    };
    

    Block_size 表示 __main_block_impl_0 能占多少内存。

    假如 block 内使用了外部变量,如:

    int outter = 35;
    void(^block)(int a, int b) = ^(int a, int b) {
        NSLog(@"outter is %d", outter);
        NSLog(@"a + b = %d", a + b);
    };
    block(1, 2);
    

    本质结构为:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int outter;
    };
    

    若我们在 .m 文件中自行实现这些结构体:
    [图片上传失败...(image-e5da00-1555345050037)]
    然后进行转换:

    struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;
    

    加断点运行后进入 LLDB 调试环境可看到 blockStruct 的信息:


    image

    发现 outter 已经封装到 blockStruct 的内存中去了。

    我们记录下 __FuncPtr 后面的内存地址 0x0000000100000ee0,然后在 block 块内增加断点并过掉当前断点,当程序停留在 block 块内的断点的时候,然后 Debug -> Debug Workflow -> Always Show Disassembly 会看到如下界面:
    [图片上传失败...(image-fe45c2-1555345050037)]
    第一行 0x100000ee0 <+0>: pushq %rbp 的地址就是__FuncPtr 的地址,这说明 block 块内的代码都封装到了函数里面,这个函数的首地址(例子中的 0x0000000100000ee0)在 block 结构体的成员结构体 __block_impl 中。

    深入探究

    底层数据结构

    main 函数中例子的代码 C++ 的实现为:

    int outter = 35;
    // 定义 block 变量
    void(*block)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, outter));
    // 执行 block 内部代码
    ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
    

    去除强制转换的干扰代码,简化后:

    int outter = 35;
    // 定义 block 变量
    void(*block)(int a, int b) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, outter));
    // 执行 block 内部代码
    ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
    

    这里的 block 会指向什么?首先得明白 _main_block_impl_0() 会返回什么?我们在 .cpp 文件中发现该函数在 __main_block_impl_0 的结构体中:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int outter;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _outter, int flags=0) : outter(_outter) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    该函数接收 4 个参数,flags 默认为 0,并且函数名和结构体名相同,是 C++ 中的构造函数,和 Java 的构造函数道理类似,也和 Objective-C 中的 init 方法类似,并且无任何返回。

    outter(_outter) 表示传进来的 _outter 的值会赋给结构体成员变量 outter,相当于 outter = _outter

    4 个传入的参数中 outter 不必多言,那么来 __main_block_func_0 是什么:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
      int outter = __cself->outter;
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_f61ac8_mi_0, outter);
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_f61ac8_mi_1, a + b);
    }
    

    可见,__main_block_func_0 封装了 block 执行逻辑的函数。__main_block_func_0 对应 __main_block_impl_0 构造方法中的 void *fp, fp 赋值给了 impl.FuncPtr。这样 impl.FuncPtr 存储的就是执行逻辑的函数的地址。

    在该构造方法中同时初始化了 isa 指针:

    impl.isa = &_NSConcreteStackBlock;
    

    说明 block 的类型为 _NSConcreteStackBlock

    回过头我们再看传入的第二个参数 &__main_block_desc_0_DATA,有关它的完整代码为:

    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)};
    

    该处 0 赋值给了 reservedsizeof(struct __main_block_impl_0) 计算该 block 结构体的大小并将结果赋值给 Block_size

    &__main_block_desc_0_DATA 对应 __main_block_impl_0 构造方法中的 struct __main_block_desc_0 *desc, 并赋值给了 Desc。换而言之 __main_block_impl_0 中的 Desc 指向的是 __main_block_desc_0 结构体变量。

    所以在执行结构体的构造函数的时候,outter 为 35。倘若在外部将 outter 重新赋值,结构体中的 outter 是不会更改的。也就是说 outter 是以值传递的形式传递的。

    __main_block_func_0 中:

    int outter = __cself->outter; 
    

    该步骤为取出 outter 的值(35)。

    block 的变量捕获

    为确保 block 能正确访问外部变量,block 有变量捕获机制,如下图:


    auto: 局部变量默认是 auto 修饰的:int a = 0; 等价于 auto int a = 0;,它表示自动变量,离开作用域后自动销毁。

    那么 block 中的捕获是什么意思?就是 block 内部会新增一个成员变量用来存储外部变量的值,这个过程为捕获。

    auto 修饰的变量

    上一节例子中的 int 型 outter 就是自动变量,默认 auto 修饰。其访问方式是值传递

    static 修饰的变量

    我们添加静态变量 outter2:

    int outter = 35;
    static int outter2 = 1210;
    void(^block)(int a, int b) = ^(int a, int b) {
                NSLog(@"outter is %d", outter);
                NSLog(@"outter2 is %d", outter2);
                NSLog(@"a + b = %d", a + b);
    };
    block(10, 20);
    

    运行后打印了 outter 和 outter2 的值。说明无论是 auto 修饰还是 static 修饰的外部变量,block 内部都是能捕获到的。
    那么内部访问方式是否一样?重写 C++ 代码后发现:

    void(*block)(int a, int b) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, outter, &outter2));
    

    outter2 是以 &outter2 传入 __main_block_impl_0 结构体的构造方法的,并且 __main_block_impl_0 中的 outter2 是:

    struct __main_block_impl_0 {
      ...
      int outter;
      int *outter2;
      ...
    };
    

    发现这里的 outter2 是通过传址的方式传进去的,在打印的 C++ 实现中 outter2 是这样取值的:

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_e5faae_mi_1, (*outter2));
    

    *outter2 这样的取值方式是直接取出外面静态变量内存里的值。

    Q:为什么会有这样的差异?
    A:因为 auto 修饰的变量是可能自动销毁的,而 block 执行的时机未定,所以存在 block 执行内部代码的时候变量已经销毁的情况,这会导致程序的 Crash,所以外部变量需进行值传递。而 static 修饰的变量会一直存在于内存当中,不存在 block 执行的时候变量已经销毁的情况。

    全局变量

    我们验证全局变量的捕获机制,添加一个全局的成员变量 outter3:

    int static outter3 = 1314;
    

    并在内部打印,发现打印 1314,若在执行 block 之前修改 outter3 的值:

    void(^block)(int a, int b) = ^(int a, int b) {
        NSLog(@"outter is %d", outter);
        NSLog(@"outter2 is %d", outter2);
        NSLog(@"outter3 is %d", outter3);
        NSLog(@"a + b = %d", a + b);
    };
    outter3 = 999;
    block(10, 20);
    

    打印得 outter3 = 999,看似和局部静态变量的道理一样,我们看下 C++ 实现得 __main_block_impl_0 结构体:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int outter;
      int *outter2;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _outter, int *_outter2, int flags=0) : outter(_outter), outter2(_outter2) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    发现并无 outter3,在 block 内部打印的地方为:

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_33_1p51hcyn1738b6zr050n01qh0000gn_T_main_147262_mi_2, outter3);
    

    也就是说,全局变量并没有捕获到 block 内部,而且,内部访问全局变量是直接访问的。

    那么假如 block 中是如何捕获 self 的呢?
    我们新建 Test 类:
    .h:

    @interface Test : NSObject
    
    @property(nonatomic, copy) NSString* param;
    
    - (void)test;
    - (instancetype)initWithParam:(NSString*)param;
    
    @end
    

    .m:

    @implementation Test
    
    - (void)test {
        void(^block)(void) = ^{
            NSLog(@"====>%p", self);
        };
        block();
    }
    
    - (instancetype)initWithParam:(NSString*)param
    {
        self = [super init];
        if (self) {
            self.param = param;
        }
        return self;
    }
    
    @end
    

    外部调用 test() 方法便可执行 block,并访问内部的 self。打印:

    ====>0x10070be20
    

    重写 Test.m 文件后发现其 block 结构为:

    struct __Test__test_block_impl_0 {
      struct __block_impl impl;
      struct __Test__test_block_desc_0* Desc;
      Test *self;
      __Test__test_block_impl_0(void *fp, struct __Test__test_block_desc_0 *desc, Test *_self, int flags=0) : self(_self) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    发现 self 是通过指针传递进来的。而且可推导,既然能捕获,说明 self 是局部变量。
    我们可看到 test() 函数的底层为:

    static void _I_Test_test(Test * self, SEL _cmd) {
        void(*block)(void) = ((void (*)())&__Test__test_block_impl_0((void *)__Test__test_block_func_0, &__Test__test_block_desc_0_DATA, self, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    

    发现底层函数默认添加了两个参数:self_cmd,这也是我们为什么能在函数内可以调用到 self 和 _cmd 的原因。
    若假如打印 _param 呢?运行

    Test* t = [[Test alloc] initWithParam:@"something"];[t test];
    

    发现可打印:

    something
    

    此时是捕获的 _param?错,_param 等价于 self->_param,所以捕获的还是 self。

    block 的类型

    block 有三种类型,亦是可以通过 class 方法或者查看 isa 指针查看其具体类型,但最终都是继承自 NSBlock

    类型
    _NSGlobalBlock_ 全局 block
    _NSStackBlock_ 栈区 block
    _NSMallocBlock_ 堆区 block

    我们为探究其类型,运行:

    void(^block)(void) = ^{
         NSLog(@"This is a block");
    };
    block();
    NSLog(@"%@", [block class]);
    

    打印:

    This is a block
    __NSGlobalBlock__
    

    追加打印:

    NSLog(@"%@", [[block class] superclass]);
    NSLog(@"%@", [[[block class] superclass] superclass]);
    NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
    

    得:

    __NSGlobalBlock
    NSBlock
    NSObject
    

    现在得到继承链:


    那么现在打印:

    void(^block)(void) = ^{
                NSLog(@"This is a block");
            };
            
    int num = 10;
    void(^block1)(void) = ^{
        NSLog(@"The num is %d", num);
    };
            
    NSLog(@"%@ %@ %@", [block class], [block1 class], [^{
        NSLog(@"The num is %d", num);
    } class]);
    

    得:

    __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
    

    重写后我们可看见有三个 block:__main_block_impl_0、__main_block_impl_1、__main_block_impl_2。
    但奇怪的是三者得 isa 指针都指向的是 _NSConcreteStackBlock

    为什么会出现这样的问题,是因为在重写命令中通过 clang 转成的 C++ 代码并不能完全代表 Objective-C 最终的底层实现。

    所以我们还是按照打印的标准也判断 block 的类型,可发现, block 的存储类型和捕获外部的局部变量也有关系。

    image

    text 区存放的是程序代码,data 区存放的是全局变量,堆区放的是 alloc 出来的对象,动态分配内存,需要开发者手动调用,也需要开发者主动管理内存(现在有 ARC 了),栈区放的是局部变量,系统自动销毁内存。

    具体的 block 类型是区分的?如下表:

    block 类型 区别
    _NSGlobalBlock_ 没有访问 auto 变量
    _NSStackBlock_ 访问 auto 变量
    _NSMallocBlock_ _NSStackBlock_ 调用了 copy

    对于 NSStackBlock 的 block 存在一个问题,代码如下:

    void(^block)(void);
    
    void test() {
        int num = 35;
        block = ^{
            NSLog(@"The num is %d", num);
        };
        NSLog(@"%@", [block class]);
    }
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            test();
            block();
        }
    }
    

    运行结果为:

    __NSStackBlock__
    The num is -272632472
    

    做这个实验请将内存管理改为手动(MRC):Build Setting -> Objective-C Automatic Reference Counting -> No

    为何会出现 -272632472?是因为,执行过 test() 后栈区的 对应数据被回收,存在的可能就是垃圾数据,那么再访问结构体内的成员的时候得到的就是这些垃圾数字。

    将上述代码稍作改动,test 内的 block 改为:

    block = [^{
        NSLog(@"The num is %d", num);
    } copy];
    

    打印结果为:

    __NSMallocBlock__
    The num is 35
    

    此时的 block 已经进行了 copy 操作,栈 block 变为堆 block,内存需要我们手动释放,而我并没有释放,所以打印的 num 是正确的。

    产生疑惑,_NSGlobalBlock_ 类型的栈进行了 copy 操作会变成 _NSMallocBlock_ 类型吗?
    去掉 block 内部对 num 的打印再来运行发现:

    __NSGlobalBlock__
    

    即使使用了 copy 操作,block 依然为 _NSGlobalBlock_ 类型。

    copy 操作

    由上节可知,对于 _NSStackBlock_ 类型的 block 有太多的不确定性,所以在对这种 block 使用的时候需要对其进行一次 copy 操作将栈 block 复制到堆区。

    但上节的例子是基于 MRC 的环境下操作的,在 ARC 的环境下,编译器会根据情况自动讲 block 进行 copy 操作。
    在 ARC 环境下执行:

    void(^block)(void);
    
    void test() {
        int num = 35;
        block = ^{
            NSLog(@"The num is %d", num);
        };
        NSLog(@"%@", [block class]);
    }
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            test();
            block();
        }
    }
    

    得:

    __NSMallocBlock__
    The num is 35
    

    在以下条件下,编译器会自动将 block 进行 copy 操作:

    • block 作为返回值
    • 将 block 复制给 __strong 指针时
    • block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
      如:
    NSArray* arr = ...;
    [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    }];
    

    对象类型的 auto 变量捕获

    前面的例子内部捕获外部变量都是基本类型,如 int,那么对象类型的外部变量是如何捕获的?
    将 Test 类,添加 NSInteger 类型的属性 num。
    外部:

    Test* test = [[Test alloc] init];
    test.num = 35;
            
    TestBlock block = ^{
        NSLog(@"The num is %ld", (long)test.num);
    };
    block();
    

    TestBlock 定义为:typedef void(^TestBlock) (void);
    运行得:

    The num is 35
    

    我们稍作改动:

    TestBlock block;
    {
        Test* test = [[Test alloc] init];
        test.num = 35;
                
        block = ^{
             NSLog(@"The num is %ld", (long)test.num);
        };
    }
    NSLog(@"=====end=====");
    

    也重写了 Test 的 dealloc() 方法打印 dealloc。我们增加断点在打印 “end” 的一行,运行发现断点处,并没有打印 Test 的 delloc 信息,也就是说,内部 {} 执行完了 Test 也没有立即被销毁。
    我们将代码改成:

    Test* test = [[Test alloc] init];
    test.num = 35;
            
    TestBlock block = ^{
        NSLog(@"The num is %ld", (long)test.num);
    };
     NSLog(@"=====end=====");
    

    重写后发现 block 的结构体中有 Test *test 成员变量。回到修改之前的代码,在执行:

    block = ^{
        NSLog(@"The num is %ld", (long)test.num);
    };
    

    的时候,block 进行了 copy 操作成为堆区的 block,不会轻易销毁,那么意味着对 test 也是强引用持有,test 亦不会轻易被释放,所以 dealloc 信息延后打印:

    =====end=====
    =====dealloc=====
    

    若是 MRC 环境(需添加 [t release] 操作,并且 dealloc 方法内须调用父类的 dealloc 方法),即使 block 还在,也会先执行 Test 的 dealloc 方法。结果为:

    =====dealloc=====
    =====end=====
    

    若在 MRC 环境下改为:

    block = [^{
        NSLog(@"The num is %ld", (long)test.num);
    } copy];
    

    则会达到 ARC 下同样的效果,因为进行了 copy 操作后在 block 内部相当于调用了一次 [t reatain] 操作。结果为:

    =====end=====
    =====dealloc=====
    

    回到 ARC 环境,假如 Test 对象进行 __weak 修饰,则情况又有所不同:

    =====dealloc=====
    =====end=====
    

    在用 __weak 修饰的情况下重写 C++ 代码会报错:

    cannot create __weak reference because the current deployment target does not support weak references
    

    是因为命令需要支持 ARC 并且指定运行时系统版本,如:

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m
    

    重写成功后发现 block 结构体为:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      Test *__weak test;
      ...
    };
    

    test 对象为 weak 修饰,所以在离开作用域后立即释放。去掉 weak 后的结构体再用上命令重写,得到:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      Test *__strong test;
      ...
    };
    

    发现 weak 默认用了 strong 修饰,所以“延长了”其寿命。
    最后来个总结:

    • 当 block 在栈上,不会对 auto 变量产生强引用
    • 当 block 在堆上,会根据 auto 是否由 __strong 或者 —__weak 修饰来决定是否产生强引用 [下有说明]
    • 当 block 从堆上移除,将放弃对 auto 变量的引用,相当于进行了一次 release 操作

    copy 操作后的 block 其 Desc 是有变化的:

    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};
    

    原本只有 reservedBlock_size 现在又多了两个函数指针: copydisposecopy 保存的是 __main_block_copy_0,dispose 保存的是 __main_block_dispose_0
    当 block 执行了 copy 操作后,这两个函数便会执行。
    __main_block_copy_0 和 __main_block_dispose_0是现实:

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    // 会根据 test 对象是 strong 还是 weak 修饰来决定是否对 test 对象产生强引用
    _Block_object_assign((void*)&dst->test, (void*)src->test, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    // 对 test 对象进行释放
    _Block_object_dispose((void*)src->test, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    
    函数 调用时机
    copy 栈上的 block 复制到堆时
    dispose 堆上的 block 被收回时

    __block

    我们再来新建一个例子工程:

    typedef void(^TestBlock) (void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            int num = 10;
            TestBlock block = ^{
                NSLog(@"The num is %d", num);
            };
            block();
        }
        return 0;
    }
    

    运行上面这段代码,结果为:

    The num is 10
    

    那么实际情况中,我们常常需要在 block 内部改变外面变量的值,在 block 内部直接修改是不允许的:

    ^{
        num = 35; // ✘
    }
    

    这是因为 num 的作用域属于 main 函数,而 block 内执行逻辑属于另一个函数 __main_block_func_0,是无法跨域进行修改的。

    但是通过 static 修饰的局部变量是可以用这种方式修改的:

    static int num = 10;
    TestBlock block = ^{
        num = 35;
        NSLog(@"The num is %d", num);
    };
    block();
    

    结果为:

    The num is 35
    

    因为 static 修饰的是引用传递,block 的结构体存储的是指向 num 的指针,所以在内部修改 num 的值是可以成功的。

    那么如何修改非 static 修饰的的局部变量?就是 __block 关键字。

    __block int num = 10;
    TestBlock block = ^{
        num = 35;
        NSLog(@"The num is %d", num);
    };
    block();
    

    结果:

    The num is 35
    

    __block 本质

    __block 变量不能修饰全局变量、静态变量。并且编译器会将 __block 变量包装成一个对象。
    重写 C++ 代码后发现 block 结构体 num 的成员变量和之前未用 __block 修饰的 num 有本质的区别:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_num_0 *num; // by ref
      ...
    };
    

    这里的 num 为 __Block_byref_num_0 * 类型。__Block_byref_num_0 也是个结构体,其内部定义是这样的:

    struct __Block_byref_num_0 {
      void *__isa;
    __Block_byref_num_0 *__forwarding;
     int __flags;
     int __size;
     int num;
    };
    

    我们可推断一开始 num 的值为 10,这个值一定是存储在 __Block_byref_num_0 的成员变量 num 中。那么 __forwarding 表示什么?
    首先我们看到由 __block 修饰后的 num,在 main 函数的源码中变成了:

    __attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};
    

    简化版本:

    __Block_byref_num_0 num = {(0,
                                &num,
                                0,
                                sizeof(__Block_byref_num_0),
                                10};
    

    此时第一个 0 赋值给 __isa,第二个 0 赋值给 __flags,第四个参数是计算当前结构体有多大并赋值给 __size,最后 10 赋值给 num,推断得到验证。第二个参数 &num 就是 num 结构体本身,也就是说它将自身的结构体地址传递给了 __forwarding。换而言之 __forwarding 指向的是自己。

    image

    同时 &num 也传给了 __main_block_impl_0 的 *num

    TestBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
    

    block 修改 num 的源码为:

    // 首先拿到 __Block_byref_num_0 中的 __forwarding
    __Block_byref_num_0 *num = __cself->num;
    // 取得 num 再修改
    (num->__forwarding->num) = 35;
    

    倘若多加了一个对象类型的局部变量:

    __block int num = 10;
    __block NSObject* obj = [[NSObject alloc] init];
    TestBlock block = ^{
        obj = nil;
        num = 35;
        NSLog(@"The num is %d", num);
    };
    block();
    

    num 和 obj 在底层会生成两个机构体:

    struct __Block_byref_num_0 {
      void *__isa;
    __Block_byref_num_0 *__forwarding;
     int __flags;
     int __size;
     int num;
    };
    
    struct __Block_byref_obj_1 {
      void *__isa;
    __Block_byref_obj_1 *__forwarding;
     int __flags;
     int __size;
     // copy 操作
     void (*__Block_byref_id_object_copy)(void*, void*);
     // dispose 操作
     void (*__Block_byref_id_object_dispose)(void*);
     NSObject *__strong obj;
    };
    

    block 结构体会有两个成员变量指向它们在这里不贴出。

    我们去掉对象类型的 obj 回到最简状态,在 block() 后打印 num 的内存地址,得:

    0x10051e968
    

    这个内存地址和底层的谁有对应关系?是 __main_block_impl_0 中的 *num?还是 __Block_byref_num_0 中的 num?我们自己实现这些低层结构:


    然后运行:

    __block int num = 10;
    TestBlock block = ^{
               
        num = 35;
        NSLog(@"The num is %d", num);
    };
    struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;
    NSLog(@"%p", &num);
    

    在最后一行加断点发现 __Block_byref_num_0 * 型 num 的地址为:0x000000010204b490,打印局部变量的 num 为 0x10204b4a8,两者并不相同。
    0x000000010204b490 为 __Block_byref_num_0 * 型 num 的地址也就意味着是 __isa 的地址,那么 age 的地址是什么?
    __isa 大小为 8,__forwarding 大小为 8(地址为 0x000000010204b498),__flags 大小为 4(地址为0x000000010204b4a0), __size 大小为 4(地址为0x000000010204b4a4),num 的地址为 0x000000010204b4a8。是不是很眼熟?没错 num 的地址和外部变量的 num 一样。
    通过:

    print/x &(blockStruct->num->num)
    

    命令得到的打印结果和 NSLog(@"%p", &num); 得到的结果也是一样的也可以验证。

    __block 内存管理

    我们来看这个熟悉的例子:

    int num = 0;
    TestBlock block = ^{
         NSLog(@"%d", num);
    };
    block();
    

    底层的 __main_block_desc_0 是没有 copydispose 两个成员函数的,但是当 num 用 __block 的时候就多了这两个函数,并在 copy 函数中调用 _Block_object_assign() 对 结构体中的 __Block_byref_num_0 *num 进行内存管理。
    假如有 Block 0 和 Block 1 分别对 __block 变量引用,则:

    在 ARC 环境下首先 Block 0 会 copy 到堆上,然后 __block 修饰的变量也同样会 copy 到堆上,然后进行强引用。
    然后 Block 1 也会 copy 到堆上并对 __block 变量有强引用:


    image

    当 block 从堆上移除的时候,首先会调用内部 dispose 函数,其内部会调用 _Block_object_dispose() 函数,然后释放 __block 变量:

    image

    若外部是:

    __block int num = 0;
    __block  NSObject* obj == ...;
    TestBlock block = ^{
         ...
    };
    block();
    

    则底层对 int 和 obj 都会产生强引用。

    _Block_byref名字_0 就是强引用

    若:

     __block int num = 0;
    NSObject* obj = [[NSObject alloc] init];
    __weak NSObject* weakObj = obj;
    TestBlock block = ^{
         ...
    };
    block();
    

    则底层不会对 weakObj 产生强引用。

    另,我们在 C++ 代码中看到:

    __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    

    8 表示 __block 修饰的变量,对应注释:BLOCK_FIELD_IS_BYREF
    3 表示对象,对应注释:BLOCK_FIELD_IS_OBJECT

    __block 的 __forwarding 指针

    当 block 在栈上时,__forwarding 指针指向自己。那么堆上的 __forwarding 指向谁呢?答案也是自己,但是需要注意的是,经过 copy 操作后,原栈上的 __forwarding 指针指向堆上的 block,即:

    image

    循环引用问题

    当对象对 block 本身有强引用,而 block 又对对象持有,则会引发循环引用。如:

    Test* t = [[Test alloc] init];
    t.num = 35;
    t.block = ^{
        NSLog(@"%ld", t.num);
    };
    

    ARC

    使用 __weak 和 __unsafe_unretained 解决

    在 ARC 环境下可通过,__weak__unsafe_unretained 解决:

    Test* t = [[Test alloc] init];
    t.num = 35;
    __weak Test* weakT = t;
    t.block = ^{
        NSLog(@"%ld", weakT.num);
    };
    

    或者:

    Test* t = [[Test alloc] init];
    t.num = 35;
    __weak typeof(t) weakT = t;
    t.block = ^{
        NSLog(@"%ld", weakT.num);
    };
    

    对于 self 的情况也是同理:

    __weak typeof(self) weakSelf = self;
    
    image

    __unsafe_unretained 同理,但 __unsafe_unretained 是不安全的,若 __weak 指向的对象销毁,则 weakXXX 会自动置为 nil但 __unsafe_unretained 不会,它还是会指向那个销毁对象的地址,所以进行访问 weakXXX 的时候很有可能产生野指针错误。

    使用 __block 解决

    __block 情况下的循环应用如下:

    image
    必须调用 block 的情况下还可以使用 __block 来解决。
    __block id weakSelf = self;
    

    并且 block 内部的 weakSelf 要职位 nil:

    xxx.block = ^{
        ...
        weakSelf = nil;
    };
    

    因为一旦 weakSelf 置为 nil,三者互相“僵持不下”的状态就会打破,也就不存在循环引用的问题了。


    image

    MRC

    使用 __unsafe_unretained 解决

    同 ARC 环境的方式一样。

    MRC 下不支持 __weak。

    使用 __block 解决

    在 MRC 环境下使用 __block 修饰的话在底层是不会对外部变量进行 retain 也就是强引用操作的,而 ARC 会。
    并且不需要调用 weakSelf = nil 就可以解决循环引用的问题。

    image

    相关文章

      网友评论

        本文标题:iOS - block

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