美文网首页iOS底层探究
Block的实现原理

Block的实现原理

作者: XIAOHANG | 来源:发表于2017-06-21 23:23 被阅读60次

    摘要:

    我们在开发的过程中经常使用到block,不仅如此,apple的api里面也有很多使用到block,好比gcd里面也是大量使用了block。block在语法上来说比较简洁,不过还是需要注意不要引起了循环引用。

    裸block:

    我们先来看看最基本的block编译之后长啥样:

    typedef void (^Block)();
    
    int main(int argc, const char * argv[]) {
        
        @autoreleasepool
        {
            Block b = ^(){
                printf("Hello Gay");
            };
        }
        return 0;
    }
    

    我们使用 clang-rewrite-objc filename 指令编译一下到底是什么鬼

    struct __block_impl {
      void *isa;     //什么类型的block
      int Flags;     //block的一些附加信息,里面包含了何种类型的block
                     //以及引用计数,下面讲到copy的时候就能知道到底是干嘛的了
      int Reserved;  //保留位
      void *FuncPtr; //函数指针
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc; //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;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
                printf("Hello Gay");
    }
    
    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[]) {
        { 
            __AtAutoreleasePool __autoreleasepool; 
            Block b = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        }
        return 0;
    }
    

    我们可以看到声明的block以结构体的方式进行了存储,从侧面上来说,block是一个指向结构体的指针。其中__block_impl结构体记录的是block的相关信息包括block的类型以及引用计数等。

    block里面捕获变量之后

    我们假设在block里面捕获了一个变量,看看内部会变成什么样?

    typedef void (^Block)();
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool
        {
            int i = 0;
            Block b = ^(){
                printf("%d",i);
            };
        }
        return 0;
    }
    

    同样clang编译之后

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int i; //相对于未捕获变量的block来说,多了一个变量来保存值
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
    {
       int i = __cself->i; // bound by copy
       printf("%d",i);
     }
    
    int main(int argc, const char * argv[]) 
    {
        { 
            __AtAutoreleasePool __autoreleasepool; 
            int i = 0;
            Block b = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, i));
            //注意这里的i传的是值,因此意味着在当我们在外部修改这个值的时候,你调用block打印出来的时候,这个值依旧是之前的值
        }
        return 0;
    }
    

    乍一看,好像跟刚才的大差不差,__main_block_impl_0里面产生了一个变量用来保存之前捕获的值。

    block捕获可修改的变量之后

    贴上low B代码

    typedef void (^Block)();
    
    int main(int argc, const char * argv[]) {
    
        @autoreleasepool
        {
            __block int i = 0;
            Block b = ^(){
                printf("%d",i);
            };
        }
        return 0;
    }
    

    我们再次编译一下👀

    struct __Block_byref_i_0 {
      void *__isa;  //什么类型的数据
    __Block_byref_i_0 *__forwarding;  //这个到copy的时候就用得着了,主要是保证copy之后能够找到在堆上的那个变量
     int __flags;//变量的引用计数
     int __size;
     int i;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_i_0 *i; // by ref 这里跟之前不一样了,这次是用指针来保存的
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref
          printf("%d",(i->__forwarding->i));
    }
    
    //辅助block copy的时候,对捕获变量的存储方案
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    //辅助block release的时候,对捕获变量的释放策略
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    //这里面添加copy和dispose两个函数
    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[]) {
    
        { __AtAutoreleasePool __autoreleasepool; 
    
           //这里将结构体i的__forwarding指针指向了自身
            __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
    
            Block b = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
    //570425344干嘛的?后面识别block类型的时候就能用上了
    
        }
        return 0;
    }
    
    

    通过编译我们可以看到,block的描述里面多了两个函数(关于copy和release)

    Block_copy的实现

    之前的热身内容是为了让大家思路更加清(meng)晰(bi)。
    我们可以在编译器上使用Block_copy()或者在Block.h里面查看到关于Block_copy的定义

    #define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
    

    runtime.c中_Block_copy函数以这种方式实现了

    void *_Block_copy(const void *arg) {
        return _Block_copy_internal(arg, WANTS_ONE);
    }
    

    该函数内部调用了_Block_copy_internal,同样我们在runtime.c中能够查看到该函数的实现方式

    先贴上几个block类型的枚举,在这里能够看到Block_private.h

    enum{
        BLOCK_REFCOUNT_MASK =     (0xffff),
        BLOCK_NEEDS_FREE =        (1 << 24), //堆block
        BLOCK_HAS_COPY_DISPOSE =  (1 << 25),
        BLOCK_HAS_CTOR =          (1 << 26), */* Helpers have C++ code. */*
        BLOCK_IS_GC =             (1 << 27),
        BLOCK_IS_GLOBAL =         (1 << 28),   //全局block
        BLOCK_HAS_DESCRIPTOR =    (1 << 29)
    };
    

    _Block_copy_internal实现如下

    static void *_Block_copy_internal(const void *arg, const int flags) 
    {
        struct Block_layout *aBlock;
        
        const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
     
        if (!arg) return NULL;
        
        aBlock = (struct Block_layout *)arg;
       //   还记得上面的那个初始化bloc时候赋值给flags标志位的570425344吗
       //   570425344 & (1 << 24) = 0 不满足跳过
        if (aBlock->flags & BLOCK_NEEDS_FREE) { 
            latching_incr_int(&aBlock->flags);
            return aBlock;
        }
       //这边也是不满足条件的
        else if (aBlock->flags & BLOCK_IS_GC) {
           //此处省略若干行代码。。。
            return aBlock;
        }
        else if (aBlock->flags & BLOCK_IS_GLOBAL) 
        {
            return aBlock;
        }
    
        //最后来到这
        //isGC在runtime.c文件里面能够找到,该变量被初始化为false
        // Its a stack block.  Make a copy.
        // 对栈block的copy
        if (!isGC) {
            struct Block_layout *result = malloc(aBlock->descriptor->size);
            if (!result) return (void *)0;
            memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
            // reset refcount
            result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    
    
            //这里是重点给flags标识为加上BLOCK_NEEDS_FREE标识位
            //同时增加了一个引用计数
            result->flags |= BLOCK_NEEDS_FREE | 1;
    
           //修改isa指针
            result->isa = _NSConcreteMallocBlock;                  
            if (result->flags & BLOCK_HAS_COPY_DISPOSE) 
           {
                //如果存在copy的辅助函数,会调用该辅助函数,
                //当捕获了引用的时候,显然是满足条件的。
                (*aBlock->descriptor->copy)(result, aBlock); // do fixup
            }
            return result;
        }
        else {
            //此处省略。。。
        }
    }
    

    至此,相信都知道了copy是怎么样一个过程了。当对一个堆上的block再次进行调用copy的时候,因为我们之前给flags打入了BLOCK_NEEDS_FREE这个值,所以最后走的是这个判定条件

        if (aBlock->flags & BLOCK_NEEDS_FREE) { 
            latching_incr_int(&aBlock->flags);
            return aBlock;
        }
    

    结果就是增加引用计数,然后返回该block。

    辅助copy/dispose函数

    1.普通变量的copy

    在前面我们说到,如果有block复制到堆上的时候,有copy辅助函数的,该函数会被调用。以__block int i = 0为例子生成的辅助函数如下

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
    {   
         _Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) 
    {
        _Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    

    在此之前,先赋上block中copy辅助函数,flags标识位能够支持的类型

    enum {
        BLOCK_FIELD_IS_OBJECT   =  3,  /* id, NSObject, __attribute__((NSObject)), block, ... */
        BLOCK_FIELD_IS_BLOCK    =  7,  /* a block variable */
        BLOCK_FIELD_IS_BYREF    =  8,  // __block修饰的基本数据类型
        BLOCK_FIELD_IS_WEAK     = 16,  /* declared __weak, only used in byref copy helpers */
        BLOCK_BYREF_CALLER      = 128  /* called from __block (byref) copy/dispose support routines. */
    };
    

    runtime.c里面_Block_object_assign函数的实现方式如下

    void _Block_object_assign(void *destAddr, const void *object, const int flags) 
    {
        if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) 
       {
            if ((flags & BLOCK_FIELD_IS_WEAK) == BLOCK_FIELD_IS_WEAK) 
            {
                _Block_assign_weak(object, destAddr);
            }
            else 
            {
                _Block_assign((void *)object, destAddr);
            }
        }
        
        //之前的生成的辅助函数,flags = 8 => BLOCK_FIELD_IS_BYREF
    
        else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF)  
        {
           //在这里调用了_Block_byref_assign_copy,看下面的那个函数
            _Block_byref_assign_copy(destAddr, object, flags);
        }
        else if ((flags & BLOCK_FIELD_IS_BLOCK) == BLOCK_FIELD_IS_BLOCK) 
        {
            _Block_assign(_Block_copy_internal(object, flags), destAddr);
        }
        else if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) 
        {
            _Block_retain_object(object);
            _Block_assign((void *)object, destAddr);
        }
    }
    
    static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) 
    {
        struct Block_byref **destp = (struct Block_byref **)dest;
        struct Block_byref *src = (struct Block_byref *)arg;
            
        if (src->forwarding->flags & BLOCK_IS_GC) {
            ;   // don't need to do any more work
        }
        else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
            //省略若干行
        }
        //因为之前block已经拷贝到对上了,所以最后走的是这边,对该变量增加了一个引用计数
        else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
            latching_incr_int(&src->forwarding->flags);
        }
        // 在这里进行赋值操作
        _Block_assign(src->forwarding, (void **)destp);
    }
    
    

    上面就是一个被__block修饰的基本数据类型拷贝到堆上的时候,copy函数的实际调用过程。

    普通oc对象的复制

    以捕获NSObject对象为栗子
    辅助函数如下:

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    

    同样是调用_Block_object_assign这个函数,不过最终走的是这个

    if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) 
    {
            _Block_retain_object(object);
            _Block_assign((void *)object, destAddr);
    }
    

    把该对象retain之后,然后再赋值,这也就是说为什么block里面的对象需要使用weak的原因。

    __block修饰的oc对象的复制

    还是以NSObject为栗子,辅助函数如下:

    //131即为BLOCK_FIELD_IS_OBJECT|BLOCK_BYREF_CALLER 
    
    static void __Block_byref_id_object_copy_131(void *dst, void *src) {
     _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
    }
    
    static void __Block_byref_id_object_dispose_131(void *src) {
     _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
    }
    

    同样是调用_Block_object_assign这个函数,最终走的是这个

        if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) {
            if ((flags & BLOCK_FIELD_IS_WEAK) == BLOCK_FIELD_IS_WEAK) {
                _Block_assign_weak(object, destAddr);
            }
            else {
                // 最终走的是这个
                // do *not* retain or *copy* __block variables whatever they are
                _Block_assign((void *)object, destAddr);
            }
        }
    

    该函数说明,如果你使用了______block对oc对象进行修饰,那么仅仅只是赋值,不增加引用计数,这就是使用______block不会造成循环引用的原因。

    小结

    通过上面的分析,相信大家对block有了更加清晰的理解。🐶
    如果你看完以上内容觉得so easy,那么你可能是大神。
    如果你看完以上内容觉得啥JB玩意,那么可能是我写的太渣了。
    如果文章中,有错误的地方欢迎大家提出指正

    附录

    本文参考了以下两篇帖子,特此奉上:
    没事蹦蹦的Block实现原理
    BobooO的iOS中block介绍(四)揭开神秘面纱(下)

    相关文章

      网友评论

        本文标题:Block的实现原理

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