心辰·Dev

Block 底层探秘

Block底层实现

运用clang -rewrite-objc命令可以把 Objective-C 代码转换为 C/C++ 代码,从而展示 Block 的底层实现。以下是一个最普通的 Block 代码示例:

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

经过clang -rewrite-objc的转换,可以得到 Block 结构体的声明:

1
2
3
4
5
6
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

具体到名为 blk 的 Block, 它的结构体实现如下:

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

其中在构造函数中初始化__main_block_impl_0的成员变量。注意到 impl 结构体的 isa 变量指向_NSConcreteStackBlock,此__main_block_impl_0结构体相当于基于objc_object结构体的类对象结构体,说明该 Block 是一个位于栈上的对象。Block 的实质就是对象。
当 Block 是全局变量时,isa 会指向_NSConcreteGlobalBlock,当 Block 被 copy 到堆(heap)时,isa 会指向 _NSConcreteMallocBlock
另外,Block 里面包含的匿名函数被转换为一个静态函数,实现如下:

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

结构体__main_block_desc_0包含了该 Block 的大小:

1
2
3
4
5
6
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
// 初始化 __main_block_desc_0
__main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

综合以上的实现,main 函数中的 Objc 代码被转换为:

1
2
3
4
5
6
7
8
9
int main(int argc, const char * argv[]) {
// 去掉一系列类型转换,相当于
// void (^blk)(void) = ^{printf("Block\n");};
void (*blk)
(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
// blk();
// (*blk->impl.FuncPtr)
(blk);

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}

截获自动变量

待转换的 Objective-C 代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, const char * argv[]) {
int dmy = 250;
int val = 10;
const char *hhh = "val = %d\n";

void (*blk)(void) = ^{
printf(hhh, val);
};
blk();
return 0;
}

经过转换之后,__main_block_impl_0结构体对比上一节的实现有所变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;

const char *hhh;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_hhh, int _val, int flags=0) : hhh(_hhh), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

Block 中使用的局部变量加入到了结构体的成员变量中,只捕捉了实际使用到的变量。
Block 匿名函数转换为如下所示的函数:

1
2
3
4
5
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *hhh = __cself->hhh;
int val = __cself->val;
printf(hhh, val);
}

__main_block_impl_0中的成员变量给函数中的局部变量赋值,所以函数中使用的变量值始终等于声明 Block 时捕获到的该变量的值。因为 C 语言不支持a[] = b[],在 Block 中无法使用 C 语言数组。如果在 Block 中对局部变量进行赋值,因为 Block 不写回对局部变量的修改,会导致一个编译期错误。
最后 main 函数中的代码被转换为:

1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
int dmy = 250;
int val = 10;
const char *hhh = "val = %d\n";

void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, hhh, val));
((void (*)
(__block_impl *))((__block_impl *)blk)
->FuncPtr)
((__block_impl *)blk);
return 0;
}

说明 Block 构造时传入的参数是 Block 声明时还未改变值的局部变量。

截获静态变量

如果在 Block 中使用了静态变量,__main_block_impl_0中会增加一个成员变量,用来保存指向该静态变量的指针。在 Block 匿名函数的实现中,使用这个指针变量操作值的改变和赋值。在声明带有静态变量的 Block 时,__main_block_impl_0的构造函数通过传递指针实现对静态变量的捕获。如下:

1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
static int sta_val = 253;

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

((void (*)
(__block_impl *))((__block_impl *)blk)
->FuncPtr)
((__block_impl *)blk);
return 0;
}

__block 变量

在变量捕获之后,对于自动变量,不能再改变其值。如果想改变变量的值,可以考虑使用静态变量或全局变量,实际通过操作指针来改变其值。如果不想使用静态变量,__block 变量可以成为另一个选择。
如果捕获了 __block 变量,代码转换后会生成一个新的 __block 变量结构体,如下所示:

1
2
3
4
5
6
7
8
struct __Block_byref_block_val_0 {
void *__isa;
__Block_byref_block_val_0 *__forwarding;
int __flags;
int __size;
// 存储变量
int block_val;
};

