探究 block (一):block 的本质
1. 简单概述
block 是 C 语言的扩充功能,我们可以认为它是 带有自动变量的匿名函数,同时也是一个对象。
block 是一个匿名的 inline 代码集合,有如下特点:
- 参数列表,就像一个函数(看起来是个函数,执行起来像是一个函数);
- 是一个对象;
- 有声明的返回类型。
2. block怎么写
最简单的写法。
int (^DefaultBlock1)(int) = ^int (int a) {
return a + 1;
};
DefaultBlock1(1);
升级版。
// 利用 typedef 声明block
typedef return_type (^BlockTypeName)(var_type);
// 作属性
@property (nonatomic, copy ,nullable) BlockTypeName blockName;
// 作方法参数
- (void)requestForSomething:(Model)model handle:(BlockTypeName)handle;
3. block的实现
在LLVM的文件中,我找到了一份文档,Block_private.h,这里可以查看到block的实现情况
- 注:实际上真实的代码结构和使用 clang 指令转换过来的代码,是有可能不一样的。
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
结构如图所示:
里面的 invoke 就是指向具体实现的函数指针,当 block 被调用的时候,程序最终会跳转到这个函数指针指向的代码区。
而 Block_descriptor 里面最重要的就是 copy 函数和 dispose 函数,从命名上可以推断出,copy 函数是用来捕获变量并持有引用,而 dispose 函数是用来释放捕获的变量。函数捕获的变量会存储在结构体 Block_layout 的后面,在 invoke 函数执行前全部读出。
不过光看文档并不直观。我们使用 clang -rewrite-objc 将一份 block 代码进行编译转换,将得到一份C++代码。刨除其他无用的代码:
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) {
}
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[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
(void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
}
return 0;
}
先看最直接的 __block_impl 代码,
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
这是一个结构体,里面的元素分别是
- isa:
指向所属类的指针,也就是 block 的类型- flags
标志变量,在实现 block 的内部操作时会用到- Reserved
保留变量- FuncPtr
block 执行时调用的函数指针
接着, __main_block_impl_0 因为包含了 __block_impl ,我们可以将它打开,直接看成
__main_block_impl_0{
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0 *Desc;
}
通过观察它,我们可以将 block 理解为,一个对象,内部包含一个函数。
4. block的类型
我们常见的block是有三种:
- __NSGlobalBlock
- __NSStackBlock
- __NSMallocBlock
比如说
void (^block)(void) = ^{
NSLog(@"biboyang");
};
block();
或者
static int age = 10;
void(^block)(void) = ^{
NSLog(@"Hello, World! %d",age);
};
block();
像是这种,没有对外捕获变量的,就是 GlobaBlock 。
而我们在写一个捕获变量的。
int b = 10;
void(^block2)(void) = ^{
NSLog(@"Hello, World! %d",b);
};
block2();
这种 block,在 MRC 中,是 StackBlock 。在 ARC 中,因为编译器做了优化,自动进行了 copy ,这种就是 MallocBlock 了。 虽然在 ARC 中 strong 和 copy 均可正确管理 Block 内存,但 Apple 官方推荐使用 copy 以明确语义,同时兼容 MRC 历史代码(但是现在已经很难找到使用 MRC 的项目了)。
做这种优化的原因很好理解:
如果 StackBlock 访问了一个自动变量,因为自己是存在栈上的,所以变量也就会被保存在栈上。但是因为栈上的数据是由系统自动进行管理的,随时都有可能被回收,非常容易造成野指针的问题。
那该如何解决呢?复制到堆上就好了!
ARC 机制也确实这么做的。它会自动将栈上的 block 复制到堆上,所以,ARC 下的 block 的属性关键词其实使用 strong 和 copy 都不会有问题,不过为了习惯,还是使用 copy 为好。
Blcok 的类 | 副本源的配置存储域 | 复制效果 |
---|---|---|
__NSStackBlock | 栈 | 堆 |
__NSGlobalBlock | 程序的数据区域 | 无用 |
__NSMallocBlock | 堆 | 引用计数增加 |
系统默认调用 copy 方法把 block 复制的四种情况
- 手动调用 copy
- block 是函数的返回值
- block 被强引用,block 被赋值给 __strong 或者 id 类型
- 调用系统 API 入参中含有 usingBlcok 的 Cocoa 方法或者 GCD 的相关 API
ARC 环境下,一旦 block 赋值就会触发 copy,block 就会 copy 到堆上,block也就会变成 __NSMallocBlock 。当然,如果刻意的去写(没有实际用处),ARC 环境下也是存在 __NSStackBlock 的,这种情况下,block 就在栈上。
从报错看内存
如果我们把 block 设置为 nil ,然后去调用,会发生什么?
void (^block)(void) = nil;
block();
当我们运行的时候,它会崩溃,报错信息为 Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)。
我们可以发现,当把 block 置为 nil 的时候,第四行的函数指针,被置为 NULL ,注意,这里是 NULL 而不是 nil 。
我们给一个对象发送 nil 消息是没有问题的,但是给如果是 NULL 就会发生崩溃。
- nil:指向oc中对象的空指针
- Nil:指向oc中类的空指针
- NULL:指向其他类型的空指针,如一个c类型的内存指针
- NSNull:在集合对象中,表示空值的对象
- 若obj为 nil:[obj message] 将返回NO,而不是NSException
- 若obj为 NSNull:[obj message] 将抛出异常NSException
它直接访问到了函数指针,因为前三位分别是 void、int、int,大小分别是 8、4、4,加一块就为 16 ,所以在 64 位中,就表示出 0x10 地址的崩溃。
Block_layout 内存布局(64位系统):
isa (8字节) | flags (4) | reserved (4) | invoke (8) | descriptor (8) | variables...
访问 invoke 的地址为 Block_layout 起始地址 + 16 字节(0x10)
如果是在 32 位的系统中,void 的大小是 4,崩溃的地址应该就是 0x0c。
引用
Blocks Programming Topics - Apple Developer
Working with Blocks
fuckingblocksyntax.com