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

22 阅读11分钟

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

动手实践

  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 引用关系检测工具