文章

Block的实现

Block的实现

Block的实质

main.m

1
2
3
4
5
6
7
#include <stdio.h>

int main(int argc, const char * argv[]) {
    void (^blk) (void) = ^ () { printf("Block\n"); };
    blk();
    return 0;
}

在命令行输入: clang -rewrite-objc main.m,可以通过clang转换成C++源码。

main.cpp(部分代码)

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
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("Block\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 (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

事实上,Block的部分:

1
printf("Block\n");

被转换成了C语言函数,参数__cself为指向Block的变量(__main_block_impl_0结构体的指针)。

1
2
3
4
static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
{
    printf("Block\n"); 
}

现在,我们来关注__main_block_impl_0这个结构体。抛开构造函数,该结构体是这样的:

1
2
3
4
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
};

有两个成员变量,一个是__block_impl类型的impl,一个是__main_block_desc_0类型的指针Desc,他们的声明在上面源码也有给出。

继续关注他的构造函数:

1
2
3
4
5
6
    __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;
  }

这段构造函数,在main函数中被调用了:

1
void (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

去掉转换部分:

1
2
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;

blk结构体指针是通过__main_block_impl_0构造出来的,而__main_block_impl_0相当于Objective-C类对象。 __main_block_impl_0中的impl.isa是通过_NSConcreteStackBlock初始化的。在将Block作为Objective-C的对象处理时,关于该类的信息放置于_NSConcreteStackBlock中。

因此,Block的本质为Objective-C对象

最后看看使用block的部分:

1
blk();

源代码:

1
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

转换后:

1
(*blk->impl.FuncPrt)(blk);

这是使用函数指针调用函数,由Block语法转换的 __main_block_func_0函数的指针被赋值在了impl.FuncPtr中。另外,也说明了 __main_block_func_0函数的参数 __cself指向Block(即 blk)值,在函数指针调用函数中,blk正是作为函数参数传递的。

截获自动变量

在Block中使用的自动变量,会被作为成员变量追加到__main_block_impl_0结构体中,没有使用的则不会被追加。

在源码中添加一个自动变量:

1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
    int dmy = 256;
    void (^blk) (void) = ^ () {
        printf("%d\n", dmy);
    };
    blk();
    return 0;
}

clang转换后:

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int dmy;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _dmy, int flags=0) : dmy(_dmy) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
1
2
3
4
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int dmy = __cself->dmy; // bound by copy
    printf("%d\n", dmy);
}
1
2
3
4
5
6
int main(int argc, const char * argv[]) {
    int dmy = 256;
    void (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, dmy));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

总的来说,“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到了Block的结构体实例(即Block自身)中。

__block说明符

解决Block不能保存值这一问题,有两种方法。

  1. 这3种变量

    • 静态变量
    • 静态全局变量
    • 全局变量
  2. 使用__block说明符

    通过clang转换后的源码分析,可以发现,使用__block说明符转换后的变量,变成一个结构体实例。原本的变量,成为了该结构体变量的一个变量。

Block存储域

Block和__block变量的实质

名称 实质
Block 栈上Block的结构体实例
__block变量 栈上__block变量的结构体实例

Block类

设置对象的存储域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 程序的数据区域(.data区)
_NSConcreteMallocBlock

通过clang转换的源代码通常是_NSConcreteStackBlock对象,但是这两种情况下,Block为_NSConcreteGlobalBlock对象:

  • 记叙全局变量的地方有Block语法时
  • Block语法的的表达式中不使用截获的自动变量时

大多数情况下,编译器会恰当的进行判断,自动生成将Block从栈上复制到堆上。

以下情况需要程序员自己通过copy方法将Block从栈区复制到堆区:

  • 向方法或函数的参数中传递Block时

不需要手动复制的情况:

  • Cocoa框架的方法且方法名中含有usingBlock等时

  • GCD的API

Block的副本:

Block的类 Block原区域 复制效果
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteMallocBlock 引用计数增加

__block变量存储域

Block从栈复制到堆时对__block变量产生的影响:

__block变量的配置存储域 Block从栈复制到堆的影响
从栈复制到堆并被Block持有
被Block持有

在多个Block中使用 __block 变量时,因为最先会将所有的Block配置在栈上,所以 __block变量也会配置在栈上。在任何一个Block从栈复制到堆时,__block变量也会一并从栈复制到堆并被该Block持有。当剩下的Blcok从zhan复制到堆时,被复制的Block持有__block变量,并增加__block变量的引用计数。

截获对象

什么时候栈上的Block会复制到堆呢?

  • Block调用copy方法时

  • Block作为函数返回值返回时

  • 将Block赋值给赋有__strong修饰符id类型的类或Block类型成员变量时

  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

Block循环引用

一个循环引用是这样子的:对象持有Block,Block又持有对象。这样,对象的dealloc方法就无法执行,造成了循环引用。

通常,我们会使用__weak修饰符声明一个弱引用的变量,来避免循环引用。在确定变量一定会被释放的情况下,也可以使用__unsafe_unretained修饰符。

来看一下一些这段代码:

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
typedef void(^MyBlock)(void);

@interface MyObject : NSObject

@end

@implementation MyObject
{
    MyBlock blk;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        __block id blockSelf = self;
        blk = ^ () {
            NSLog(@"%@", blockSelf);
            blockSelf = nil;
        };
    }
    return self;
}

- (void)execBlock {
    blk();
}

- (void)dealloc
{
    NSLog(@"dealloc");
}

@end

int main(int argc, const char * argv[]) {
    MyObject *obj = [[MyObject alloc] init];
    [obj execBlock];
    return 0;
}

obj实例持有blk,blk持有blockSelf,blockSelf持有obj。粗略一看,还是造成了循环引用。实际上,执行了[obj execBlock]后,blockSelf被赋值为nil,解除了循环引用。

所以说,使用__block修饰符也是可以解决循环引用的,但是为了避免循环引用,必须执行Block。

本文由作者按照 CC BY 4.0 进行授权