美文网首页iOS
Block深入分析

Block深入分析

作者: 纸简书生 | 来源:发表于2018-03-14 22:59 被阅读627次

    虽然网上Block已经被写烂了,自己还是觉得,无论如何自己抱着求真的态度自己看一遍源码。之前介绍过Block的基本使用Block。这一片将深入介绍一下Block的具体实现。

    真没想到自己一直认为自己做了这么久的iOS不应该再去纠结Block这样的知识点,回头一看,知识还是得温故而知新。

    测试代码

    分析思路:使用clang -rewrite-objc将含有Block的.m文件翻译为.cpp。对照着翻译之后的源码进行分析。

    现在定义如下几个block。

    //没有捕获变量
    void blockFunc0()
    {
        void (^block)(void) = ^{
            NSLog(@"num ");
        };
        block();
    }
    
    //普通局部变量
    void blockFunc1()
    {
        int num = 100;
        void (^block)(void) = ^{
            NSLog(@"num equal %d", num);
        };
        num = 200;
        block();
    }
    //普通__block局部变量
    void blockFunc2()
    {
        __block int num = 100;
        void (^block)(void) = ^{
            NSLog(@"num equal %d", num);
        };
        num = 200;
        block();
    }
    
    //全局变量
    int num = 100;
    void blockFunc3()
    {
        void (^block)(void) = ^{
            NSLog(@"num equal %d", num);
        };
        num = 200;
        block();
    }
    
    //静态变量
    void blockFunc4()
    {
        static int num = 100;
        void (^block)(void) = ^{
            NSLog(@"num equal %d", num);
        };
        num = 200;
        block();
    }
    

    上面是准备的测试代码

    普通局部变量(blockFunc1)

    blockFunc1和blockFunc0翻译之后的差距就只是__blockFunc0_block_impl_0和__blockFunc1_block_impl_0结构体中多了num这个字段,其余的都一样。所以这几直接从blockFunc1讲起。

    执行完clang -rewrite-objc命令之后会得到不少的警告,最终翻译出来的文件比较大。因为翻译之后方法的名字不会变,所以可以通过搜索相关的方法名就能快速找到翻译之后方法对应的位置。

    blockFunc1翻译为(注意对比翻译前后的结果

    void blockFunc1()
    {
        int num = 100;
        void (*block)(void) = ((void (*)())&__blockFunc1_block_impl_0((void *)__blockFunc1_block_func_0, &__blockFunc1_block_desc_0_DATA, num));
        num = 200;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    

    根据上面的结果可以提取出如下几个重要的数据结构

    数据结构

    __block_impl

    定义block的结构体,根据定义可以知道Block实际上就是对象,保存了一个ISA指针。那么如果是对象的话就很顺利成长的把block的内存,生命周期管理同对象关联起来。目前block_impl的isa指针有_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock

    struct __block_impl {
      void *isa;//表明Block实际上是个对象
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    

    字段对应的含义

    字段名 含义
    isa isa 指向实例对象,表明 block 也是一个 Objective-C 对象。block 有三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,isa 对应有三种值。因为block是对像,则就有对象的内存管理及生命周期管理。恰恰三个对应的值也就是这个作用。
    Flags 按位表示一些block的附加信息
    Reserved 保留变量
    FuncPtr 函数指针,指向具体的block实现的函数调用地址

    __blockFunc1_block_desc_0

    保存对block的一些描述信息,比如保留字段大小,以及block结构大小。并且这里定义__blockFunc1_block_desc_0_DATA,并且还初始化了。可以看到初始化的情况下reserved为0,Block_size就是__blockFunc1_block_impl_0(包含了两种结构体的结构体,下面会介绍)大小。

    static struct __blockFunc1_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __blockFunc1_block_desc_0_DATA = { 0, sizeof(struct __blockFunc1_block_impl_0)};
    
    
    字段名 含义
    reserved 保留字段的大小
    Block_size block结构大小

    __blockFunc1_block_impl_0

    保存了block相关的信息,是前面两种结构体的结合体,包含了捕获的外部变量。比如这里的num,初始化的时候会初始化block_imp内部变量,如函数指针,block_imp的对象类型。

    struct __blockFunc1_block_impl_0 {
      struct __block_impl impl;
      struct __blockFunc1_block_desc_0* Desc;
      int num;//定义的变量
      //初始化传入参数有函数指针,block的秒速
      __blockFunc1_block_impl_0(void *fp, struct __blockFunc1_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    字段对应的含义

    字段名 含义
    impl block_imp结构体信息
    Desc 对block的附加描述
    *** 对外部捕获的变量

    方法翻译

    Block内容

    Block里面的具体内容被翻译为了__blockFunc1_block_func_0函数,注意int num = __cself->num; // bound by copy表明了num的值是值接拷贝过去的。这一点将会随着捕获外部变量的作用域不同而不同,后面会总结。

    static void __blockFunc1_block_func_0(struct __blockFunc1_block_impl_0 *__cself) {
      int num = __cself->num; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_8f_84gy29_n1dvdwnqfkytdv4nw0000gn_T_BlockTest_581040_mi_1, num);
        }
    

    Block调用

    这就是OC中的blockFunc1最终被翻译的结果。

    void blockFunc1()
    {
        int num = 100;
        void (*block)(void) = ((void (*)())&__blockFunc1_block_impl_0((void *)__blockFunc1_block_func_0, &__blockFunc1_block_desc_0_DATA, num));
        num = 200;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    

    注意void在C++中的一些不同,比如void*表示“任意类型的指针”或表示“该指针与一地址值相关,但是不清楚在此地址上的对象的类型”。,可以简单的理解为泛型的表示。 最终block被翻译为了函数指针。

    因为在__blockFunc1_block_impl_0创建的时候设置了最后一个参数flags=0的默认值,所以上面在创建__blockFunc1_block_impl_0没有传入参数flags。

    这里还需要注意的就是使用了C++中的结构体强转类型。对应的代码的就是__blockFunc1_block_func_0转为__block_impl类型。因为C++中,只要高地址的数据类型相同(也就是首地址相同)就可以实现强转。因为在__blockFunc1_block_func_0结构体中。第一个数据类型就是__block_impl所以可以实现强转。

    __block局部变量(blockFunc2)

    上面通过了第一个例子分析了整个过程。后面加与不加block其实实现内容都一样。下面列举几个不同点。

    如果使用__block修饰变量,则在生成为__blockFunc2_block_impl_0的时候对外部变量的修饰符不一样。具体来讲对应到下面的__Block_byref_num_0 *num; // by ref。同时在初始化__blockFunc2_block_impl_0的时候num会初始化为_num->__forwarding

    struct __blockFunc2_block_impl_0 {
      struct __block_impl impl;
      struct __blockFunc2_block_desc_0* Desc;
      __Block_byref_num_0 *num; // by ref
      __blockFunc2_block_impl_0(void *fp, struct __blockFunc2_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    数据结构

    _block_imp的结构同上面一样。

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

    其中各个字段含义如下:

    字段名 含义
    __isa 对象指针
    __forwarding 指向自己的指针
    __flags 标志位
    __size 结构体大小
    num 外部变量

    __blockFunc2_block_impl_0

    相比之前多了一个__Block_byref_num_0字段。该字段在初始化的时候就已经赋值了。

    struct __blockFunc2_block_impl_0 {
      struct __block_impl impl;
      struct __blockFunc2_block_desc_0* Desc;
      __Block_byref_num_0 *num; // by ref
      __blockFunc2_block_impl_0(void *fp, struct __blockFunc2_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    __blockFunc2_block_desc_0

    static struct __blockFunc2_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __blockFunc2_block_impl_0*, struct __blockFunc2_block_impl_0*);
      void (*dispose)(struct __blockFunc2_block_impl_0*);
    } __blockFunc2_block_desc_0_DATA = { 0, sizeof(struct __blockFunc2_block_impl_0), __blockFunc2_block_copy_0, __blockFunc2_block_dispose_0};
    

    相对于blockFunc1的翻译结果多了一个copy的方法以及一个dispose方法,并且在初始化的时候就确定了这两个方法。

    后面会知道这两个方法起的作用就是内存管理的作用。

    __blockFunc2_block_copy_0
    static void __blockFunc2_block_copy_0(struct __blockFunc2_block_impl_0*dst, struct __blockFunc2_block_impl_0*src) {_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}
    

    找到了_Block_object_assign的函数声明为void _Block_object_assign(void *, const void *, const int);。当block field是指针类型的时候就会发生拷贝。可以很明确的看到进行拷贝的其实是__blockFunc2_block_impl_0结构体中的__Block_byref_num_0。

    __blockFunc2_block_dispose_0
    static void __blockFunc2_block_dispose_0(struct __blockFunc2_block_impl_0*src) {_Block_object_dispose((void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}
    

    同样最终释放的也是__blockFunc2_block_impl_0中的__Block_byref_num_0。所以__Block_byref_num_0这个对象非常重要。

    方法翻译

    Block内容

    追忆这里直接传递的是__Block_byref_num_0结构体指针。在这个结构体指针里面保存了之前的外部变量。使用到外部变量的时候直接从结构体里面获取。

    static void __blockFunc2_block_func_0(struct __blockFunc2_block_impl_0 *__cself) {
      __Block_byref_num_0 *num = __cself->num; // bound by ref
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_8f_84gy29_n1dvdwnqfkytdv4nw0000gn_T_BlockTest_581040_mi_2, (num->__forwarding->num));
        }
    

    block调用

    void blockFunc2()
    {
        __attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 100};
        void (*block)(void) = ((void (*)())&__blockFunc2_block_impl_0((void *)__blockFunc2_block_func_0, &__blockFunc2_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
        (num.__forwarding->num) = 200;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    

    首先创建了一个__Block_byref_num_0结构体。里面记录了自己的指针以及结构体大小和外部变量的值100。在给外部变量赋值的时候使用了(num.__forwarding->num) = 200;,也通过指针的方式复制。这样就达到改变外部变量值得目的。

    情况分析

    根据上面的内容,当在使用__block修饰变量之后,之前直接出现外部变量(这里的num)的地方都被__Block_byref_num_0这个结构体所替换换。并且在此基础上还多了copy以及dispose方法。最终也是通过__Block_byref_num_0实现的内存管理。

    copy和dispose的作用如下:

    • 当blockFunc2从栈上被copy到堆上时,会调用__blockFunc2_block_copy_0将blockFunc2类型的成员变量num(具体来讲应该是__Block_byref_num_0)从栈上复制到堆上,而这个时候__forwarding指针就会指向堆上的结构体;

    • 当blockFunc2被释放时,相应地会调用__blockFunc2_block_dispose_0来释放blockFunc2类型的成员变量num(具体来讲应该是__Block_byref_num_0)。同样__forwarding指针指向堆上的结构体也就被释放。

    • 为什么要这么做呢?其实也是为了保证block能够访问有效的正确内存区域。

    因为blockFunc2函数中的局部变量num和函数__blockFunc2_block_impl_0不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中,我们可以通过指针来实现局部变量的修改。不过这是由于在调用__blockFunc2_block_impl_0时,blockFunc2函数栈还没展开完成,变量num还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了已经被销毁了,再用指针访问就会报常见的坏内存访问。

    因为block可以作为属性,并且也经常作为参数传递,而Block最终展开的是一个函数,展开的函数里面的变量作用域和被block被调用的函数作用域是不同的。通常在情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了已经被销毁了,再用指针访问就会报常见的坏内存访问。因此block有copy方法将其在堆上,所以在block被copy的同时,将局部变量间接也copy放在堆上就能够保证局部变量可以被block正常访问到。具体来讲可以看看下面这张图,最终是通过__forwarding指针来实现这个目的:

    全局变量(blockFunc3)

    这种情况下转换的结果block和不捕获任何变量的block结果是一样的。

    struct __blockFunc0_block_impl_0 {
      struct __block_impl impl;
      struct __blockFunc0_block_desc_0* Desc;
      __blockFunc0_block_impl_0(void *fp, struct __blockFunc0_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    int num = 100;//注意,这里已经声明了一个num为全局变量
    
    struct __blockFunc3_block_impl_0 {
      struct __block_impl impl;
      struct __blockFunc3_block_desc_0* Desc;
      __blockFunc3_block_impl_0(void *fp, struct __blockFunc3_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    因为是全局变量,所以任何地方都可以修改。跟block没什么关系(因为block最终也是转换为函数来调用)。

    static局部变量(blockFunc4)

    这种情况下重点看一下如下几点:

    __blockFunc4_block_impl_0中保持的是int *num;也即是指针。

    struct __blockFunc4_block_impl_0 {
      struct __block_impl impl;
      struct __blockFunc4_block_desc_0* Desc;
      int *num;
      __blockFunc4_block_impl_0(void *fp, struct __blockFunc4_block_desc_0 *desc, int *_num, int flags=0) : num(_num) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    block调用被转换为:

    void blockFunc4()
    {
        static int num = 100;
        void (*block)(void) = ((void (*)())&__blockFunc4_block_impl_0((void *)__blockFunc4_block_func_0, &__blockFunc4_block_desc_0_DATA, &num));
        num = 200;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    

    注意这里声明了static int num = 100;所以传递的是指针,和blockFunc1中最大的不同就是int num 变成了int *num 由整型变成了整型指针类型,注意:这个不再是传值,而是传地址,这说明对应 static 修饰的自动变量值在被 block 截获之后仍可以与外部自动变量保持同步,因为它们的地址是同一个。

    Block内存管理

    Block一共有三种类型,三面介绍的几种block全是_NSConcreteGlobalBlock。


    _NSConcreteGlobalBlock

    经过测试只有当 block 字面量写在全局作用域时,即为 global block。而且仅此一种,网上有人当 block 字面量不获取任何外部变量时也是global block,但是经过自己测试还是_NSConcreteStackBlock类型。

    globalblock如下形式:

    _NSConcreteStackBlock

    这种类型block是最多的一种,处于内存的栈区,如果其变量作用域结束,这个 block 就被废弃,block 上的 __block 变量也同样会被废弃。


    block 提供了 copy 的功能,将 block 和 __block 变量从栈拷贝到堆,就是 _NSConcreteMallocBlock。

    _NSConcreteMallocBlock

    当 block 从栈拷贝到堆后,当栈上变量作用域结束时,仍然可以继续使用 block

    堆上的 block 类型为 _NSConcreteMallocBlock,所以会将 _NSConcreteMallocBlock 写入 isa。对应到代码上就是impl.isa = &_NSConcreteMallocBlock

    ARC 下的 block

    在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上。
    block 作为函数的参数传递时,编译器不会自动调用 copy 方法

    如下这几种情况都不用手动拷贝

    • 当 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 拷贝;

    比如自动拷贝的情况:

    /************ ARC下编译器自动拷贝block ************/
    typedef int (^blk_t)(int);
    blk_t func(int rate)
    {
        return ^(int count){return rate * count;};
    }
    

    如果没有自动拷贝,因为外部传入的参数放到的是栈上,如果后面去调用这个返回的block肯定会发生异常,但是在ARC下面不会出问题,于是翻译一下。可以查到如下代码

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

    由于 block 字面量是创建在栈内存,通过 objc_retainBlock() 函数拷贝到堆内存,让 tmp 重新指向堆上的 block,然后将 tmp 所指的堆上的 block 作为一个 Objective-C 对象放入 autoreleasepool 里面,从而保证了返回后的 block 仍然可以正确执行。

    需要手动执行copy的block:

    /************ ARC下编译器手动拷贝block ************/
    id getBlockArray()
    {
        int val = 10;
        return [[NSArray alloc] initWithObjects: 
                                ^{NSLog(@"blk0:%d", val);}, 
                                ^{NSLog(@"blk1:%d", val);}, nil];
    }
    

    这里block最为了函数参数,编译器不会自动拷贝。所以在调用这个方法的时候会出现异常。

    总结

    Block原理总算是讲完了。其难点就是对于外部变量的捕获场景比较多。通过分析源码,比较麻烦的就是加了__block的情况。通过__forwarding指针达到栈和堆上面的切换。这里没有列举对象类型的例子原因是对象类型其实就是一个指针。简单来讲就是把上面的num换成指针类型而已,其余的规则都是一样的。

    __block的作用可以简单总结为,处理两个函数作用域访问访问变量的时候,防止坏内存访问。

    后面讲了Block的几种类型以及相关内存管理,同堆栈一样。最后将了ARC环境下Block的注意事项。

    相关文章

      网友评论

        本文标题:Block深入分析

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