前言
Block 是 Objective-C 开发中非常常见的一种语法。网络请求回调、按钮点击、动画完成通知、GCD 异步任务和数组排序,都可能使用 Block。
很多人第一次接触 Block 时会觉得它只是“可以保存起来、以后执行的一段代码”。这个理解可以帮助入门,但面试还会继续追问:Block 如何保存外部变量?变量什么时候复制?为什么 Block 属性通常用 copy?__block 和 __weak 有什么区别?如何解决循环引用?
本文从最基础的使用方式开始,逐步解释 Block 的本质、变量捕获、三种类型、内存管理和循环引用,并把常见面试题串联起来。
一、Block 是什么
1. 先看一个普通函数
普通函数可以把一段逻辑封装起来,调用时执行:
void sayHello(void) {
NSLog(@"Hello");
}
sayHello();
这个函数有一个特点:函数代码和调用环境是分开的。函数本身不会自动保存某个局部变量的值。
2. Block 的基本写法
Block 可以把一段代码保存成一个值,之后再调用:
void (^sayHello)(void) = ^{
NSLog(@"Hello");
};
sayHello();
拆开看:
void (^sayHello)(void)
- 第一个
void表示 Block 的返回值类型。 ^表示这是一个 Block 类型。sayHello是变量名。- 最后的
(void)表示 Block 不接收参数。
带参数和返回值的例子:
int (^sum)(int, int) = ^int(int a, int b) {
return a + b;
};
int result = sum(2, 3);
如果编译器可以推断类型,也可以省略返回值类型:
int (^sum)(int, int) = ^(int a, int b) {
return a + b;
};
3. Block 的本质
面试中可以先给出一句准确但容易理解的话:
Block 本质上是一个 Objective-C 对象,它封装了函数调用以及调用时需要的环境。
说它是对象,是因为 Block 内部也有类似 isa 的信息,可以使用 class 方法查看它的运行时类型。说它封装了环境,是因为 Block 会把需要在内部使用的外部变量保存下来。
因此,Block 不只是一个函数指针。函数指针只保存代码地址,而 Block 还可能保存:
- 函数实现地址
- 被捕获的外部变量
- 描述 Block 大小和复制、销毁行为的信息
- 运行时管理 Block 所需的其他标记
二、Block 是怎样被调用的
下面的代码包含三步:创建 Block、保存 Block、调用 Block。
void (^printName)(NSString *) = ^(NSString *name) {
NSLog(@"%@", name);
};
printName(@"Jiayi");
从概念上看,编译器会把它转换成类似下面的结构。真实结构由编译器和运行时决定,以下代码只用于帮助理解:
struct BlockImpl {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
void *descriptor;
// 这里还可能有捕获的变量
};
isa让 Block 具有对象特征。invoke指向真正执行 Block 内容的函数。descriptor指向描述信息,例如 Block 大小和复制、销毁函数。- 如果 Block 捕获了外部变量,这些变量会成为 Block 结构的一部分,或者由 Block 结构间接指向。
面试回答
问:Block 的原理是什么?本质是什么?
可以回答:
Block 本质上是一个封装了函数调用和调用环境的 Objective-C 对象。编译器会把 Block 转换成结构体,结构体中包含
isa、调用函数指针、描述信息,以及捕获的外部变量。调用 Block 时,本质上是执行它保存的函数实现。
三、为什么 Block 可以访问外部变量
Block 有一个变量捕获机制。它会分析 Block 内部使用了哪些外部变量,并以不同方式保存它们。
int age = 20;
void (^block)(void) = ^{
NSLog(@"age = %d", age);
};
block();
这里的 age 是 Block 外部定义的局部变量。Block 能在稍后执行时仍然访问它,是因为编译器捕获了 age 的值。
1. auto 局部变量:值传递
普通局部变量默认是 auto 变量。Block 捕获它时,通常会把变量当前的值复制一份到 Block 内部:
int age = 20;
void (^block)(void) = ^{
NSLog(@"%d", age);
};
age = 30;
block(); // 通常输出 20
这里要注意两件事:
- Block 捕获的是变量当时的值,不是外部变量本身。
- 如果 Block 捕获的是对象类型的局部变量,复制的是对象指针,但 ARC 下会按照捕获方式管理对象的引用关系。
2. static 局部变量:指针传递
局部 static 变量的生命周期贯穿整个程序运行过程。Block 捕获它时,通常通过指针访问同一个变量:
static int age = 20;
void (^block)(void) = ^{
NSLog(@"%d", age);
};
age = 30;
block(); // 输出 30
3. 全局变量:直接访问
全局变量的生命周期也贯穿程序运行过程。Block 不需要把它复制到内部,可以直接访问:
int globalAge = 20;
void (^block)(void) = ^{
NSLog(@"%d", globalAge);
};
4. 捕获规则速记
| 变量类型 | Block 是否捕获 | 典型访问方式 |
|---|---|---|
普通局部变量 auto | 是 | 捕获值 |
局部静态变量 static | 是 | 保存地址,通过指针访问 |
| 全局变量 | 否 | 直接访问 |
四、__block 到底有什么作用
1. 解决什么问题
普通局部变量被 Block 捕获后,Block 内部拿到的是它的一份值,默认不能直接修改外部变量:
int count = 0;
void (^block)(void) = ^{
// count = 1; // 编译错误
};
使用 __block 后,编译器会把变量包装成一个可共享的 byref 结构,Block 和外部代码都可以访问这个共享变量:
__block int count = 0;
void (^block)(void) = ^{
count = 1;
};
block();
NSLog(@"%d", count); // 1
所以,__block 的核心作用是:
允许 Block 内部修改局部变量,并让 Block 与外部代码共享这个变量。
2. __block 不能修饰什么
__block 主要用于局部变量,不能用于全局变量和已经是 static 的变量:
__block int globalValue; // 不能这样使用全局变量
static __block int staticValue; // 不能这样组合使用
3. __block 的底层结构
为了让变量可以在 Block 和原作用域之间共享,编译器会生成类似这样的 byref 结构:
struct BlockByrefCount {
void *isa;
struct BlockByrefCount *forwarding;
int flags;
int size;
int count;
};
关键是 forwarding 指针。变量最开始可能位于栈上,Block 被复制到堆上后,byref 变量也可能被复制到堆上。原来的结构会通过 forwarding 指向堆上的新结构,这样无论从栈上的 Block 还是堆上的 Block 访问,都能找到当前有效的变量。
开发者通常不需要手动操作这些结构。面试时理解它们的意义即可:__block 不是简单加了一个关键字,而是改变了变量的存储方式。
面试回答
问:__block 的作用是什么?
可以回答:
__block用于解决 Block 内部不能修改普通局部变量的问题。编译器会把__block变量包装成 byref 结构,Block 和外部代码通过同一份存储访问它。它也会影响变量的内存管理,但它本身不等于weak,也不能自动解决循环引用。
五、修改 NSMutableArray 需要 __block 吗
这是一个非常典型的面试题,关键在于区分“修改对象内容”和“修改变量指向”。
1. 修改数组内容,不需要 __block
NSMutableArray *array = [NSMutableArray array];
void (^block)(void) = ^{
[array addObject:@"A"];
};
block();
这里没有给 array 变量重新赋值,只是向它指向的可变数组发送 addObject: 消息。因此不需要 __block。
2. 修改数组变量本身,需要 __block
__block NSMutableArray *array = [NSMutableArray array];
void (^block)(void) = ^{
array = [NSMutableArray array];
};
block();
这里改变的是 array 这个变量保存的指针,所以需要 __block。
3. 不可变对象和可变对象的区别
下面两种操作含义不同:
NSString *name = @"A";
name = @"B"; // 修改变量指向,需要 __block
NSMutableString *text = [NSMutableString stringWithString:@"A"];
[text appendString:@"B"]; // 修改对象内容,不需要 __block
面试回答
如果只是通过数组指针修改
NSMutableArray的内容,不需要__block。如果要让 Block 内部给数组变量重新赋值,才需要__block。判断标准是修改对象,还是修改变量本身。
六、Block 的三种类型
Block 按照存储位置通常分为三类。可以通过 class 方法或 isa 查看具体类型,但这些类名属于实现细节,不建议在业务逻辑中依赖它们。
1. Global Block
全局 Block 通常不访问外部的 auto 局部变量:
void (^globalBlock)(void) = ^{
NSLog(@"global");
};
它可以放在程序的数据区域,不需要保存局部变量环境。全局 Block 调用 copy 后通常仍然是全局 Block,复制不会产生新的堆对象。
运行时名称通常类似:
__NSGlobalBlock__
_NSConcreteGlobalBlock
2. Stack Block
访问了 auto 局部变量的 Block,初始情况下通常位于栈上:
int age = 20;
void (^stackBlock)(void) = ^{
NSLog(@"%d", age);
};
栈上的 Block 只在当前作用域有效。作用域结束后,不能再依赖这个栈 Block 继续存在。
运行时名称通常类似:
__NSStackBlock__
_NSConcreteStackBlock
3. Malloc Block
栈上的 Block 调用 copy 后会复制到堆上,成为 Malloc Block:
int age = 20;
void (^stackBlock)(void) = ^{
NSLog(@"%d", age);
};
void (^heapBlock)(void) = [stackBlock copy];
运行时名称通常类似:
__NSMallocBlock__
_NSConcreteMallocBlock
4. 调用 copy 的结果
| 原来的 Block | copy 后的结果 |
|---|---|
| Global Block | 通常仍是 Global Block |
| Stack Block | 复制到堆上,变成 Malloc Block |
| Malloc Block | 引用计数增加,仍在堆上 |
Block_copy 和 Block_release 在 MRC 下更常见。ARC 下,编译器和对象属性会自动处理大部分复制和释放工作。
七、为什么 Block 属性通常使用 copy
1. 栈 Block 的生命周期问题
如果把一个访问了局部变量的 Block 直接保存起来,而没有复制到堆上,原作用域结束后,栈上的 Block 可能已经无效:
@interface Downloader : NSObject
@property (nonatomic, copy) void (^completion)(NSData *data);
@end
使用 copy 后,setter 会把栈上的 Block 复制到堆上,保证属性在作用域结束后仍然可以调用。
2. ARC 下为什么仍然建议 copy
ARC 会在部分场景自动复制 Block,例如:
- Block 作为函数返回值
- Block 赋值给强引用变量
- Block 作为某些 Cocoa API 的参数
- Block 作为 GCD API 的参数
但是,属性声明仍然应该表达清楚语义:这个属性需要拥有一份可以跨作用域保存的 Block。copy 是 Objective-C 中 Block 属性最明确、最稳妥的声明方式。
3. strong 和 copy 的区别
在 ARC 下,对 Block 属性使用 strong,系统通常也会保证 Block 被复制到堆上,因此许多情况下表现相同。但从属性语义和跨 ARC/MRC 兼容角度看,Block 属性更推荐写成:
@property (nonatomic, copy) void (^completion)(NSData *data);
MRC 下几乎必须使用 copy,否则保存栈 Block 会带来悬空指针问题。
4. ARC 下 assign 为什么仍然不安全
下面这种声明不适合作为长期保存 Block 的属性:
@property (nonatomic, assign) void (^completion)(NSData *data);
assign 只保存 Block 的地址,不会复制 Block,也不会建立持有关系。假设有以下代码:
- (void)setup {
NSObject *object = [NSObject new];
void (^tempBlock)(void) = ^{
NSLog(@"%@", object);
};
self.completion = tempBlock;
}
ARC 下,tempBlock 作为默认的强引用局部变量,通常会让 Block 在这段代码执行期间具备堆上的生命周期。可是 setup 结束后,局部强引用 tempBlock 也会释放,而 assign 属性没有留下新的持有关系。如果没有其他对象持有这个 Block,Block 可能被释放,属性中留下的地址就会变成悬空指针。
因此,下面两件事要分开理解:
强引用局部变量:可能让 Block 在当前作用域内自动复制到堆上
copy 属性:让属性长期复制并持有 Block
ARC 常见的自动复制场景包括 Block 作为返回值、赋值给强引用变量、作为部分 Cocoa API 参数和作为 GCD API 参数。它只保证对应场景需要的生命周期,不会把 assign 属性自动变成 copy 属性。
5. 悬空指针和野指针
严格来说,两者有区别:
- 悬空指针:指针原来指向有效对象,但对象已经释放,指针仍保存旧地址。
- 野指针:指针没有正确初始化,或者指向完全不可用的地址。
assign 保存已经释放的 Block 地址时,准确说是形成了悬空指针。访问这个地址时,可能表现为野指针访问错误,例如 EXC_BAD_ACCESS。日常交流中,人们也常把悬空指针统称为野指针。
面试回答
问:Block 属性修饰词为什么是 copy?
Block 可能最初位于栈上,栈帧结束后就不能继续使用。
copy会把栈 Block 复制到堆上,使它可以跨作用域保存。ARC 会在一些场景自动复制 Block,但属性使用copy可以明确表达 Block 的生命周期语义,并兼容 MRC。
八、Block 捕获对象时的内存管理
理解 Block 的内存管理,要区分两种情况:Block 自己在哪里,以及 Block 捕获的变量是什么修饰符。
1. 栈上的 Block
栈上的 Block 生命周期很短。对于捕获的对象变量,不能简单地用“Block 一定强引用对象”来概括。真正稳定的生命周期管理通常发生在 Block 被复制到堆上时。
2. Block 复制到堆上
当 Block 被复制到堆上,系统会执行 Block 内部的 copy 逻辑。对于捕获的对象类型 auto 变量,ARC 会根据它的修饰符处理引用关系:
__strong:通常形成强引用。__weak:保持弱引用,不增加对象引用计数。__unsafe_unretained:不持有对象,也不会在对象释放后自动置 nil。
概念上可以理解为:
Block copy
-> 处理捕获变量
-> strong 变量 retain
-> weak 变量注册弱引用
-> unsafe_unretained 只保存地址
当堆上的 Block 被销毁时,系统会执行 dispose 逻辑,释放它持有的对象或 byref 变量。
PPT 中常见的底层函数名称包括:
_Block_object_assign
_Block_object_dispose
这些是编译器和 Block 运行时使用的内部机制。开发者一般不直接调用它们,但可以通过它们理解:Block 复制时建立引用关系,销毁时解除引用关系。
3. 捕获的是对象指针,不是对象的完整副本
Person *person = [[Person alloc] init];
void (^block)(void) = ^{
NSLog(@"%@", person.name);
};
Block 捕获的是 person 这个指针变量。ARC 下,当 Block 需要长期保存并被复制到堆上时,默认会强引用该对象,而不是把 Person 对象深拷贝一份。
4. 把状态放到对象属性中
如果需要让多个 Block 共享一个状态,可以把状态放到对象的属性中。修改对象的属性不需要 __block:
@interface FinishState : NSObject
@property (nonatomic, assign) BOOL finished;
@end
@property (nonatomic, copy) void (^checkBlock)(void);
- (void)setup {
FinishState *state = [FinishState new];
void (^startBlock)(void) = ^{
state.finished = YES;
};
self.checkBlock = ^{
NSLog(@"%@", state.finished ? @"YES" : @"NO");
};
startBlock();
}
- (void)check {
self.checkBlock();
}
startBlock 和 checkBlock 访问的是同一个 state 对象。checkBlock 被 copy 到堆上后,会继续持有捕获的 state,因此 setup 方法结束后,稍后调用 checkBlock 仍然可以读到 finished == YES。
这里的区别是:
state.finished = YES; // 修改对象属性,不需要 __block
state = [FinishState new]; // 修改 state 变量指向,需要 __block
九、__block 变量的内存管理
1. 基本过程
__block 变量有自己的 byref 结构。它从栈移动到堆时,也会参与 Block 的复制和销毁流程:
Block copy
-> 复制 byref 结构
-> 处理 byref 中保存的对象
Block dispose
-> 销毁 byref 结构
-> 释放 byref 中持有的对象
2. ARC 与 MRC 的差异
在 ARC 下,如果 __block 变量保存的是对象,byref 结构在移动到堆上后通常会按照 ARC 规则持有对象。因此,__block 在 ARC 下不再像早期 MRC 时代那样天然具有“不会 retain 对象”的效果。
在 MRC 下,__block 对象变量的行为不同,不能简单套用 ARC 的结论。面试回答时必须先说明内存管理环境。
3. 重要结论
__block的主要职责是允许修改变量并共享存储位置。它不是专门用来弱引用对象的修饰符。
如果目标是避免 Block 持有对象,应明确使用 __weak,或者在不支持 ARC 的旧代码中使用 __unsafe_unretained,但后者有访问悬空指针的风险。
4. ARC 下仍然会使用 __block 的场景
ARC 并没有取消 __block。它仍然适合以下场景:
修改计数器或结果变量
__block NSInteger count = 0;
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
count++;
}];
让多个 Block 共享一个变量
__block BOOL finished = NO;
void (^startBlock)(void) = ^{
finished = YES;
};
void (^checkBlock)(void) = ^{
NSLog(@"%@", finished ? @"YES" : @"NO");
};
startBlock();
checkBlock();
如果 checkBlock 被 copy 到属性或传给会保存它的异步 API,finished 对应的 byref 存储也会随 Block 一起保留,之后仍能读到最新值。BOOL 本身不是对象,延长的是保存它的 byref 存储生命周期。
但如果只是修改 NSMutableArray 的内容,仍然不需要 __block。如果目的是避免 Block 强引用对象,则应该使用 __weak,不要依赖 __block。
十、Block 和循环引用
1. 循环引用是怎样形成的
典型场景如下:
@interface ViewController ()
@property (nonatomic, copy) void (^action)(void);
@end
@implementation ViewController
- (void)setup {
self.action = ^{
NSLog(@"%@", self.title);
};
}
@end
引用关系是:
ViewController 强引用 Block
Block 强引用 ViewController
两边互相持有,引用计数无法归零,导致对象无法释放。
2. ARC 下用 __weak
推荐写法:
__weak typeof(self) weakSelf = self;
self.action = ^{
NSLog(@"%@", weakSelf.title);
};
Block 只弱引用 self,因此 self 销毁时不会因为 Block 而继续存活。
3. 回调执行期间使用 strong 临时变量
如果回调执行过程中必须保证对象仍然存在,可以在 Block 内部临时建立强引用:
__weak typeof(self) weakSelf = self;
self.action = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
[strongSelf updateUI];
};
这段代码的含义是:Block 长期只弱引用 self,执行回调时,如果对象还存在,就在本次调用期间临时强引用它。
现代 Objective-C 也可以写成:
__weak typeof(self) weakSelf = self;
self.action = ^{
typeof(self) self = weakSelf;
if (!self) {
return;
}
[self updateUI];
};
4. __unsafe_unretained 的风险
__unsafe_unretained id weakObject = self;
__unsafe_unretained 不会 retain 对象,也不会在对象销毁时自动置为 nil。对象释放后,变量可能仍然保存着原来的地址,再次访问可能造成野指针崩溃。
因此,ARC 下优先使用 __weak。只有在非常明确的性能、兼容性或底层场景中,才考虑 __unsafe_unretained。
5. __block 能不能解决循环引用
有时可以看到这种写法:
__block id blockSelf = self;
self.action = ^{
NSLog(@"%@", blockSelf);
blockSelf = nil;
};
它依赖 Block 执行后把 byref 变量置为 nil,从而打破引用关系。但它有前提:Block 必须确实执行,而且必须执行到清空变量的代码。如果 Block 永远不执行,循环引用仍然存在。
在 ARC 下,__block 对象变量在移动到堆上通常可能被强引用,因此不能把它当成通用的弱引用方案。一般优先使用 __weak。
6. MRC 下的旧方案
在 MRC 环境中,__block 变量和 ARC 下的内存管理行为不同,历史代码可能利用 __block 或 __unsafe_unretained 解决循环引用。但这类代码依赖手动内存管理,容易出现释放时机错误和野指针问题。维护旧项目时需要先确认是否开启 ARC,再判断方案。
面试回答
问:使用 Block 有哪些注意事项?
可以回答:
首先要注意 Block 的生命周期,长期保存时使用
copy。其次要注意变量捕获,普通局部变量按值捕获,需要修改时使用__block。第三要注意 Block 与对象之间的循环引用,ARC 下通常使用__weak,必要时在执行期间转成临时强引用。__block主要用于修改变量,不能简单当作弱引用使用。
十一、Block 作为属性、参数和返回值
1. Block 作为属性
typedef void (^RequestCompletion)(NSData *data, NSError *error);
@interface RequestManager : NSObject
@property (nonatomic, copy) RequestCompletion completion;
@end
使用 typedef 可以让复杂的 Block 类型更容易阅读。
2. Block 作为方法参数
- (void)loadDataWithCompletion:(void (^)(NSData *data, NSError *error))completion;
更推荐使用类型别名:
typedef void (^LoadCompletion)(NSData *data, NSError *error);
- (void)loadDataWithCompletion:(LoadCompletion)completion;
3. Block 作为返回值
- (NSInteger (^)(NSInteger))makeCalculator {
return ^NSInteger(NSInteger value) {
return value * 2;
};
}
Block 作为返回值时,ARC 通常会保证它能够离开当前作用域继续使用。MRC 下则需要特别注意返回 Block 的复制处理。
4. 回调执行时机
一个异步 API 接收了 Block,并不代表 Block 一定马上执行。Block 可能:
- 立即同步执行
- 在后台线程执行
- 投递到主线程执行
- 执行多次
- 永远不执行,例如请求被取消或流程提前结束
因此设计回调 API 时,应明确线程、执行次数和是否可能不回调。调用者也不应该默认回调一定在主线程。
十二、Block 与 GCD
GCD 大量使用 Block:
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
NSData *data = [self loadData];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateUIWithData:data];
});
});
这里有两个需要注意的点:
- GCD 会保存异步任务 Block,因此 Block 及其捕获的对象可能比当前方法存活得更久。
- 外层 Block 如果强引用
self,而self又长期持有执行任务的对象或队列相关回调,就需要检查是否形成环。
常见写法:
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
NSData *data = [weakSelf loadData];
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf updateUIWithData:data];
});
});
但不能机械地对所有 GCD Block 都使用 weak。一次性异步任务通常会自然结束,是否需要弱引用取决于任务生命周期和业务语义。更重要的是明确谁持有谁,以及任务取消时如何处理对象生命周期。
十三、Block、代理和通知的选择
1. Block 回调
适合一次性结果返回或逻辑简单的回调:
typedef void (^LoginCompletion)(BOOL success, NSError *error);
优点是调用位置集中、代码直观。风险是容易捕获 self 形成循环引用,也不适合表达大量不同事件。
2. Delegate
适合一个对象持续向另一个对象报告多个事件:
@protocol PlayerDelegate <NSObject>
- (void)playerDidStart;
- (void)playerDidFinish;
@end
代理通常使用 weak,避免被代理对象和代理方相互持有。
3. Notification
适合多个对象关注同一个广播事件,但关系较松散。使用通知时要注意及时移除观察者,避免事件来源和接收者难以追踪。
十四、Block 常见误区
误区 1:Block 就是函数指针
Block 不只是代码地址,还可能保存捕获变量和内存管理信息。函数指针不能直接完成同样的变量捕获能力。
误区 2:用了 __block 就没有循环引用
__block 解决的是变量修改和共享存储问题。在 ARC 下,__block 对象变量可能形成强引用,仍然可能循环引用。
误区 3:修改 NSMutableArray 必须加 __block
只修改数组内容不需要 __block。给数组变量重新赋值才需要。
误区 4:所有 Block 属性都应该用 strong
ARC 下某些场景 strong 和 copy 结果相近,但 Block 属性使用 copy 更明确,也兼容 MRC,是通用推荐写法。
误区 5:使用 weakSelf 就一定安全
weakSelf 只解决强引用环的一部分问题。它可能在异步回调执行前已经释放,导致回调什么也不做。还要考虑任务是否需要保证对象存活、回调是否在正确线程执行,以及回调是否会被重复调用。
误区 6:__unsafe_unretained 等同于 weak
两者都不会增加引用计数,但 weak 会在对象销毁后自动清零,__unsafe_unretained 不会,后者可能产生野指针。
十五、如何查看 Block 的类型和底层代码
1. 查看运行时类型
NSLog(@"%@", [block class]);
NSLog(@"%p", block);
运行时可能打印出:
__NSGlobalBlock__
__NSStackBlock__
__NSMallocBlock__
这些名称主要用于调试和学习,不建议业务代码依赖。
2. 将 Objective-C 转换成 C++
可以使用 Clang 查看编译器生成的近似 C++ 代码:
xcrun -sdk iphoneos clang -arch arm64 \
-rewrite-objc \
-fobjc-arc \
-fobjc-runtime=ios-8.0.0 \
main.m \
-o main.cpp
如果源文件依赖 UIKit 等框架,可以补充:
-framework UIKit
转换后的代码可能看到类似 __block_literal_global、__block_impl、__Block_byref_xxx 等结构。不同 Xcode、Clang、架构和编译参数会生成不同细节,所以转换结果适合辅助理解,不应当当作稳定的公开 ABI。
3. 为什么可能出现 cannot create __weak reference
使用 Clang 转换包含 __weak 的代码时,可能因为没有开启 ARC 或没有指定合适的 Objective-C Runtime 而报错。可以确认:
- 编译参数是否包含
-fobjc-arc - 是否指定了支持 weak 的运行时版本
- 当前文件和工程是否使用 ARC
例如:
xcrun -sdk iphoneos clang -arch arm64 \
-rewrite-objc \
-fobjc-arc \
-fobjc-runtime=ios-8.0.0 \
main.m \
-o main.cpp
十六、面试题集中回答
问:Block 的本质是什么?
答:Block 本质上是一个 Objective-C 对象,封装了函数实现和调用环境。它可以捕获外部变量,并在之后的时间和指定的线程中执行。
问:Block 有哪些类型?
答:通常分为 Global Block、Stack Block 和 Malloc Block。没有捕获 auto 局部变量的通常是 Global Block;捕获了 auto 变量的 Block 初始通常位于栈上;栈 Block 调用 copy 后会复制到堆上,成为 Malloc Block。具体类型属于实现细节。
问:为什么 Block 属性使用 copy?
答:Block 可能位于栈上,作用域结束后就不能继续使用。copy 会把栈 Block 复制到堆上,保证它可以跨作用域保存。ARC 会在强引用局部变量、返回值和部分 API 参数等场景自动处理复制,但 assign 属性不会因此获得长期持有关系。Block 属性使用 copy 可以明确表达生命周期语义,并兼容 MRC。
问:ARC 下 Block 属性使用 assign 会有问题吗?
答:会。assign 只保存 Block 地址,不复制也不持有 Block。即使 Block 赋值给强引用局部变量时被 ARC 自动复制到堆上,局部变量离开作用域后,Block 仍可能因为没有其他持有者而释放,assign 属性就会留下悬空指针。长期保存 Block 应使用 copy。
问:ARC 下强引用局部变量会自动复制 Block 吗?
答:通常会。Block 赋值给默认的强引用局部变量时,ARC 会使用适合 Block 的持有语义,使栈 Block 能在当前强引用生命周期内安全使用。但这只保证强引用变量本身的生命周期,不能替代属性的 copy。
问:__block 的作用是什么?
答:__block 让 Block 可以修改局部变量。编译器会把它包装成 byref 结构,Block 和外部代码共享同一份存储。它不等于 weak,在 ARC 下也不能把它当作通用的循环引用解决方案。
问:Block 修改 NSMutableArray 需要 __block 吗?
答:如果只是调用 addObject: 修改数组内容,不需要。若要给数组变量重新赋值,才需要 __block。判断关键是修改对象内容,还是修改变量本身。
问:Block 如何捕获变量?
答:普通局部 auto 变量通常按值捕获,局部 static 变量通常通过指针访问,全局变量不需要捕获而是直接访问。对象类型变量被捕获后,Block 被复制到堆时会按照 strong、weak 或 unsafe_unretained 等修饰符处理引用关系。
问:ARC 下如何解决 Block 循环引用?
答:如果对象强引用 Block,而 Block 又捕获对象,应使用 __weak typeof(self) weakSelf = self,让 Block 弱引用对象。执行回调时如果需要保证对象在本次调用期间存在,可以把 weakSelf 临时转成强引用。一次性 GCD 任务是否需要 weak,要根据生命周期判断。
问:__block 和 __weak 有什么区别?
答:__block 主要解决变量可修改和共享存储问题,__weak 主要解决对象引用关系和生命周期问题。一个关注变量存储方式,一个关注对象是否被持有,二者用途不同。
问:对象作为 Block 捕获变量时,一般需要 __block 吗?
答:不一定。只读取对象或修改对象内容时不需要,例如 [array addObject:obj]。只有重新给对象变量赋值时才需要 __block。如果需要避免 Block 强引用对象,则使用 __weak;ARC 下 __block 对象变量移动到堆后仍可能被强引用。
问:assign 造成的是悬空指针还是野指针?
答:严格来说是悬空指针,因为属性保存了已经释放对象的旧地址。访问悬空指针时,可能触发通常所说的野指针错误,例如 EXC_BAD_ACCESS。日常语境中两者经常混用。
十七、学习和排查 Block 问题的顺序
遇到 Block 相关问题时,可以按下面顺序分析:
- 先看 Block 的签名,确认参数和返回值是否匹配。
- 再看 Block 捕获了哪些外部变量。
- 判断捕获的是普通值、对象指针、
static变量还是全局变量。 - 判断 Block 是否会离开当前作用域,长期保存时确认是否需要
copy。 - 检查 Block 和对象之间是否互相强引用。
- 确认回调执行线程、执行次数和对象是否可能提前释放。
- 如果行为仍然不清楚,再用
class、断点和 Clang 转换结果辅助验证。
十八、总结
理解 Block 可以抓住四个核心问题:
Block 是什么?
封装了函数调用和调用环境的对象。
外部变量怎么进 Block?
auto 通常按值捕获,static 通过地址访问,全局变量直接访问。
为什么要 copy?
让可能位于栈上的 Block 复制到堆上,延长可用生命周期。
如何避免循环引用?
分析对象持有关系,ARC 下通常用 __weak,而不是把 __block 当成 weak。
面试时,先讲清楚这四点,再补充 Block 类型、byref 结构、forwarding 指针、ARC 与 MRC 差异以及 GCD 场景,答案就会从“记概念”变成“能解释实际问题”。