iOS 知识点 - Block 讲解
核心概念
- 本质:带有 “自动变量捕获功能” 的匿名函数(闭包)。
Block 与其他概念的对比:
| 概念 | 特点 | 是否捕获环境 |
|---|---|---|
| C 函数指针 | 纯函数地址 | ❌否 |
| Block | 函数 + 捕获的上下文 | ✅是 |
| Objective-C 方法 | 对象方法,隐含 self | 通过 self 间接访问 |
类继承链:
NSObject
└── NSBlock (私有基类)
├── __NSGlobalBlock__ (全局 Block)
├── __NSStackBlock__ (栈 Block)
└── __NSMallocBlock__ (堆 Block)
基本语法:
// 完整形式
返回值类型 (^Block名称)(参数类型列表) = ^返回值类型(参数列表) {
// 函数体
};
int (^sumBlock)(int, int) = ^int (int a, int b) {
return a + b;
};
变量捕获机制
三种 Block 类型
| 类型 | isa 指向 | 存储位置 | 特点 |
|---|---|---|---|
__NSGlobalBlock__ |
__NSGlobalBlock__(类对象) |
数据段(全局静态区) | 不捕获外部变量,生命周期=进程 |
__NSStackBlock__ |
__NSStackBlock__(类对象) |
栈 | 捕获变量,随栈帧消亡 |
__NSMallocBlock__ |
__NSMallocBlock__(类对象) |
堆 | 从栈拷贝来,引用计数管理 |
ARC 下自动 copy 规则
在 MRC 下,必须手动将栈 block 拷贝到堆上。
Block_copy(block);
Block_release(block);
在 ARC 下,编译器会根据 block 的使用场景自动将栈 block 拷贝到堆上。
- 触发条件:
- 赋值给__strong变量,会自动将栈 block 拷贝到堆上。
- 作为方法返回值,自动 copy。
- 作为 Cocoa API 回调参数 (例如:dispatch_async)。
- 放入容器(NSArray,NSDictionary)
从 🔝 看出,ARC 下大部分 block 都会变成 __NSMallocBlock__,那么在什么情况下,仍然会有 __NSStackBlock__ 呢?
// 局部作用域内,[直接调用] + [捕获外部变量] + [不满足 “触发条件”] 的 block。
void test(void) {
int x = 0;
// 默认是 __strong(触发条件),所以需要用 __weak 或 __unsafe_unretained 修饰。
__weak void (^block)(void) = ^{
NSLog(@"%d", x);
};
}
底层实现原理
举例代码:
void test() {
int a = 10;
__block int b = 10;
void (^blk)(void) = ^{
NSLog(@"a = %d", a);
b += 1;
};
blk();
}
编译后的简化结果:
struct __main_block_impl_0 {
struct __block_impl impl; // block 基础信息(isa、FuncPtr 等)
struct __main_block_desc_0* Desc; // 绑定 impl.descriptor 指针
/// 自动捕获变量
int a; // 捕获到的变量
struct __Block_byref_b *b_byref; // 捕获到的 __block 变量
};
block()调用实际是impl.FuncPtr()。
__block 修饰符
普通的变量是无法在闭包内部修改的(只读),如果需要修改变量,需要通过 __block 修饰来共享内存。
__block 变量会被包装成一个结构体:
struct __Block_byref_x {
void *__isa;
__Block_byref_x *__forwarding; // 保证 block copy 到堆后指针仍指向最新值
int __flags;
int __size;
int x; // 实际存储的值
};
-
当 block 捕获
__block变量时,并不会直接拷贝变量a的值,而是会捕获__Block_byref_x*指针,无论 block 在栈 or 堆上,访问的都是同一个__Block_byref_x→ 保证修改同步。 -
__forwarding指针作用- 当 block 从栈拷贝到堆时:
__Block_byref_x本身也会被拷贝到堆__forwarding指针更新为堆上的新地址
- block 内访问 x 的方式:
Block -> __Block_byref_x -> forwarding -> x += 1; - 当 block 从栈拷贝到堆时:
block 编译结果示例
举例代码:
void test() {
int a = 10;
__block int b = 10;
void (^blk)(void) = ^{
NSLog(@"a = %d", a);
b += 1;
};
blk();
}
编译后全部的展开结果:
////////////////////
/// 0️⃣__block 修饰的 b
////////////////////
struct __Block_byref_b {
void *__isa;
struct __Block_byref_b *__forwarding;
int __flags;
int __size;
int b; // 实际值
};
////////////////////
/// 1️⃣Block 结构体
////////////////////
struct __main_block_impl_0 {
struct __block_impl impl; // block 基础信息(isa、FuncPtr 等)
struct __main_block_desc_0* Desc; // 绑定 impl.descriptor 指针
/// 自动捕获变量
int a; // 捕获到的变量
struct __Block_byref_b *b_byref; // 捕获到的 __block 变量
};
////////////////////
/// 2️⃣_block_impl 展开
////////////////////
struct __block_impl {
void *isa; // 指向 __NSStackBlock__/__NSMallocBlock__/__NSGlobalBlock__
int flags; // 标志位,比如是否是 __block、是否捕获对象
int reserved; // 保留字段
void (*invoke)(void *, ...); // block 的执行函数指针
struct __block_descriptor *descriptor; // 描述信息(size, copy, dispose)
};
////////////////////
/// 3️⃣__block_descriptor 展开
////////////////////
struct __block_descriptor {
unsigned long reserved; // 保留字段
unsigned long size; // block 总大小(impl + 捕获变量)
void (*copy_helper)(void *dst, void *src); // copy 时调用,处理对象 / __block
void (*dispose_helper)(void *src); // 释放 block 时调用
};
////////////////////
/// 4️⃣__main_block_desc_0 展开
////////////////////
struct __main_block_desc_0 {
unsigned long reserved;
unsigned long size;
void (*copy_helper)(void *dst, void *src);
void (*dispose_helper)(void *src);
};
////////////////////
/// 5️⃣block 执行函数伪代码
////////////////////
void __main_block_invoke(struct __main_block_impl_0 *blk) {
// a 是按值拷贝
int a = blk->a;
printf("a = %d\n", a);
// b 是 __block 变量,通过 forwarding 修改
struct __Block_byref_b *b = blk->b_byref->forwarding;
b->b += 1;
}

浙公网安备 33010602011771号