Block/闭包捕获与生命周期
面试回答版
一句话概括
Block 是携带上下文的匿名函数,在 OC 中基于 _NSConcreteStackBlock/_NSConcreteMallocBlock/_NSConcreteGlobalBlock 三个类实现;核心考点是变量捕获规则、内存语义、循环引用治理。
Block 底层结构
__block_impl 结构
struct __block_impl {
void *isa; // 指向对应的 Block 类(栈/堆/全局)
int Flags; // 标志位(是否被 copy、是否有销毁辅助等)
int Reserved; // 保留
void *FuncPtr; // 函数实现指针(即 block {} 内的代码)
};
// 编译器为每个 Block 生成的结构(示例)
struct __main_block_impl_0 {
struct __block_impl impl; // 上面的基础结构
struct __main_block_desc_0* Desc; // 描述信息(大小、copy/dispose 函数等)
// 捕获的变量展开在这里
int capturedInt; // 值捕获的变量
id __strong capturedObj; // OC 对象(带所有权修饰)
};
编译器展开示例
// 源代码
int base = 10;
void (^block)(void) = ^{
NSLog(@"%d", base);
};
block();
// 编译器展开后(简化)
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int base; // 值捕获
};
// 构造函数:把 base 的值拷贝进结构体
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _base) {
base = _base; // 值拷贝
}
// 函数实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog(@"%d", __cself->base);
}
变量捕获规则
| 变量类型 | 捕获方式 | 是否在 Block 内修改 |
|---|---|---|
| 局部变量(基本类型) | 值捕获,编译时拷贝值到 Block 结构体 | 不可修改 |
| 局部变量(OC 对象) | 带所有权的指针捕获(strong/weak/copy) | 不可修改 |
__block 局部变量 | 指针捕获,包装为 __Block_byref_xxx 结构体 | 可修改 |
| 静态变量 | 指针捕获(捕获地址) | 可修改 |
| 全局变量 | 不捕获,直接访问 | 可修改 |
| 实例变量 (self->xxx) | 捕获 self(self 是隐式参数,对象类型) | 通过 self 访问 |
__block 修饰的变量底层
// __block int val = 42;
// 被编译器包装为:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding; // 关键:指向自己(栈上指向自己,copy 后指向堆上的副本)
int __flags;
int __size;
int val; // 真正的变量
};
- 栈上 Block 中的
__forwarding指向栈上自己。 - Block 被 copy 到堆后,
__forwarding指向堆上的副本。 - 无论 Block 在栈上还是堆上,通过
val.__forwarding->val总能访问到正确值。
Block 的三种类型
// 1. _NSConcreteGlobalBlock(全局 Block)
// 不捕获外部变量或只捕获全局/静态变量,存储在 __TEXT,__const 段
void (^globalBlock)(void) = ^{ NSLog(@"global"); };
// isa = &_NSConcreteGlobalBlock
// 2. _NSConcreteStackBlock(栈 Block)
// 捕获了外部变量,存储在栈区
int a = 10;
void (^stackBlock)(void) = ^{ NSLog(@"%d", a); };
// 默认 isa = &_NSConcreteStackBlock
// 3. _NSConcreteMallocBlock(堆 Block)
// 栈 Block 被 copy 到堆上
void (^mallocBlock)(void) = [stackBlock copy];
// 或者 Block 属性声明为 copy 时自动触发
// isa = &_NSConcreteMallocBlock
栈 Block 什么时候会被 copy 到堆?
| 触发场景 | 说明 |
|---|---|
手动调用 [block copy] | 显式 copy |
Block 作为 @property (copy) 赋值 | 属性 setter 自动 copy |
| Block 作为方法/函数返回值(ARC) | 编译器自动 copy |
Block 赋值给 __strong 变量(ARC) | 编译器自动 copy |
| Block 作为 GCD / UIView 动画等参数 | API 内部会自动 copy |
MRC 下:Block 默认在栈上,必须手动 copy。属性需用
copy而非retain。 ARC 下:Block 作为参数传递或赋值给 strong 变量时会自动 copy,但属性仍需声明为copy(文档规范 + 与 MRC 兼容)。
Block 生命周期总结
栈 Block
│
├─ 作用域结束 → 自动释放(栈空间回收)
│
└─ 被 copy(ARC 下赋值给 strong 变量 / 手动 copy)
│
├─ 堆 Block
│ ├─ 持有捕获的 OC 对象(retain 语义)
│ ├─ 三次 release 后 dealloc(与普通 OC 对象一致)
│ └─ 对于 __block 变量:将 __forwarding 指向堆上副本
│
└─ __block 变量也从栈搬到堆,变成堆上的 __Block_byref_xxx
循环引用 4 种场景
1. Block 属性直接捕获 self
// 错误:self → block → self(循环)
self.block = ^{
[self doSomething];
};
// 正确:[weak self]
__weak typeof(self) weakSelf = self;
self.block = ^{
typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf doSomething];
};
2. NSTimer target 强引用
// 错误:self → timer → target(self),加上 RunLoop 持有 timer
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(tick) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
// 修复:使用 block-based API
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf tick];
}];
3. Delegate 强引用
// 错误:对象 → delegate → 对象
@property (nonatomic, strong) id<SomeDelegate> delegate;
// 修复:delegate 应为 weak
@property (nonatomic, weak) id<SomeDelegate> delegate;
4. 多层 Block 嵌套
// 每层都要 weak-strong dance
self.networkService.fetchData { result in
[weakSelf processData:result] {
[weakSelf updateUI];
};
};
weak/strong dance 的必要性
// 典型场景:网络回调
service.fetchData { [weak self] result in
guard let self else { return }
// 在 DispatchQueue.main.async 之前 self 不会被释放
DispatchQueue.main.async {
self.updateUI() // 这里 self 是 strong 的
}
}
为什么不是全程用 weak?
- 如果全程 weak,回调执行过程中 self 可能被提前释放,UI 更新到一半被中断。
- weak → strong 保证回调执行的完整周期内对象存活。
guard let self后的self是 strong 的,仅此闭包内有效。
高频追问清单
| 问题 | 关键要点 |
|---|---|
__block 的作用是什么? | 让 Block 可以修改捕获的变量,底层包装为 __Block_byref_xxx 结构体 |
Block 属性为什么用 copy? | 栈 Block 出作用域即释放,copy 到堆保证 Block 在赋值后持续有效 |
| 栈 Block 何时被 copy 到堆? | ARC 下赋值给 strong 变量、作为返回值、GCD/动画等 API 参数传递时 |
_NSConcreteStackBlock 和 _NSConcreteMallocBlock 的区别? | 前者在栈上,后者在堆上;堆 Block 管理捕获对象的引用计数 |
__forwarding 的作用? | 保证 Block 无论是在栈上还是堆上,都能访问到正确的 __block 变量值 |
| Block 捕获实例变量会怎样? | 通过捕获 self 间接访问,所以需要在 Block 外先用 weakSelf 捕获 |
| Swift 闭包和 OC Block 的关系? | OC Block 是 __block_impl 结构体,Swift 闭包是函数 + 上下文对象;语义类似但底层不同 |
| 怎么避免 Block 导致的循环引用? | 弱引用捕获 + 回调内强引用(weak-strong dance) |
项目落地版
场景 1:网络回调更新 UI(标准模式)
final class FeedListViewController: UIViewController {
private let service = FeedService()
func loadData() {
service.fetchFeed { [weak self] result in
guard let self else { return }
switch result {
case .success(let items):
self.render(items)
case .failure(let error):
self.showError(error)
}
}
}
}
场景 2:异步编排(串行多次回调)
service.login { [weak self] token in
guard let self else { return }
self.service.fetchProfile(token: token) { [weak self] profile in
guard let self else { return }
self.service.fetchAvatar(url: profile.avatarURL) { [weak self] image in
guard let self else { return }
self.updateUI(profile: profile, avatar: image)
}
}
}
多层嵌套时可考虑引入 Promise/async-await 来 flatten。
场景 3:动画完成回调
UIView.animate(withDuration: 0.3) { [weak self] in
self?.view.alpha = 0
} completion: { [weak self] _ in
guard let self else { return }
self.view.removeFromSuperview()
self.didDismiss()
}
场景 4:自定义 Block 回调 API(安全设计)
typealias Completion<T> = (Result<T, Error>) -> Void
protocol AsyncOperation {
associatedtype Output
func execute(completion: @escaping Completion<Output>)
func cancel()
}
final class DataLoader: AsyncOperation {
typealias Output = Data
private var isCancelled = false
func execute(completion: @escaping Completion<Data>) {
DispatchQueue.global().async { [weak self] in
guard let self, !self.isCancelled else { return }
// 执行加载...
DispatchQueue.main.async {
completion(.success(data))
}
}
}
func cancel() { isCancelled = true }
}
场景 5:__block 用于计数/取消
// 传统方式:用 __block 标记已完成个数
__block NSInteger completedCount = 0;
for (NSInteger i = 0; i < 3; i++) {
[self asyncTaskWithIndex:i completion:^(id result) {
@synchronized(self) { completedCount++; }
if (completedCount == 3) {
NSLog(@"所有任务完成");
}
}];
}
学习路径与优先级
初级(P0)— 会写会避坑
- 理解 Block 的基本语法和回调写法
- 理解
[weak self]和guard let self的作用 - 能解释什么是循环引用、什么场景会触发
- 知道 Block 属性要用
copy声明 - 能写出标准的 weak-strong dance 模式
自检:
- 写出一个会发生循环引用的 Block 示例
- 写出对应的修复方法
中级(P0)— 理解底层原理
- 理解 Block 的
__block_impl结构 - 掌握三种 Block 类型及转换时机
- 理解
__block的底层结构(__Block_byref_xxx+__forwarding) - 理解 ARC 下自动 copy 的触发条件
- 掌握
__block在 MRC 和 ARC 下的内存语义差异
动手实践:
- 用
clang -rewrite-objc展开 Block 看底层结构 - 在 MRC 环境下验证栈 Block 被 copy 前后的变化
- 实现一个包含多层回调的模拟网络请求,分析每一层的引用关系
高级(P1)— 设计回调 API
- 能设计带取消、线程语义、超时的异步回调 API
- 理解 Combine / async-await 与 Block 回调的关系
- 能为团队沉淀 Block 使用规范(命名、内存、线程)
- 了解 Promise/Future 模式和 Block 回调的关系
实战项目:
- 实现一个轻量级 Promise 模式封装异步回调
- 重构多层嵌套 Block 为 async-await(Swift 5.5+)
- 设计一套可视化 Block 引用关系检测工具