OC-Block

作者: xiaoyouPrince | 来源:发表于2020-07-01 17:22 被阅读0次

    Block

    一个最简单的block,将源码编译后如下

    // 基本使用 block
    void (^block)() = ^{
        NSLog(@"block - 调用");
    };
        
    // 调用
    block();
    

    编译成C++后,其具体定义的类型如下,命令如下: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

    struct __block_impl {
      void *isa;        // 其 isa 并非指向 Class,表明其为匿名对象
      int Flags;        // 系统传值默认为0
      int Reserved;     // 系统传值默认为0
      void *FuncPtr;    // 内部保存一个回调函数地址,可以在外界回调
    };
    
    // block 结构体的
    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;
      }
    };
    
    // block 调用时候的具体函数
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_3l_tslpwwd93px60pg28clgyfvh0000gn_T_main_654937_mii_3);
            }
    
    // block 描述信息 __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)};
    

    重写为 C++ 代码后。block 的定义 & 调用

    void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
    // 移除类型转换如下
    void (*block)() = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
    
    (block->FuncPtr)(block); // 找到block本身保存的 funcPtr 直接调用
    
    15934170754263.jpg

    Block 捕获变量

    为了保证 block 能正常的访问外部的变量,其有一个变量捕获的机制

    15934190192032.jpg

    Block 的类型

    Block 根据其存储域,分为三种类型

    15934223328514.jpg

    没有访问 Auto 变量 -> global 类型 -> 就相当于一个函数,全局只有一份
    访问了 Auto 变量 -> stack 类型 -> 实际就是一个局部变量,出了其变量作用域直接就被销毁了
    stack 类型调用 copy -> malloc 类型

    block 的 copy

    在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,如下
    
    1. 将block作为返回值返回时
    2. 将block赋值给 __strong 指针时候,会自动copy,block的声明周期就是 strong的生命周期
    3. block 作为 Cocoa Api 中方法,方法名中含有 usingBlock 的方法参数时
    4. block 作为 GCD API 的方法参数时
    
    MRC 下block属性的建议写法,直接拷贝到堆
    @property (copy, nonatomtic) void (^block)(void);
    
    ARC 下block属性的建议写法
    @property (strong, nonatomtic) void (^block)(void);
    @property (copy, nonatomtic) void (^block)(void);
    
    • Block 作为函数返回值的时候,会自动进行 copy 操作,复制到堆空间
    • 将 Block 赋值给强指针的时候也会自动进行 copy 操作

    Block 捕获对象类型的 auto 变量

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

    1. 如果block 在栈上,将不会对auto变量产生强引用
    2. 如果block被拷贝到堆上
        1. 会调用block内部的copy函数
        2. copy 函数内部会调用_Block_object_assign函数
        3. _Block_object_assign函数会根据auto变量的修饰符(__strong,__weak,__unsage_unretained)做出响应的操作,类似retain(简单来说常用的 weakself 就是防止强引用)
    
    3. 如果 block 从堆上移除
        1. 会调用block内部的 dispose 函数
        2. dispose 函数内部会调用_Block_object_dispose 函数
        3. _Block_object_dispose 函数会自动释放引用的 auto 变量,类似release 
    

    Block 修改外界局部变量的值

    通常情况下,block 无法修改外界变量的值,原因是 block 只是捕获了 auto 变量,实际在内部函数调用时候是一个新函数,无法访问外部变量。 如果是 static 变量或者全局变量就可以正常修改外部变量值,因为它们的访问方式不一样.

    要实现修改外部的 auto 变量,可以增加 __block 修饰符

    __block int b = 10;
    void (^block)() = ^{
        b = 20;
    };
    

    可以看到,增加 __block 后代码经过重写为 C++

    // block 源码如下:
    
    struct __Block_byref_b_0 {
      void *__isa;
    __Block_byref_b_0 *__forwarding;
     int __flags;
     int __size;
     int b;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_b_0 *b; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_b_0 *_b, int flags=0) : b(_b->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_b_0 *b = __cself->b; // bound by ref
    
                (b->__forwarding->b) = 20;
            }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->b, (void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    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};
    
    // 声明和调用如下:
    __attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 10};
    void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_b_0 *)&b, 570425344));
    
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
    // 移除类型转换,简化如下:
    __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 10};
    block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &b, 570425344));
    (block->FuncPtr)(block);
    
    

    可以发现block结构体 __main_block_impl_0 内多了一个__Block_byref_b_0 *b; 指针,外界变量 b 就相当于被封装成了一个对象,其指针被block引用,真正修改值操作是在该 b 对象内部修改的。

    对象类型的 auto 变量,__block 变量

    1. 当block 在栈上时候,对它们都不会产生强引用
    2. 当block 被拷贝到堆上时候,会通过 copy 函数来处理它们,计数加1
    3. 当block 被移除的时候,会通过 dispose 函数来移除它们,计数减1
    

    被 __block 修饰的对象类型

    1. 当__block 变量在栈上时候,不会对指向的对象产生强引用
    2. 当__block 变量被 copy 到堆上时候
        1. 会调用 __block 变量内部的 copy 函数
        2. copy 函数内部会调用 _Block_object_assign 函数
        3. _Block_object_assign 函数会根据指向类型的修饰符(__strong,__weak,__unsage_unretained)做响应的持有操作,// 这里只有 ARC 时候会retain持有该对象,MRC时候不会持有该对象
    3. 如果__block 变量从堆上移除
        1. 会调用 __block 变量内部的 dispose 函数
        2. dispose 函数内部会调用 _Block_object_dispose 函数
        3. _Block_object_dispose 函数会自动释放指向的对象
    

    可以看出:Block 捕获变量的时候无论是否是 __block 变量其操作逻辑一样
    __block 变量的作用为修改 Block持有其捕获变量的结构,如图

    15935849845676.jpg

    __block 变量的__forwording指针

    Block 本身是可能存在堆和栈上的。 __block 变量也会因为Block从栈拷贝到堆上而赋值到堆上,所以它本身也是可能存在栈上和堆上的。

    __block int b = 10;
    void (^block)() = ^{
        b++; // 变量b为复制到堆上__block变量结构体的实例
    };
    b++;    // 变量b为复制到堆上之前,栈上__block 变量结构体的示例
    
    // 以上两个地方的使用都可以转化为
    (b.__forwording->b)++; // 通过__forwording指向无论是栈/堆上的结构体实例。
    

    如图:

    831593572769_.pic.jpg

    Block 循环引用处理

    由 Block 捕获外部 auto 变量的原理来看,如果 auto 变量持有 Block 且被 Block 捕获就会发生循环引用。处理方式如下:

    ARC下:
    
    1. __weak 修饰符修饰 auto 变量
    2. __unsafe_unretained 修饰符修饰 auto 变量(其为弱引用,不安全,不会在变量被销毁时候被设置为nil)
    3. __block 修饰,这样会修改 Block 持有该对象的数据结构,但是必须调用Block,并且在函数调用内部给变量手动置空。(不安全,容易发生泄漏)
    
    MRC 下:
    
    1. __block 修饰,在MRC下Block不会retain被__block修饰的对象
    2. __unsafe_unretained 修饰符修饰
    

    面试题------------

    Block 原理是怎么样的,本质是什么?

    Block 本质是一个封装了函数调用以及调用环境的 OC 对象,
    原理如上面....
    

    __block 的作用是什么,有什么注意点

    作用:是的Block可以修改其捕获的外部变量
    注意点: 内存管理,注意循环引用。。。在MRC下不会retain被捕获的对象,这点和ARC不同
    

    Block 修饰词为什么是Copy,使用Block有哪些需要注意?

    block 如果不 copy,其作用域在栈(数据区)上,为了给Block保活,在使用的时候不会被释放掉。
    
    注意:内存管理,不要发生循环引用
    

    block 在修改 NSMutableArray时候,是否需要加 __block 修饰符

    如果是修改其实例指针的值,需要。如: array = nil;
    
    如果是修改实例的内容,如添加新元素,不需要。如:[array addObject:obj];
    

    相关文章

      网友评论

        本文标题:OC-Block

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