美文网首页
探究Block的实现

探究Block的实现

作者: Helly1024 | 来源:发表于2016-04-16 00:06 被阅读471次

    在开发过程中,我们会经常使用到Block,今天就让我们来探究一下Block的实现。

    一、NSConcreteGlobalBlock类型的block的实现

    首先我们写一个最简单的Block,然后用clang -rewrite-objc命令将其重写成C++的实现。

    #import <Foundation/Foundation.h>
    
    int main(int argc, const char * argv[]) {
        
        void (^printBlock)(void) = ^{
            printf("Hello, World!\n");
        };
        
        printBlock();
        
        return 0;
    }
    

    上述代码通过clang -rewrite-objc命令将变换成如下形式(省略了无关代码):

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    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("Hello, World!\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 (*printBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
        ((void (*)(__block_impl *))((__block_impl *)printBlock)->FuncPtr)((__block_impl *)printBlock);
    
        return 0;
    }
    

    首先我们从两段代码相似度最高的地方入手,可以发现:

    ^{
        printf("Hello, World!\n");
    };
    

    被转换成了:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
            printf("Hello, World!\n");
    }
    

    我们可以看到Block花括号中的代码实际上是作为一个C语言的函数来处理的。多做几个实验可以发现,该函数名的前缀是Block所在的函数名(这里是main),后缀是该Block在所在函数中出现的顺序值(这里是0)。
    该函数的参数__cself是一个指向结构体__main_block_impl_0的指针。该结构体的声明如下:

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

    这个结构体看起来有些复杂,但是当我们先除去它的构造函数并展开它嵌套的两个结构体,就会发现它跟常规的结构体是一样的:

    struct __main_block_impl_0 {
      void *isa;    // 指向该对象所属的类
      int Flags;    // 用于按位表示一些block的附加信息
      int Reserved;   // 保留字段
      void *FuncPtr;    // 指向实现Block的函数的地址(这里即函数__main_block_func_0的地址)
      size_t reserved;    // 保留字段
      size_t Block_size;    // Block占用内存空间的大小
    }
    

    下面我们来看看结构体__main_block_impl_0的构造函数:

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

    它是在int main(int argc, const char * argv[])函数中调用的:

      void (*printBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    

    这段代码进行了很多转换,所以看起来比较复杂,下面我们来一步步地分析:

    __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    

    首先是调用__main_block_impl_0结构体的构造函数生成了一个__main_block_impl_0结构体实例;
    然后使用取地址符(&)获取该实例的地址;
    最后将该地址赋值给__main_block_impl_0结构体指针printBlock

    上面的代码也可以转换成如下形式:

    struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
    struct __main_block_impl_0 *printBlock = &tmp;
    

    上述代码对应最初源码中的这一段:

    void (^printBlock)(void) = ^{
      printf("Hello, World!\n");
    };
    

    我们再来看构造函数的参数,在调用构造函数时传递了两个参数:指向__main_block_func_0的函数指针和__main_block_desc_0类型的结构体__main_block_desc_0_DATA的地址。在函数的实现部分则将这两个参数分别赋值给__main_block_impl_0类型结构体的成员变量FuncPtrdesc,用以进行结构体的初始化。现在就不难理解__main_block_func_0(struct __main_block_impl_0 *__cself)函数中的参数__cself了:它指向了将该函数指针作为成员变量的__main_block_impl_0结构体的实例,相当于C++实例方法中指向实例自身的变量this,或者是OC实例方法中指向对象自身的变量self。

    接下来在最初源码中调用了该Block:

    printBlock();
    

    对应转换后的这行代码:

    ((void (*)(__block_impl *))((__block_impl *)printBlock)->FuncPtr)((__block_impl *)printBlock);
    

    去掉转换部分是这样:

    (*printBlock->FuncPtr)(printBlock);
    

    其实就是使用函数指针来调用函数,后面的括号中的printBlock是参数,这也印证了上面对__cself的解释。

    现在可以确定,Block的实际上就是一个结构体,使用Block就是通过结构体中的成员变量,指向__main_block_func_0函数的函数指针FuncPtr来调用__main_block_func_0函数。

    __main_block_impl_0结构体中我们还有一个成员变量isa一直没有说,它代表了Block的类型,有以下三种类型:

    • _NSConcreteGlobalBlock,全局静态Block,它不会访问任何外部变量,我们前面研究的那个Block就是全局Block。虽然它的isa指针指向的是_NSConcreteStackBlock,但这是由于我们使用clang命令将OC的实现转换成C++的的实现方式和LLVM不同。实际上在代码运行过程中po这个Block就会发现它是_NSConcreteGlobalBlock
    • _NSConcreteStackBlock,保存在栈中的Block,只存在于某个固定的作用域(如函数)当中,当超出这个作用域Block就会被销毁;
    • _NSConcreteMallocBlock,保存在堆中的Block,这种Block无法直接创建,是通过_NSConcreteStackBlock拷贝到堆中而来,要在多个地方使用同一个栈上的Block时,就需要将Block从栈上拷贝到堆中,以防止Block在栈中被销毁。

    另外,ARC对Block也有影响。在开启ARC的情况下,只会有_NSConcreteGlobalBlock_NSConcreteMallocBlock_NSConcreteStackBlock将会被_NSConcreteMallocBlock替代。

    二、NSConcreteStackBlock类型的block的实现

    接下来我们看看_NSConcreteStackBlock的实现方式和_NSConcreteGlobalBlock有什么不同。这两种Block的区别在于_NSConcreteStackBlock将会捕捉外部变量:

    #import <Foundation/Foundation.h>
    
    int main(int argc, const char * argv[]) {s
        int a = 1024;
        void (^printBlock)(void) = ^{
            printf("Hello, World!\n%d\n",a);
        };
        
        printBlock();
        
        return 0;
    }
    

    以上代码通过clang -rewrite-objc命令转换后是这样的:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int a;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
    
            printf("Hello, World!\n%d\n",a);
        }
    
    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 a = 1024;
        void (*printBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    
        ((void (*)(__block_impl *))((__block_impl *)printBlock)->FuncPtr)((__block_impl *)printBlock);
    
        return 0;
    }
    

    它与_NSConcreteGlobalBlock源码不同的地方在于:

    1. 在实现Block的结构体__main_block_impl_0中多出了一个成员变量int a
    2. __main_block_func_0函数中多了一行代码int a = __cself->a;;
    3. main函数调用__main_block_impl_0的构造函数时增加了一个参数,main函数中的局部变量a

    现在Block捕获外部变量的过程就可以理解了:外部变量的值作为参数传递给__main_block_impl_0结构体的构造函数,并在结构体中添加一个同名的成员变量来保存。在执行block花括号中的代码的过程其实就是调用__main_block_func_0函数,这时__main_block_func_0函数通过参数__cself就可以获取外部变量的值。在这个过程中传递的是外部变量的值,这也是没有用__block来修饰的外部变量不能在Block中修改的原因。

    三、使用__block修饰的外部变量的实现

    如果使用了__block修饰外部变量:

    #import <Foundation/Foundation.h>
    
    int main(int argc, const char * argv[]) {s
        __block int a = 1024;
        void (^printBlock)(void) = ^{
            printf("Hello, World!\n%d\n",a);
        };
        
        a += 1;
        printBlock();
        
        return 0;
    }
    

    那转换成C++后的源码又将大不相同:

    struct __Block_byref_a_0 {
      void *__isa;
    __Block_byref_a_0 *__forwarding;
     int __flags;
     int __size;
     int a;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_a_0 *a; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
    
            printf("Hello, World!\n%d\n",(a->__forwarding->a));
        }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1024};
        void (*printBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    
        (a.__forwarding->a) += 1;
        ((void (*)(__block_impl *))((__block_impl *)printBlock)->FuncPtr)((__block_impl *)printBlock);
    
        return 0;
    }
    

    这与没有使用__block修饰的外部变量的源码不同的地方是__main_block_impl_0结构体中的int a;变成了__Block_byref_a_0 *a;,一个指向__Block_byref_a_0结构体的指针。在main函数中声明了一个__Block_byref_a_0结构体变量a并为它赋值:

        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1024};
    

    与它对应的OC代码是:

        __block int a = 1024;
    

    带有__block修饰的局部变量a转换成了一个__Block_byref_a_0类型的结构体变量a,结构体中保存了该结构体变量的地址和变量a的值。下一行代码同样是通过构造函数使__main_block_impl_0结构体变量保存了结构体变量a的地址。

        void (*printBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    

    这样,在__main_block_func_0函数中就可以获取到原来局部变量a的值了。同时在Block外,main函数中访问局部变量a的值得方式也发生了改变。它同__main_block_func_0函数中一样,同样是通过__Block_byref_a_0结构体变量中指向自己的结构体指针来访问的:

        (a.__forwarding->a) += 1;
    

    总结成一句话:使用__block修饰的外部变量在Block中能被修改是因为Block是通过指针访问的,而没有使用__block修饰的外部变量,仅仅是将它的值拷贝到了Block中。

    四、NSConcreteMallocBlock类型的block的实现

    另外,在__main_block_desc_0结构体中还多了两个成员变量:指向__main_block_copy_0函数的函数指针copy和指向__main_block_dispose_0函数的函数指针dispose。根据函数名和实现可以确定它们跟Block的拷贝有关,那就先来看看将Block从栈上拷贝到堆中是如何实现的。拷贝操作需要调用Block_copy()函数,在Block.h文件中可以找到它的定义:

    #define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
    
    BLOCK_EXPORT void *_Block_copy(const void *aBlock)
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    

    Block_copy是一个宏定义,它将参数进行了强制类型转换然后传给了_Block_copy函数。在LLVM源码的runtime.c文件中可以看到它的实现。这个函数的作用是在堆中创建一个Block的拷贝,或者为一个已经在堆中的Block添加引用。需要注意的是它必须和Block_release成对出现以恢复内存。

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

    _Block_copy函数又将Block_copy函数传入的Block和WANTS_ONE作为参数调用了_Block_copy_internal函数。在的runtime.c的286~355行可以找到_Block_copy_internal的实现(删除了垃圾回收相关的代码并添加注释):

    /* Copy, or bump refcount, of a block.  If really copying, call the copy helper if present. */
    static void *_Block_copy_internal(const void *arg, const int flags) {
        struct Block_layout *aBlock;
    
        // 判断如果参数为`NULL`则直接返回
        if (!arg) return NULL;
        
        // 将参数从指针还原成Block结构体
        aBlock = (struct Block_layout *)arg;
        
        // 如果flags中包含BLOCK_NEEDS_FREE,则说明这个Block在堆上,于是通过latching_incr_int函数将引用计数加1,这里可以判断出,Block结构体的flags中包含了Block类型和引用计数等信息
        if (aBlock->flags & BLOCK_NEEDS_FREE) {
            latching_incr_int(&aBlock->flags);
            return aBlock;
        }
        
        // 如果这是一个全局Block,就什么也不做
        else if (aBlock->flags & BLOCK_IS_GLOBAL) {
            return aBlock;
        }
    
            // 代码运行到这可以确定这是一个在栈上的Block了,于是在堆上开辟一块和Block对应大小的空间,失败则返回0
            struct Block_layout *result = malloc(aBlock->descriptor->size);
            if (!result) return (void *)0;
            
            // 将原来的的Block按位拷贝到新开辟的内存空间
            memmove(result, aBlock, aBlock->descriptor->size);
            
            // 修改堆上Block的flags,重置Block的类型信息和引用计数
            result->flags &= ~(BLOCK_REFCOUNT_MASK);
            result->flags |= BLOCK_NEEDS_FREE | 1;
            
            // 将Block的isa指针设置为_NSConcreteMallocBlock
            result->isa = _NSConcreteMallocBlock;
            
            // 如果存在,则调用Block的辅助拷贝函数
            if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
                (*aBlock->descriptor->copy)(result, aBlock); // do fixup
            }
            return result;
    }
    

    在这段代码的最后判断了Block的结构体实例中是否存在一个copy函数,如果存在,则会以指向堆上Block结构体实例的指针和指向栈上结构体实例的指针为参数调用copy函数。这个copy函数就是我们之前发现在__main_block_desc_0结构体中多出来的两个成员变量中的一个。下面来看看copy函数的实现。

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

    __main_block_copy_0函数只是简单的调用了_Block_object_assign。根据在_Block_copy_internal函数的源码调用copy时传的传递的参数可知_Block_object_assign的参数分别是堆上Block结构体实例中保存外部变量的成员变量的地址和栈上Block结构体实例中保存的外部变量和一个用来表示外部变量类型的常数。在runtime.c文件中我们同样可以找到_Block_object_assign函数的实现(省略了无关代码):

    void _Block_object_assign(void *destAddr, const void *object, const int flags) {
        
        if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF)  {
            _Block_byref_assign_copy(destAddr, object, flags);
        }
    }
    

    在这个函数中调用了_Block_byref_assign_copy函数。这个函数的作用是将栈上__block修饰的变量(也就是__Block_byref_a_0结构体实例)拷贝到堆中。以下是它的实现(删除了垃圾回收相关代码)。

    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_REFCOUNT_MASK) == 0) {
            
            // 在堆中开辟一块与传入的Block_byref结构体相同大小的内存空间
            struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
            
            // 设置堆中Block_byref结构体的flags的值
            copy->flags = src->flags | _Byref_flag_initial_value; 
            
            // 使forwarding指针指向自己
            copy->forwarding = copy;
            
            // 设置栈上的Block_byref结构体中的forwarding指针指向堆中的Block_byref结构体,这样无论通过哪个结构体的forwarding指针,访问到的都是堆上的Block_byref结构体
            src->forwarding = copy; 
            
            // 设置堆中Block_byref结构体的size的值
            copy->size = src->size;
        }
        
        // 已经在堆中,增加引用计数
        else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
            latching_incr_int(&src->forwarding->flags);
        }
        
        // 使通过参数传进来的结构体指针指向将堆上Block_byref结构体,即持有`__block`修饰的变量
        _Block_assign(src->forwarding, (void **)destp);
    }
    

    在上文中我们提到过Block_copy必须和Block_release成对出现以恢复内存。那么Block_release的作用就不言而喻,它在我们不再使用Block的时候将其释放,同样在Block_release中也会调用__main_block_desc_0结构体中的dispose函数,用来释放被Block持有的__block修饰的变量。

    通过上面的分析可以知道,在Block从栈上拷贝到堆中的过程中,Block中使用的__block变量同样会被从栈上拷贝到堆中。这点并不难理解,当需要将Block拷贝到堆上时,很多时候是因为要在其他地方使用这个Block,而此时很有可能已经超出了__block变量的作用域。为了避免出现这样的问题,将__block变量拷贝到堆中并由Block持有也是顺理成章的事了。

    五、Block的循环引用

    堆上的Block不光会持有__block变量,同样也会持有在Block中使用的没有用__block修饰的外部变量,这也是Block会出现循环引用问题的根源。因此在编码过程中,我们要避免出现Block和Block中使用的外部变量相互强引用的情况(这里所说的外部变量默认为是由__strong修饰)。

    __weak typeof(self) weakSelf = self;
    
    void (^printBlock)(void) = ^{
        NSLog(@"%@", weakSelf);
    };
    

    或者,在合适的时机打断它们之间的相互强引用。

    __block blockSelf = self;
    
    void (^printBlock)(void) = ^{
        NSLog(@"%@", blockSelf);
        blockSelf = nil;
    };
    

    但是这种方法有一个缺点,那就是:为了防止循环引用,必须执行Block。

    相关文章

      网友评论

          本文标题:探究Block的实现

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