iOS-Block的实现

作者: FlyElephant | 来源:发表于2019-04-16 09:59 被阅读6次

    Block是C语言的扩充功能,是带有自动变量的匿名函数。block 将同一逻辑的代码放在一个块,使代码更简洁紧凑,易于阅读,比函数使用更方便,代码更美观,开发中受到广泛的使用。

    block 的底层实现

    将main.m中的代码通过clang编译成main.cpp代码:

    int main(int argc, const char * argv[]) {
        // insert code here...
        void (^blk)(void) = ^{ printf("FlyElephant---Block\n"); };
        blk();
        return 0;
    }
    

    main.cpp代码:

    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;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
     printf("FlyElephant---Block\n"); }
    
    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[]) {
    
        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;
    }
    

    block通过__main_block_impl_0初始化,后序工作通过 __block_impl ,__main_block_desc_0实现。

    __block_impl

    __block_impl结构体代码如下:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    • isa 指向实例对象,block 本身也是一个 Objective-C 对象。block 的三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,即当代码执行时,isa 有三种取值 :
    impl.isa = &_NSConcreteStackBlock; 
    impl.isa = &_NSConcreteMallocBlock; 
    impl.isa = &_NSConcreteGlobalBlock;
    
    • Flags 按位承载 block 的附加信息;
    • Reserved 保留变量;
    • FuncPtr 函数指针,指向 Block 要执行的函数
      以上的四个字段均在初始化的时候完成:
      __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;
      }
    

    其中FuncPtr指针指向的block函数:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
     printf("FlyElephant---Block\n"); }
    

    __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;
      }
    };
    
    • impl block 实现的结构体变量;
    • Desc 描述 block 的结构体变量;
    • __main_block_impl_0 结构体的构造函数,初始化结构体变量 impl、Desc;

    __main_block_desc_0

    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)};
    
    • reserved 结构体信息保留字段
    • Block_size 结构体大小

    整个实现过程就是初始化__main_block_impl_0,返回impl,执行impl->FuncPtr.

    block 捕获外部变量

    int main(int argc, const char * argv[]) {
        // insert code here...
        int localValue = 1;
        void (^blk)(void) = ^{ printf("FlyElephant = %d\n", localValue); };
        blk();
        return 0;
    }
    

    block捕获外部变量编译之后的cpp代码如下:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int localValue;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localValue, int flags=0) : localValue(_localValue) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int localValue = __cself->localValue; // bound by copy
     printf("FlyElephant = %d\n", localValue); }
    
    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[]) {
    
        int localValue = 1;
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localValue));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        return 0;
    }
    

    __block_impl定义不变:

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

    block通过参数传递获取到了localValue,保存到结构体中的同名变量中。赋值的时候通过__cself来赋值:

     int localValue = __cself->localValue; // bound by copy
    

    不过暂时还不能还不能修改值,如果修改localValue会报错:

    Variable is not assignable (missing __block type specifier)
    

    内存区域

    讨论block的存储区域我们先了解一下C/C++编译的程序在内存中的分布情况 :

    1.栈区(stack)— 程序运行时由编译器自动分配,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。程序结束时由编译器自动释放。

    2.堆区(heap) — 在内存开辟另一块存储区域。一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是不一样,分配方式类似于链表。用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。

    3.全局区(静态区)(static)—编译器编译时即分配内存。全局变量和静态变量的存储是放在一块的。对于C语言初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。而C++则没有这个区别 - 程序结束后由系统释放。

    4.文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放

    5.程序代码区—存放函数体的二进制代码。

    block访问变量有两种方式一种是静态变量,全局变量和__block形式。

    静态变量,全局变量

    block使用静态变量,全局变量,全局静态变量代码:

    int global_val = 1;
    static int static_global_val = 2;
    int main(int argc, const char * argv[]) {
        // insert code here...
        static int static_val = 3;
        void(^blk)(void) = ^ {
            global_val = 2;
            static_global_val = 3;
            static_val = 4;
        };
        return 0;
    }
    

    编译之后的代码:

    int global_val = 1;
    static int static_global_val = 2;
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *static_val;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *static_val = __cself->static_val; // bound by copy
    
            global_val = 2;
            static_global_val = 3;
            (*static_val) = 4;
        }
    

    对静态全局变量和全局变量访问和转换之前一样,对静态变量的方式是将指针保存了起来。block对于其自动变量而言没有将指针保存起来,是因为自动变量超出其作用域之后就会被废弃。

    __block

    通过__block看下代码:

        __block int localValue = 0;
        void (^blk)(void) = ^{
            localValue = 1;
        };
    

    编译之后代码:

    struct __Block_byref_localValue_0 {
      void *__isa;
    __Block_byref_localValue_0 *__forwarding;
     int __flags;
     int __size;
     int localValue;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_localValue_0 *localValue; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_localValue_0 *_localValue, int flags=0) : localValue(_localValue->__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_localValue_0 *localValue = __cself->localValue; // bound by ref
    
            (localValue->__forwarding->localValue) = 1;
        }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->localValue, (void*)src->localValue, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->localValue, 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};
    int main(int argc, const char * argv[]) {
    
        __attribute__((__blocks__(byref))) __Block_byref_localValue_0 localValue = {(void*)0,(__Block_byref_localValue_0 *)&localValue, 0, sizeof(__Block_byref_localValue_0), 0};
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_localValue_0 *)&localValue, 570425344));
        return 0;
    }
    

    相对之前的代码多了不少代码,主要新增代码:

    • __Block_byref_localValue_0 结构体:用于封装 __block 修饰的外部变量。
    • _Block_object_assign 函数:当 block 从栈拷贝到堆时,调用此函数。
    • _Block_object_dispose 函数:当 block 从堆内存释放时,调用此函数。

    OC源码中的 __block localValue 翻译后变成了 __Block_byref_intValue_0 结构体指针变量 intValue,通过指针传递到 block 内,与静态变量的指针传递是一致的。__Block_byref_intValue_0 这个结构体的字段需要注意:

    struct __Block_byref_localValue_0 {
      void *__isa;
    __Block_byref_localValue_0 *__forwarding;
     int __flags;
     int __size;
     int localValue;
    };
    
    __block变量结构体.png

    结构体里面还多了个 __forwarding 指向自己的指针变量,这与block的类型有关系。

    block类型

    block 有三种类型 NSConcreteGlobalBlock,NSConcreteStackBlock和NSConcreteMallocBlock。

    NSConcreteGlobalBlock

    _NSConcreteGlobalBlock 类型的 block 处于内存的 ROData 段,不捕获局部变量,运行不依赖上下文,内存管理比较简单。

    *block 字面量写在全局作用域时,编译之后也是_NSConcreteGlobalBlock类型。

    void (^blk)(void) = ^{ printf("Global Block\n"); };
    int main(int argc, const char * argv[]) {
        blk();
        return 0;
    }
    
    struct __blk_block_impl_0 {
      struct __block_impl impl;
      struct __blk_block_desc_0* Desc;
      __blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteGlobalBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __blk_block_func_0(struct __blk_block_impl_0 *__cself) {
     printf("Global Block\n"); }
    

    NSConcreteStackBlock

    NSConcreteStackBlock 类型的 block 处于内存的栈区。global block 由于处在 data 段,可以通过指针安全访问,但 stack block 处在内存栈区,如果其变量作用域结束,这个 block 就被废弃,block 上的 __block 变量也同样会被废弃。

    栈上的Block与__block变量.png

    为了解决这个问题,block 提供了 copy 的功能,将 block 和 __block 变量从栈拷贝到堆,也就是 _NSConcreteMallocBlock。

    _NSConcreteMallocBlock

    当 block 从栈拷贝到堆后,当栈上变量作用域结束时,仍然可以继续使用 block,在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上,例如:

    • block 作为函数返回值返回时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
    • block 被赋值给 __strong id 类型的对象或 block 的成员变量时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
    • block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy 或 _Block_copy 进行拷贝;

    编译器不会自动调用 copy 方法:

    • block 作为方法或函数的参数传递时;
    typedef int (^blk_t)(int);
    blk_t func(int rate)
    {
        return ^(int count){return rate * count;};
    }
    

    上面的 block 获取了外部变量,所以是创建在栈上,当 func 函数返回给调用者时,脱离了局部变量 rate 的作用范围,如果调用者使用这个 block 就会出问题。那 ARC 开启的情况呢?运行这个 block 一切正常,编译器编译之后的代码:

    blk_t func(int rate)
    {
        blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
        tmp = objc_retainBlock(tmp);
        return objc_autoreleaseReturnValue(tmp); 
    }
    

    objc_retainBlock本质上调用的是_Block_copy函数:

    id objc_retainBlock(id x) {
        return (id)_Block_copy(x);
    }
    

    objc_autoreleaseReturnValue 本质上调用的是objc_autorelease函数:

    id 
    objc_autoreleaseReturnValue(id obj)
    {
        if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;
    
        return objc_autorelease(obj);
    }
    

    block类型拷贝:

    Block类型 源拷贝区域 Copy结果
    _NSConcreteStackBlock 从栈复制到堆
    _NSConcreteGlobalBlock 程序的数据区域 什么都不做
    _NSConcreteMallocBlock 引用计数加1

    block 内存管理

    当 block 从栈内存被拷贝到堆内存时,__block 变量的变化如下图。需要说明的是,当栈上的 block 被拷贝到堆上,堆上的 block 再次被拷贝时,对 __block 变量已经没有影响了。

    block拷贝.png 多个Block使用__block变量.png block废弃与变量释放.png

    __forwarding

    block 从栈被拷贝到堆时,__forwarding 指针变量也会指向堆区的结构体。

    block 获取局部变量,当要在其他地方(超出局部变量作用范围)使用这个 block 的时候,由于访问局部变量异常,导致程序崩溃,因此需要将block从栈区拷贝到堆区。

    将 block 拷贝到堆上的同时,将 __forwarding 指针指向堆上结构体。后面如果要想使用 __block 变量,只要通过 __forwarding 访问堆上变量,就不会出现程序崩溃了。

    简单讲就是“不管__block变量配置在栈上还是堆上,都能正确的访问该变量。”

        __block int val = 0;
        void (^blk)(void) = [^{++val;} copy];
        ++val;
        blk();
        NSLog(@"%d", val);
    

    👆代码中 ^{++val;} 和 ++val; 都会被转换成 ++(val.__forwarding->val);,堆上的 val 被加了两次,最后打印堆上的 val 为 2。

    block 循环引用

    如果在Block使用附有__strong修饰符的对象类型自动变量,那么当Block从栈复制到堆上时,该对象为Block所持有。

    经典的循环引用是self与block之间的相互引用:

    typedef void (^blk_t)(void);
    
    @interface User()
    {
        blk_t blk_;
    }
    @end
    
    @implementation User
    
    - (id)init
    {
        self = [super init];
        blk_ = ^{NSLog(@"self = %@", self);};
        return self;
    }
    
    - (void)dealloc
    {
        NSLog(@"dealloc");
    }
    
    @end
    

    编译器会提示警告:

    Capturing 'self' strongly in this block is likely to lead to a retain cycle
    

    如果block中使用self中的属性或者成员变量:

    @interface User()
    {
        blk_t blk_;
    }
    
    @property (assign, nonatomic) NSInteger age;
    
    @end
    
    @implementation User
    
    - (id)init
    {
        self = [super init];
        blk_ = ^{
            NSLog(@"FlyElephant--%ld", (long)self.age);
        };
        return self;
    }
    
    - (void)dealloc
    {
        NSLog(@"dealloc");
    }
    
    @end
    

    编译会警告提示循环引用:

    Capturing 'self' strongly in this block is likely to lead to a retain cycle
    

    __weak修饰符解决循环引用问题:

        __weak typeof(User) *weakself = self;
        blk_ = ^{
            NSLog(@"FlyElephant--%ld", (long)weakself.age);
        };
        return self;
    

    __block避免循环引用:

    typedef void (^blk_t)(void);
    @interface User : NSObject
    {
        blk_t blk_;
    }
    @end
    
    @implementation User
    
    - (id)init
    {
        self = [super init];
        __block id tmp = self;
        blk_ = ^{
            NSLog(@"self = %@", tmp);
            tmp = nil;
        };
        return self;
    }
    
    - (void)execBlock
    {
        blk_();
    }
    
    - (void)dealloc
    {
        NSLog(@"dealloc");
    }
    
    @end
    
    int main()
    {
        id object = [[User alloc] init];
        [object execBlock];
        return 0;
    }
    

    该代码没有引起循环引用。但是如果不执行execBlock实例方式,即不执行复试给成员变量blk_的Block,会循环引用并引起内存泄漏。

    参考链接

    http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-1/

    http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-2/

    https://www.zybuluo.com/MicroCai/note/51116

    相关文章

      网友评论

        本文标题:iOS-Block的实现

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