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 变量可以?
1. 局部变量(基本类型)— 值捕获,修改的是副本
int base = 10;
void (^block)(void) = ^{
base = 20; // ❌ 编译错误:Variable is not assignable
};
原因:基本类型的局部变量被 Block 捕获时,编译器将变量的值拷贝到 Block 结构体的成员变量中。Block 内部访问的是结构体里的副本,修改副本不会影响外部原始变量,编译器直接禁止这种无意义的赋值。
// 编译器展开后(简化)
struct __main_block_impl_0 {
struct __block_impl impl;
int base; // 这是外部 base 的值拷贝,不是同一个变量
};
// Block 执行时
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__cself->base = 20; // 修改的只是结构体里的副本,外部 base 完全不受影响
}
核心矛盾:Block 的代码作为一个函数,与外部的局部变量处于不同的栈帧/内存区域。如果允许直接赋值,修改的只是 Block 内部的值拷贝,会让人误以为修改了外部变量。编译器选择直接报错来避免这种语义歧义。
2. 局部变量(OC 对象)— 指针被 const 化
NSObject *obj = [[NSObject alloc] init];
void (^block)(void) = ^{
obj = [[NSObject alloc] init]; // ❌ 编译错误
};
原因:OC 对象被捕获后,Block 结构体中持有的是指向该对象的指针。编译器将这个指针视为 const(不可重新赋值),防止 Block 内部改变指针的指向。
// 编译器为 Block 结构体生成的成员类似:
// __strong NSObject *const capturedObj;
// capturedObj 可以重新持有别的对象吗?不可以——const 禁止重新赋值
但注意:虽然指针本身不能被重新赋值,但对象自身的属性/状态可以被修改:
NSMutableArray *arr = [NSMutableArray array];
void (^block)(void) = ^{
[arr addObject:@1]; // ✅ OK——修改的是对象内部状态,不是指针
};
为什么 OC 对象允许修改内部状态? 因为这里改的是
*arr(指针指向的内存内容),没有改变arr(指针的值/指向)。Block 对指针做了浅层只读约束(指针不能指向别处),但没有限制对象内部的数据修改。
3. __block 变量 — 通过 "转发指针" 间接访问
__block int val = 42;
void (^block)(void) = ^{
val = 100; // ✅ 可以修改
};
原因:__block 修饰的变量不会被直接值拷贝进 Block 结构体。编译器将该变量包装成一个 __Block_byref_xxx 结构体,Block 持有的是这个结构体的指针,而不是值的拷贝。
// __block int val = 42;
// 编译器包装为:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding; // 关键:指向自己(或堆上副本)
int __flags;
int __size;
int val; // 真正的变量值
};
// Block 结构体
struct __main_block_impl_0 {
struct __block_impl impl;
__Block_byref_val_0 *val; // 持有的是 byref 结构体的指针!
};
// Block 执行时对 val 的访问变为:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // 取出 byref 结构体指针
val->__forwarding->val = 100; // 通过 __forwarding 写值
}
__forwarding 的作用:
【Block 在栈上时】
栈上 byref 结构体
┌─────────────────────┐
│ __forwarding ──────►│ 指向自己(栈上)
│ val = 42 │
└─────────────────────┘
▲
Block 结构体持有指针
【Block 被 copy 到堆后】
栈上 byref 堆上 byref(副本)
┌─────────────────────┐ ┌─────────────────────┐
│ __forwarding ────────►│ __forwarding ────► │ 指向自己(堆上)
│ val = 42 │ │ val = 42 │
└─────────────────────┘ └─────────────────────┘
▲
Block 结构体持有指针
(已更新为堆上地址)
-
Block 在栈上时,
__forwarding指向栈上的自己。 -
Block 被 copy 到堆后,
__forwarding指向堆上的副本。 -
所有对变量的读写都经过
val->__forwarding->val,保证无论 Block 在栈上还是堆上,访问到的永远是同一个值。
一句话总结:普通变量是"值拷贝",Block 改的是副本;
__block变量是"指针传递",Block 通过__forwarding指针间接访问,改的是同一块内存。OC 对象指针本身被 const 化不能重新赋值,但对象内部状态可变。
对比总结
普通局部变量(基本类型)
外部变量 → [值拷贝] → Block 结构体成员(副本,只读)
修改副本 ≠ 修改外部,编译器直接拒绝
普通局部变量(OC 对象)
外部指针 → [指针拷贝 + const] → Block 结构体成员(指针只读)
指针不能改指向,但对象内部数据可以改
__block 变量
外部变量 → [包装为 __Block_byref_xxx] → Block 持有 byref 结构体的指针
通过 __forwarding 间接读写,Block 和外部作用域共享同一块内存
__block 修饰 OC 对象的特殊语义(ARC vs MRC)
// ARC 下
__block NSObject *obj = [[NSObject alloc] init];
void (^block)(void) = ^{
obj = nil; // ✅ 可以修改指针
NSLog(@"%@", obj); // ARC 会正确处理 retain/release
};
// MRC 下
__block NSObject *obj = [[NSObject alloc] init]; // ⚠️ MRC 下 __block 变量默认不 retain
// Block copy 到堆后,__block 对象变量才被 retain(如果 Block 在堆上)
// 需要手动管理内存,容易出问题
ARC 下
__block修饰的对象变量会自动处理 retain/release,Block copy 时自动 retain 包装结构体内的对象。MRC 下__block变量默认不 retain,容易导致野指针,这也是为什么 MRC 时代__block常用于避免循环引用(因为不 retain,不会形成引用环)。
__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 引用关系检测工具