重识Objective-C:Block底层实现

本文主要整理了Objective-C的Block实现方式。
iOS 其他相关博文链接**iOS-day-by-day**

Block语法

Block可以认为是一个匿名函数。语法声明如下:

1
return_type (^block_name)(parameters)

例如:

1
double (^multiplyTwoValues)(double, double);

Block字面值的写法:

1
2
3
^ (double firstValue, double secondValue) {
return firstValue *secondValue;
}

上面写法省略了返回值的类型,可以显示的指出返回值类型。

1
2
3
4
5
typedef double (^MultiplyTwoVlaues)(double, double);
    MultiplyTwoVlaues mtv = ^(double firstValue, double secondValue){
        return firstValue * secondValue;
    };
    NSLog(@"%f", mtv(3, 4));

Block也是一个Objective-C对象,可以用于赋值,当参数传递,也可以放在集合中。

数据结构定义

block的数据结构定义:
block
对应的结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

block实际上由6部分组成:

  1. isa指针,所有对象都有该指针,用来事项对象相关实现。
  2. flags,用于按位表示一些block附加信息。
  3. reserved, 保留变量。
  4. invoke, 函数指针,指向具体的block实现的函数调用地址。
  5. descriptor, 表示该block的附加描述信息,主要是size的大小,以及copy和dispose函数指针。
  6. variables,capture过来的变量,block能够访问它外部的局部变量,就是因为将这些变量复制到了结构体中。

Block分类

在Objective-C中,一共有3种类型的Block:

  1. _NSConcreteGlobalBlock全局的静态block,不会访问任何外部变量。
  2. _NSConcreteStackBlock 保存在栈中的block,当函数返回时会被销毁。
  3. _NSConcreateMallocBlock 保存在堆中的block,当引用计数为0时会被销毁。

Clang

为了研究编译器是如何实现block的,需要使用clang,clang命令,可以将Objective-C的源码改写成C语言,命令如下:

1
clang -rewrite-objc xxx.c

Block实现

全局的静态Block实现

新建一个globalBlock.c的源文件:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
    ^{
        printf("Hello, World!\n");
    } ();
    return 0;
}

在文件所在的文件夹,使用命令:clang -rewrite-objc globalBlock.c,在目录中得到一个名为globalBlock.c的文件。其中关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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()
{
    ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)) ();

    return 0;
}

下面具体看一下是如何实现的。__main_block_impl_0就是block的实现,从中可以看出:

  1. 一个block实际上就是一个对象,主要由isaimpldescriptor组成。
  2. 在LLVM实现中,开启ARC时。block的isa应该是_NSConcreteGlobalBlock类型。因为使用clang命令并没有开启ARC,所以还是_NSConreteStackBlock类型。
  3. impl是实际的函数指针。它指向 __main_block_func_0。其实,impl就是 invoke变量。
  4. descriptor 是描述当前这个block的附加信息,包括大小,需要capture和dispose的变量列表等。

静态Block实现

新建一个名为 stackBlock.c的文件:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
    int a = 100;
    void (^stackBlock)(void) = ^{
        printf("%d\n", a);
    };
    stackBlock();
    return 0;
}

使用clang命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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("%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 a = 100;
    void (*stackBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    ((void (*)(__block_impl *))((__block_impl *)stackBlock)->FuncPtr)((__block_impl *)stackBlock);
    return 0;
}

本例中,

  1. isa指向_NSConcreteStackBlock,这是分配在栈上的实例。
  2. __main_block_impl_0中增加了一个变量a, 在block中引用的变量a实际是在声明block时,被复制到__main_block_impl_0结构体中的那个变量a。所以,在block内部修改变量a的内容, 不会影响外部变量a。
  3. __main_block_impl_0增加了变量a, 所有结构体大小变了,该结构体被写在__main_block_desc_0中。

修改上面的代码,在变量前面添加__block关键字:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main()
{
  __block  int i = 100;
    void (^stackBlock)(void) = ^{
        printf("%d\n", i);
        i = 10;
    };
    stackBlock();
    return 0;
}

转换代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 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\n", (i->__forwarding->i));
        (i->__forwarding->i) = 10;
    }
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*/);}

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()
{
  __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 100};
    void (*stackBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)stackBlock)->FuncPtr)((__block_impl *)stackBlock);
    return 0;
}

代码可以看到:

  1. 源码中添加了一个名为__Block_byref_i_0 的结构体,用来保存需要capture并且修改的变量i。
  2. __main_block_impl_0中引用的__Block_byref_i_0的结构体指针,这样就可以达到修改外部变量的作用。
  3. __Block_byref_i_0结构体中带有isa,说明也是一个对象。
  4. 我们需要负责__Block_byref_i_0结构体相关的内存管理,所以__main_block_desc_0中增加了copy和dispose函数指针,对于在调用前后修改相应的引用计数。

堆Block实现

NSConcreteMallocBlock类型的block通常不会再源码中直接出现,默认当block被copy的时候,才会将block复制到堆中。以下是block被copy时的代码(来自这里),在第8步,目标block类型被修改为_NSConcreteMallocBlock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
    
    // 1
    if (!arg) return NULL;
    
    // 2
    aBlock = (struct Block_layout *)arg;
    
    // 3
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    
    // 4
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    
    // 5
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return (void *)0;
    
    // 6
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    
    // 7
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 1;
    
    // 8
    result->isa = _NSConcreteMallocBlock;
    
    // 9
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); // do fixup
    }
    
    return result;
}

变量的复制

  • 对于block外部变量的引用,block默认是将其复制到其数据结构中来实现访问的。
  • 对于__block修饰的外部变量引用,block是复制其引用地址来实现访问的。

另外,可以参考《招聘一个靠谱的iOS (下)13、14题》

ARC,MRC 对Block类型的影响

在ARC开启的情况下,将只会有NSConcreteGlobalBlock和NSConcreteMallocBlock类型的block。原本NSConreteStackBlock被NSConcreteMallocBlock类型替代。

在Block中,如果只使用全局或静态变量或者不是用外部变量,那么Block块的代码会存储在全局区。
在ARC中

  • 如果使用外部变量,Block块的代码会存储在堆区。

在MRC中

  • 如果使用外部变量,Block块的代码会存储在栈区。

Block默认情况下不能修改外部变量,只能读取外部变量。
在ARC中

  • 外部变量在堆中,这个变量在Block块内与在Block块外地址相同。
  • 外部变量在栈中,这个变量会被copy到为Block代码块所分配的堆中。

在MRC中

  • 外部变量在堆中,这个变量在Block块内与在Block块外地址相同。
  • 外部变量在栈中,这个变量会被copy到为Block代码块所分配的栈中

如果需要修改外部变量,需要在外部变量前声明__block。
在ARC中

  • 外部变量存在堆中,这个变量在Block块内与Block块外地址相同。
  • 外部变量存在栈中,这个变量会被转移到堆中,不是复制。

在MRC中

  • 外部变量存在堆中,这个变量在Block块内与Block块外地址相同。
  • 外部变量存在栈中,这个变量在Block块内与Block块外地址相同。

关于Block的面试题

  1. 使用block时什么情况会发生引用循环,如何解决?
  2. 在block内如何修改block外部变量?
  3. 使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?

参考链接

谈Objective-C block的实现

Block教程系列

对Objective-C中Block的追探

iOS中block实现的探究

Block 编程