🧱 Block的底层本质
面试题:block的本质是什么?它的内部结构是怎样的?
Block 本质上是一个封装了函数调用及其执行环境(捕获的变量)的 OC 对象。它内部也有一个 isa 指针。
通过 clang -rewrite-objc 命令将 OC 代码编译为 C++ 后,可以看到 Block 的底层结构如下:
c
// Block 的基础结构
struct __block_impl {
void *isa; // 指向 Class 对象,表明 Block 的类型
int Flags; // 标志位,包含引用计数等信息
int Reserved; // 保留字段
void *FuncPtr; // 函数指针,指向 Block 内部代码的执行函数
};
// Block 的最终结构 (以 main 函数中的 block 为例)
struct __main_block_impl_0 {
struct __block_impl impl; // 上述基础结构
struct __main_block_desc_0* Desc; // 描述信息,如 Block 大小
// 捕获的变量会在这里展开...
int age; // 示例:捕获的 int 类型变量
};
其核心关系可以用下图表示:
要点:
🏠 Block 的三种类型与内存管理
面试题:Block 有哪几种类型?分别在什么情况下使用?它们存在哪里?
| Block 类型 | 存储域 | 典型场景 | 是否需手动管理 |
|---|---|---|---|
__NSGlobalBlock | 数据段 | Block 没有捕获任何自动变量(如局部变量)时。 | 无需。整个程序生命周期有效。 |
__NSStackBlock | 栈区 | 在 MRC 下,Block 捕获了自动变量。栈区内存由系统管理,函数返回即可能销毁。 | 危险! 需立即 copy 到堆上使用。 |
__NSMallocBlock | 堆区 | 对 __NSStackBlock 执行了 copy 操作后产生。ARC 下编译器会自动进行此操作。 | 需手动管理引用计数(MRC),或依赖 ARC。 |
核心要点:
- ARC 下的自动 copy:在 ARC 中,当 Block 被赋值给一个
strong或copy修饰的变量,或者作为方法/函数的返回值时,编译器会自动将其从栈复制到堆上,保证其安全性。 - 为什么属性要用
copy? 这是 MRC 时代留下的最佳实践。在 ARC 下,strong和copy对 Block 的作用一样(编译器会优化),但使用copy能更清晰地表明意图,并与 MRC 代码保持兼容。
🔗 变量捕获机制与 __block
面试题:block 内部为什么不能修改普通局部变量的值?__block 的作用是什么?
- 值捕获:对于普通局部变量(auto变量) ,Block 在内部只会捕获其在定义瞬间的值(一份拷贝)。因此,之后在外部修改变量,不影响 Block 内部已捕获的值;在 Block 内部修改,也会导致编译错误。
- 指针捕获:对于静态局部变量(static) 和全局变量,Block 会捕获其指针,因此可以在 Block 内部修改外部变量的值。
__block的本质:__block的作用是让一个栈上的自动变量也能在 Block 内部被修改。其原理是将该变量包装成一个结构体对象(如__Block_byref_age_0),这个结构体包含了变量的原始值以及指向它自己的指针。当 Block 从栈复制到堆时,这个__block变量也会被一同复制到堆上,从而实现 Block 内外对该变量的共享与修改。
思考题:在 block 中修改 NSMutableArray,需要添加 __block 吗?
不需要。 修改 NSMutableArray 的内容(如 addObject:)是使用了数组变量,而不是修改数组变量本身(即指针指向)。Block 捕获的是数组变量的指针(值拷贝),通过这个指针去操作数组内容是允许的。只有当你试图修改数组指针本身(如 array = otherArray)时,才需要 __block。
🚨 循环引用 (Retain Cycle)
面试题:什么是循环引用?为什么 block 容易导致循环引用?如何解决?
-
成因:当对象(如 self)持有了一个 Block 属性(强引用) ,而这个 Block 内部又强引用了该对象(如直接使用
self) 时,就会形成循环引用,导致内存无法释放。 -
解决方案:
-
__weak+__strong舞蹈:objectivec
__weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(self) strongSelf = weakSelf; if (strongSelf) { // 在这里安全地使用 strongSelf strongSelf.property = ...; } };__weak打破了循环引用,而 Block 内部的__strong确保了在执行期间,即使self在其他地方被释放,也能在这个 Block 的作用域内保持存活,避免崩溃。 -
使用
__block并在执行后手动置 nil(MRC 常见,ARC 下不推荐,需确保 Block 被执行)。 -
使用
NSProxy等更高级的方法(用于复杂场景)。
-
✨ ARC 与 MRC 下 Block 的内存差异
面试题:ARC 和 MRC 下,Block 的内存管理有哪些关键区别?
-
默认存储位置:
-
对对象类型变量的影响:
1. 底层本质:__block 变量是如何包装的?
当使用 __block 修饰一个对象类型的变量时,编译器会将这个变量转换为一个 __Block_byref_xxx 结构体的实例。这个结构体不仅保存了原始变量的值(即对象指针),还包含了管理其内存所需的元数据和函数指针。
以 __block id obj = self; 为例,通过 clang -rewrite-objc 可以看到类似如下的结构:
c
// 生成的 __block 结构体
struct __Block_byref_obj_0 {
void *__isa; // 保留,通常为0
__Block_byref_obj_0 *__forwarding; // 指向自身(栈上时指向自己,堆上时指向堆上的拷贝)
int __flags; // 标志位
int __size; // 结构体大小
void (*__Block_byref_id_object_copy)(void*, void*); // 拷贝辅助函数
void (*__Block_byref_id_object_dispose)(void*); // 释放辅助函数
id obj; // 真正的对象指针,被 __strong 持有(默认)
};
关键点:
__forwarding指针:确保无论在栈上还是堆上,都能访问到正确的变量。当 Block 从栈复制到堆时,__forwarding会指向堆上的拷贝,保证修改的一致性。__Block_byref_id_object_copy和__Block_byref_id_object_dispose:这两个函数是专门为对象类型生成的,用于在 Block 复制和销毁时,对捕获的对象进行内存管理(retain/release 或对应操作)。
2. ARC 下 __block 对象的内存管理流程
2.1 默认强引用(__strong)
在 ARC 下, __block 修饰的对象默认会被强引用。这意味着当 Block 从栈复制到堆时,__block 变量结构体也会被复制到堆,并且它所持有的对象会被 retain 一次;当堆上的 Block 被销毁时,该对象会被 release。
具体过程:
-
Block 从栈复制到堆(例如将 Block 赋值给一个
strong属性,或作为返回值):- 调用
_Block_copy函数。 _Block_copy会递归处理 Block 捕获的所有对象类型变量,包括__block变量。- 对于
__block对象,会调用__Block_byref_obj_0中的__Block_byref_id_object_copy函数。这个函数内部最终调用_Block_object_assign,它会根据变量的修饰符(这里是默认的__strong)对对象执行retain操作。 - 同时,
__forwarding指针被更新,指向堆上的__block结构体。
- 调用
-
堆上的 Block 被释放(引用计数变为0):
- 调用
_Block_release。 - 对于
__block对象,会调用__Block_byref_id_object_dispose函数,其内部通过_Block_object_dispose对对象执行release操作。
- 调用
因此,在 ARC 下,__block 对象的行为类似于一个自动管理的强引用,无需开发者手动干预其基本内存管理。
2.2 与其他修饰符组合
__block 也可以与 __weak、__unsafe_unretained 等修饰符组合使用,以改变对对象的持有方式。
-
__block __weak id weakObj = self;- 此时生成的
__Block_byref结构体中的obj成员是__weak类型的。 - 当 Block 复制到堆时,
_Block_object_assign会对weakObj进行弱引用处理(不会 retain),只记录一个指向对象的弱指针。 - 当 Block 销毁时,
_Block_object_dispose也不会 release(因为本来就是弱引用)。 - 注意:在 Block 执行期间,如果对象被释放,
weakObj会变成 nil,需要像常规__weak那样在内部转换为__strong使用,以防止中途被释放。
- 此时生成的
-
__block __unsafe_unretained id unretainedObj = self;- 结构体中的
obj是__unsafe_unretained类型,不进行任何 retain/release,仅作指针赋值。 - 这类似于 MRC 的行为,需确保对象在 Block 执行期间一定存活,否则会造成野指针崩溃。
- 结构体中的
3. 循环引用的陷阱与解决
尽管 ARC 自动管理了 __block 对象的引用计数,但 __block 本身仍然可能导致循环引用,因为:
- 对象(如
self)持有了 Block(如通过strong属性)。 - Block 捕获了
__block变量,而该变量又强引用了self(默认__strong)。 __block结构体被 Block 持有,__block结构体中的对象指针又指向self,形成闭环。
示例:
objectivec
self.block = ^{
__block id blockSelf = self; // 默认强引用 self
dispatch_async(..., ^{
// 使用 blockSelf
});
};
这里,self → block → __block 结构体 → self,循环引用无法释放。
解决办法:
-
使用
__weak打破循环:objectivec
__weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(self) strongSelf = weakSelf; if (strongSelf) { // 安全使用 } };这是最推荐的方式,利用
__weak避免强引用,内部再用__strong保证执行期间对象存活。 -
手动置 nil(需确保 Block 一定会被执行):
objectivec
__block id blockSelf = self; self.block = ^{ // 使用 blockSelf blockSelf = nil; // 执行完毕后断开引用 }; self.block(); // 必须调用,否则循环依旧这种方法依赖 Block 执行并手动置 nil,不够安全,仅适用于明确调用 Block 的场景。
-
利用
__block+__weak的组合:objectivec
__block __weak id weakSelf = self; self.block = ^{ id strongSelf = weakSelf; if (strongSelf) { // ... } };此时
__block变量持有的是弱引用,不会造成循环。但要注意,weakSelf在 Block 外部也可能被置 nil,不过只要在 Block 内部转换一次强引用即可。
4. ARC 与 MRC 的关键差异
| 特性 | MRC | ARC |
|---|---|---|
__block 对象的内存管理 | 不自动 retain/release,需开发者手动管理(如避免提前释放)。 | 默认强引用(__strong),自动 retain/release,行为与普通 __strong 变量类似。 |
| 循环引用风险 | 同样存在,但需手动置 nil 或使用 __block 后置 nil。 | 同样存在,推荐使用 __weak 打破循环。 |
__forwarding 的作用 | 保证堆栈同步。 | 相同,但 ARC 下自动管理对象,__forwarding 依然重要。 |
在 MRC 中,__block 对象不会自动 retain,所以如果 Block 从栈复制到堆后,原栈上对象可能被销毁,需确保对象存活。而在 ARC 中,复制时自动 retain,保证了对象在 Block 生命周期内一定存活,除非存在循环引用。
5. 示例代码深入分析
objectivec
// ARC 环境
- (void)testBlock {
NSObject *obj = [NSObject new]; // 局部变量,默认 __strong
__block NSObject *blockObj = obj; // __block 包装,强引用 obj
void (^block)(void) = ^{
NSLog(@"%@", blockObj); // blockObj 指向 obj
};
block(); // 此时 obj 仍被 blockObj 强引用,不会释放
}
内存过程:
-
obj是局部强引用,指向新对象。 -
blockObj是__block变量,其底层结构体中的obj成员是__strong类型,所以blockObj也强引用同一对象。此时对象引用计数为2(obj和blockObj)。 -
Block 捕获
blockObj(注意 Block 捕获的是__block变量本身的结构体指针,而不是直接捕获对象)。Block 从栈复制到堆时,会复制__block结构体到堆,并且结构体中的obj再次被 retain?实际上,复制过程:- Block 复制:
_Block_copy复制 Block 到堆。 - 对于
__block变量,调用其copy辅助函数,该函数内部调用_Block_object_assign。 _Block_object_assign会根据__block变量的修饰符处理对象。由于__block变量是默认__strong,它会 retain 对象一次。所以对象引用计数增加到3(obj局部强引用 + 栈上__block结构体中的强引用 + 堆上__block结构体中的强引用?注意这里有点特殊:__block变量复制时,实际上是将整个结构体复制到堆,堆上的结构体中的对象指针指向同一对象,同时 retain 一次。原来的栈上结构体中的对象指针仍然指向对象,但由于栈上的__block结构体即将被销毁(Block 复制完成后,栈上 Block 和__block结构体仍存在,但通常我们只持有堆上的 Block),所以最终对象引用计数取决于堆上__block结构体的持有。- 更准确的简化理解:
__block变量被 Block 捕获后,其生命周期与持有它的 Block 相关,对象被__block结构体强引用,直到该结构体被释放。
- Block 复制:
-
当
testBlock执行完毕,局部变量obj被释放,对象引用计数减1。 -
如果
block还被其他地方持有(如赋值给属性),则对象仍存活;如果block被释放(例如是局部 Block 未传出),则 Block 释放时会释放__block结构体,进而 release 对象,对象最终销毁。
总结
- ARC 下
__block对对象的内存管理是自动的,默认__strong,开发者无需手动 retain/release。 __block变量本身是一个结构体,通过__forwarding保证堆栈一致性,通过内置的 copy/dispose 函数管理对象。- 循环引用依然可能发生,因为
__block默认强引用对象。解决方法是使用__weak修饰__block变量,或者在 Block 内部手动置 nil(不推荐)。 - 理解
__block底层有助于解释一些奇怪的内存问题,例如为什么用__block修饰对象后,在 Block 外部将变量置为 nil 并不会影响 Block 内部的引用(因为内部持有的是结构体中的对象指针,而非外部变量本身)。
💡 总结与建议
准备 Block 相关的面试时,建议你:
- 动手编译:用
clang -rewrite-objc命令亲自查看不同场景下 Block 转换后的 C++ 代码,这是理解底层最直接的方式。 - 多问为什么:思考每种修饰符(
copy、__weak、__block)背后设计的根本原因,而不仅仅是死记硬背。 - 关注内存:Block 的考察重点几乎总是围绕着内存管理(类型、生命周期、循环引用) 展开。