其中成员变量block_val存储了 __block 变量的值,__forwarding指针指向自身。在 Block 被 copy 到堆上后,它指向的是堆上的 __block 变量结构体实例。这样可以在 Block 之内和之外访问 __block 变量并修改。
在主体 Block 的实现结构体中,增加block_val成员变量保存指向 __block 变量的指针:

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;
__Block_byref_block_val_0 *block_val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_block_val_0 *_block_val, int flags=0) : block_val(_block_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

最终在 main 函数中,__block 变量被转换为__Block_byref_block_val_0结构体在栈上的一个实例。

1
2
// __block int block_val = 250;
__attribute__((__blocks__(byref))) __Block_byref_block_val_0 block_val = {(void*)0,(__Block_byref_block_val_0 *)&block_val, 0, sizeof(__Block_byref_block_val_0), 250};

与 __block 变量类似,Block 在捕获对象时,也会在主 Block 结构体中生成一个 id 类型的成员变量,保存捕获的对象。同时会生成__main_block_copy_0()__main_block_dispose_0()两个静态函数。__main_block_copy_0()在 Block 拷贝到堆上时调用,作用是把目标对象 dst 中的 block_val 赋给 Block 结构体中的成员变量 block_var,并使其持有该对象。数字3表示捕获的变量是一个普通对象,数字8表示捕获的变量是一个 __block 变量。__main_block_dispose_0在 Block 在堆上被销毁时调用,作用是释放 Block 结构体中的成员变量 block_var 持有的对象。

1
2
3
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*/);}

__main_block_desc_0结构体增加了两个函数指针类型的成员变量,分别指向 copy 和 dispose 函数。

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

消除循环引用

  • 捕获self引起循环引用。
    当 Block 从栈上复制到堆上时,捕获的strong对象将被 Block 持有。如果该对象再持有 Block, 就会引起循环引用的问题,两者内存将无法正常释放。这种内存泄漏常见的出现场景就是某个类持有 Block, 而 Block 中又捕获了强引用的self对象。
    解决方式即把self赋值给一个用__weak修饰符声明的weakSelf变量,并在 Block 中使用该weakSelf变量。当 Block 存在时,self对象始终存在,因此不需判断被赋值后的weakSelf是否为空。

    1
    2
    3
    4
    5
    6
    - (id)init {
    self = [super init];
    id __weak weakSelf = self;
    Blk blk0 = ^{NSLog(@"self = %@", weakSelf);};
    return self;
    }

    如果需要在 Block 中多次使用weakSelf,最好在 Block 里先将weakSelf赋给一个__strong修饰的strongSelf,防止weakSelf提前 nil out。这个技巧被称为”strong weak dance”,在 AFNetworking 源码中有所体现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    __weak __typeof(self)weakSelf = self;
    AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
    __strong __typeof(weakSelf)strongSelf = weakSelf;

    strongSelf.networkReachabilityStatus = status;
    if (strongSelf.networkReachabilityStatusBlock) {
    strongSelf.networkReachabilityStatusBlock(status);
    }

    };
  • __block 变量引起循环引用
    当某类Obj的对象持有 Block, Block 中又持有 __block 变量,__block 变量持有Obj对象,这样会造成循环引用。如图:
    __block 变量循环引用实例
    为了消除循环引用,在 Block 函数中通过将nil赋值给 __block 变量,可以让 __block 变量释放持有的Obj对象,即达成目标。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    -(id)init {
    self = [super init];
    __block id tmp = self;
    Blk blk0 = ^{
    NSLog(@"self = %@", tmp);
    tmp = nil;
    };
    return self;
    }

使用 __block 变量的好处

  • 可以通过控制 __block 变量来控制对象的生命周期。
  • 在不支持 __weak 修饰符的更早的 OS 版本,用 __block 替代 __unsafe_unretained 修饰符以避免迷途指针(dangling pointer)的问题。