1. Block 内存布局基础解析
在 Objective-C 和 C 语言混合编程中,Block 是一种特殊的语法结构,它本质上是一个匿名函数,可以捕获其定义时的上下文变量。理解 Block 的内存布局对于深入掌握其工作原理至关重要。
Block 在内存中的布局遵循特定的结构,这个结构在 LLVM 的 Block-ABI-Apple.rst 文档中有明确定义。一个典型的 Block 内存布局包含以下几个关键部分:
- isa 指针:表明 Block 也是一个 Objective-C 对象
- flags:各种标志位,包含 Block 的类型和行为特征
- reserved:保留字段
- invoke:函数指针,指向 Block 要执行的代码
- descriptor:描述 Block 的结构体指针
- 捕获的变量:Block 定义时捕获的上下文变量
让我们通过一个简单的例子来理解这个结构:
objective-c复制int capturedValue = 42;
void (^simpleBlock)(void) = ^{
NSLog(@"Captured value: %d", capturedValue);
};
这个简单的 Block 捕获了一个整型变量 capturedValue。在内存中,它会被编译成一个结构体,其布局大致如下:
code复制struct __block_impl {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct __block_descriptor *descriptor;
// 捕获的变量
int capturedValue;
};
2. Block 类型与内存管理
2.1 Block 的三种类型
根据 Block 的创建位置和生命周期,Block 可以分为三种类型:
- NSStackBlock:创建在栈上的 Block
- NSGlobalBlock:全局的 Block
- NSMallocBlock:创建在堆上的 Block
2.1.1 NSStackBlock
当 Block 被创建时,默认是在栈上分配的。栈上的 Block 有以下特点:
- 生命周期与创建它的函数相同
- 当函数返回时,栈上的 Block 会被自动释放
- 不能在其他作用域中安全使用
objective-c复制void (^stackBlock)(void);
void createStackBlock() {
int localValue = 10;
stackBlock = ^{
NSLog(@"Local value: %d", localValue);
};
// stackBlock 现在是 NSStackBlock
}
注意:在 ARC 环境下,编译器会自动将栈上的 Block 复制到堆上,所以实际观察到的可能是 NSMallocBlock。
2.1.2 NSGlobalBlock
全局 Block 有以下特征:
- 不捕获任何外部变量
- 生命周期与程序相同
- 存储在程序的全局数据区
objective-c复制void (^globalBlock)(void) = ^{
NSLog(@"This is a global block");
};
// globalBlock 是 NSGlobalBlock
2.1.3 NSMallocBlock
堆上的 Block 有以下特点:
- 通过 copy 操作从栈 Block 创建
- 需要手动管理内存(在 MRC 环境下)
- 可以在多个作用域中安全使用
objective-c复制void (^heapBlock)(void);
void createHeapBlock() {
int localValue = 20;
heapBlock = [^{
NSLog(@"Local value: %d", localValue);
} copy];
// heapBlock 现在是 NSMallocBlock
}
2.2 ARC 对 Block 类型的影响
在 ARC 环境下,编译器会自动处理 Block 的内存管理,这会导致 Block 类型的自动转换:
- 当 Block 被赋值给 strong 引用时,会自动执行 copy 操作
- 从栈 Block 转换为堆 Block
- 全局 Block 的 copy 操作会直接返回自身
objective-c复制void (^arcBlock)(void);
void testARCBlock() {
int value = 30;
arcBlock = ^{
NSLog(@"Value: %d", value);
};
// 在 ARC 下,arcBlock 会自动变为 NSMallocBlock
}
3. Block 描述符与标志位详解
3.1 Block 描述符结构
Block 描述符(Block Descriptor)是一个重要的结构体,它包含了 Block 的元信息:
c复制struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy_helper)(void *dst, void *src); // 可选
void (*dispose_helper)(void *src); // 可选
};
- reserved:保留字段,通常为 0
- size:整个 Block 结构体的大小
- copy_helper:当 Block 被复制时调用的辅助函数(可选)
- dispose_helper:当 Block 被释放时调用的辅助函数(可选)
3.2 Block 标志位解析
Block 的标志位(flags)包含了关于 Block 行为的重要信息。以下是主要的标志位定义:
| 标志位 | 值 | 描述 |
|---|---|---|
| BLOCK_IS_NOESCAPE | (1 << 23) | Block 是非逃逸的 |
| BLOCK_HAS_COPY_DISPOSE | (1 << 25) | 描述符包含 copy/dispose 辅助函数 |
| BLOCK_HAS_CXX_OBJ | (1 << 26) | Block 捕获了 C++ 对象 |
| BLOCK_IS_GLOBAL | (1 << 28) | Block 是全局的 |
| BLOCK_USE_STRET | (1 << 29) | 已废弃的标志位 |
| BLOCK_HAS_SIGNATURE | (1 << 30) | Block 有方法签名 |
| BLOCK_HAS_EXTENDED_LAYOUT | (1u << 31) | Block 有捕获的变量 |
3.2.1 BLOCK_HAS_COPY_DISPOSE
当 Block 捕获了需要特殊处理的对象时,这个标志位会被设置。主要有两种情况:
- 捕获了 Objective-C 对象:
objective-c复制NSObject *obj = [NSObject new];
void (^blockWithObj)(void) = ^{
NSLog(@"%@", obj);
};
- 捕获了其他 Block:
objective-c复制void (^innerBlock)(void) = ^{};
void (^blockWithBlock)(void) = ^{
innerBlock();
};
3.2.2 BLOCK_HAS_CXX_OBJ
当 Block 捕获了 C++ 对象时,这个标志位会被设置:
objective-c复制class MyCppClass {
public:
int value;
};
MyCppClass cppObj;
void (^blockWithCpp)(void) = ^{
NSLog(@"C++ value: %d", cppObj.value);
};
3.2.3 BLOCK_REFCOUNT_MASK
flags 的低 16 位用于存储 Block 的引用计数:
c复制#define BLOCK_REFCOUNT_MASK (0xffff)
可以通过以下方式获取引用计数:
c复制int refCount = flags & BLOCK_REFCOUNT_MASK;
4. Block 的变量捕获机制
4.1 变量捕获的基本规则
Block 对变量的捕获遵循以下规则:
- 全局变量:不会被捕获,直接访问
- 静态变量:不会被捕获,直接访问
- 自动变量(auto):会被捕获,值拷贝
- __block 变量:会被捕获,引用传递
objective-c复制int globalVar = 1; // 全局变量,不捕获
static int staticVar = 2; // 静态变量,不捕获
void testCapture() {
int autoVar = 3; // 自动变量,值捕获
__block int blockVar = 4; // __block 变量,引用捕获
static int localStatic = 5; // 局部静态变量,不捕获
void (^block)(void) = ^{
NSLog(@"%d, %d, %d, %d, %d",
globalVar, staticVar, autoVar,
