Block
简介
本文主要是回答以下几个问题:
- Block 是什么?
- Block 不同的类型出现场景是什么?又分别存储在哪?
- 截获的变量是如何存储的?
- 如何解决循环引用问题?
简洁版的回答:
- Block 又称为匿名函数,本质是一个 ObjC 对象,其结构体里会有一个指针指向具体的函数实现。
- 它有 3 种类型,分别存储在静态数据区、栈区、堆区。
- 截获的变量会直接拷贝到 Block 结构体里,或捕捞其指针。
- 一般可使用 weak 和 __block 修饰符来解决循环引用问题。
接下来针对每个答案进行详细阐述。
Block 是什么?
一句话描述:能捕捞局部变量的匿名函数。
但内部实现是怎样的呢?
先写一个简单的 Block
#import <Foundation/Foundation.h>
int main() {
void (^blk)(void) = ^{ printf("BLOCK\n"); };
blk();
return 0;
}
再使用 clang 转换成 c++
clang -rewrite-objc xx.m
得到结果的关键代码如下:
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;
}
};
// 源码中的 ^{printf("BLOCK\n",);} 转换成以下代码
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() {
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 就变成:
// Block
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
};
// __block_impl 的声明
struct __block_impl {
void *isa;
int Flags; // 按 bit 保留表示一些 block 附加信息
int Reserved; // 保留变量
void *FuncPtr;
};
// __main_block_desc_0 的声明
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)};
对比一下 ObjC 对象的声明,不难发现 Block 本质也是一种 ObjC 对象。
因为它也有那标志性的 isa 指针。
// Objective-C 对象的声明
// id 为 objc_object 结构体的指针类型
typedef struct objc_object {
Class isa;
} *id;
// Class 为 objc_class 结构体的指针类型
typedef struct objc_class *Class;
struct objc_class {
Class *isa;
};
Block 的 3 种类型
Block 的 isa 指向是这 3 个类之一,其实从名字上也可以看出各自存储的区域。
__NSStackBlock__存储在栈上__NSGlobalBlock__存储在静态数据区__NSMallocBlock__堆上
各种类型出现的场景如下:
int e = 3;
void (^block)() = ^{
printf("%c\n", e);
};
NSLog(@"%@", [block class]); // __NSMallocBlock__
NSLog(@"%@", [^{ printf("%c\n", e); } class]); // __NSStackBlock__
NSLog(@"%@", [^{ int a = 4; printf("%c\n", a); } class]); // __NSGlobalBlock__
简单理解,未截获变量是 __NSGlobalBlock__ 。
若截获了变量,则是 __NSStackBlock__,但它却很容易被复制到堆上。
比如以下情况:
- 当对 Block 对象调用 copy 时。
- 当 Block 作为返回值返回时。(前提是 ARC 环境)
- 当 Block 赋值给 strong 对象时。
- 在方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时。
有时也需要手动添加 copy 方法,来将 block 添加到堆上。
根据不同的 Block,copy 会有不同效果:
| Block 类 | 副本源的配置存储域 | 复制效果 |
|---|---|---|
| _NSConcreteStackBlock | 栈 | 从栈复制到堆 |
| _NSConcreteGlobalBlock | 程序的数据区域 | Nothing |
| _NSConcreteMallocBlock | 堆 | 引用计数增加 |
截获的变量是如何存储的?
截获的变量会被添加到 __main_block_impl_0 结构体中。
大概就是这样子:
| __block_impl |
|---|
| isa * |
| Flags |
| Reserved |
| FuncPtr |
| 截获的变量 1 |
| 截获的变量 2 |
| ... |
转换后的代码:
int main() {
int count = 3;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int count; // 使用的自动变量被作为成员变量追加到结构体中
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _count, int flags=0) : count(_count) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int count = __cself->count; // bound by copy
printf("BLOCK%d\n", count);
}
__block 说明符
添加__block 之后,数据若改变了,执行 block 也会打印出最新的数据:
没有添加 __block
int n = 3;
void (^block2)() = ^{
NSLog(@"%d", n);
};
n = 4;
block2(); // 3
添加了__block,会捕获变量的指针。
所以数值改变了,block 执行时,也能看到改变后的数据。
__block int n = 3;
void (^block2)() = ^{
NSLog(@"%d", n);
};
n = 4;
block2(); // 4
具体的可以看看,转换后的代码。
增加的 __Block_byref_count_0 结构体里就捕获了变量的指针(__forwarding)。
int main() {
__attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,(__Block_byref_count_0 *)&count, 0, sizeof(__Block_byref_count_0), 3};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
// 不在 __main_block_impl_0 中声明的原因是,这样可以在多个 block 中使用
struct __Block_byref_count_0 {
void *__isa;
__Block_byref_count_0 *__forwarding;
int __flags;
int __size;
int count;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_count_0 *count; // by ref
...
}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_count_0 *count = __cself->count; // bound by ref
(count->__forwarding->count) = 2;
printf("BLOCK%d\n", (count->__forwarding->count));
}
__forwarding
__forwarding 是添加 __block 后的关键实现。
(为方便叙述,以下将 __forwarding 变量用 f 代替)
当 __block 变量在栈上时,f 指向自己本身的「count」。
当 __block 变量复制到堆上时,f 会被复制到堆上,假设为 fm。
此时栈上的 f 会变成指向堆上的「count」,而 fm 也指向堆上的「count」。
所以使用
__forwarding 变量,无论在 Block 内外,修改访问的都是堆上的变量数据。
如何解决 Block 循环引用问题
使用 Block 容易引起循环引用,主要通过修饰符 __weak 和 __block 避免。
__weak 比较常见,不再赘述。
使用 __block 的主要优点:
可自由控制将变量转为 nil 的时机
使用 __block 的主要缺点:
必须执行 Block,并在 Block 中,将变量置为 nil,才能避免循环引用。
疑问
以下是笔者在整理这篇文章时,仍有疑惑的地方,若有读者知道,麻烦告知一声。
为什么以下代码,打印出来的类型不同?
int e = 3;
NSLog(@"%@", ^{ printf("%c\n", e); }); // <__NSMallocBlock__: 0x10046b050>
NSLog(@"%@", [^{ printf("%c\n", e); } class]); // __NSStackBlock__
以下分析汇编代码的思路来源于 哈就是我26593
可以看到前一行代码,会调用 objc_retainBlock(),而后者并没有看到相关操作。
根据 objc_retainBlock — Clang 12 documentation 的解释,该函数会 copy 栈上的 block 到堆上。
至于为什么要调用它,评论区已有几位大佬给出见解,但笔者仍未找到佐证的资料😂。
那为什么要将栈上的 block 复制到堆上呢?
个人理解是,它被堆上的对象「所需要了」,也就是说,需要控制其被销毁的时机了,所以复制到堆上是为了便于管理。
感谢
谈 Objective-C block 的实现 · 唐巧的博客