探究Block的实现

2016-04-16  本文已影响471人  Helly1024

在开发过程中,我们会经常使用到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的类型,有以下三种类型:

另外,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。

上一篇 下一篇

猜你喜欢

热点阅读