三.iOS Block/闭包捕获与生命周期

4 阅读7分钟

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 而非 retainARC 下: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 下的内存语义差异

动手实践

  1. clang -rewrite-objc 展开 Block 看底层结构
  2. 在 MRC 环境下验证栈 Block 被 copy 前后的变化
  3. 实现一个包含多层回调的模拟网络请求,分析每一层的引用关系

高级(P1)— 设计回调 API

  • 能设计带取消、线程语义、超时的异步回调 API
  • 理解 Combine / async-await 与 Block 回调的关系
  • 能为团队沉淀 Block 使用规范(命名、内存、线程)
  • 了解 Promise/Future 模式和 Block 回调的关系

实战项目

  1. 实现一个轻量级 Promise 模式封装异步回调
  2. 重构多层嵌套 Block 为 async-await(Swift 5.5+)
  3. 设计一套可视化 Block 引用关系检测工具