这是一个非常敏锐的问题。在 ARC(自动引用计数) 几乎接管了一切的今天,手动调用 Block_copy 或 [block copy] 的场景确实极度萎缩,但它并没有完全“退役”。
简单直接的回答是:在 95% 的业务开发中,你不需要它;但在处理底层 C 接口、某些特定集合或避免“延迟提升”时,它依然不可替代。
1. 为什么它看起来“没用”了?
正如你所知,ARC 编译器在检测到以下情况时会自动插入 objc_retainBlock(内部调用的就是 Block_copy):
- 赋值给
__strong变量。 - 作为函数返回值。
- 传入 Cocoa 框架/GCD 方法。
这涵盖了绝大多数日常开发场景,使得手动 copy 显得多余。
2. 它“真的有用”的 3 个特殊场景
即使在 ARC 下,以下情况你可能依然需要手动 copy:
A. 将 Block 放入 C 语言数组或结构体中
ARC 主要管理 Objective-C 对象指针。如果你将一个 Block 存入一个 C 数组或单纯的 C 结构体成员中,编译器有时无法识别出这里需要进行“强引用提升”,此时 Block 依然留在栈上。
Objective-C
// 潜在风险:struct 成员不一定会触发 ARC 的自动 copy
struct MyStruct {
void (^block)(void);
};
void test() {
struct MyStruct s;
s.block = ^{ NSLog(@"Check"); }; // 这里 block 可能是 StackBlock
// 出了 test 函数,s.block 就会变成野指针
}
B. 显式地将 Block 转换为指针(void *)
当你需要将 Block 传递给某些底层 C 库(如 pthread 或自定义的 C 回调接口),并且通过 (__bridge void *) 进行转换时,ARC 将不再接管该对象的生命周期。
Objective-C
// 必须手动 copy 到堆,否则 C 函数异步回调时栈已经销毁
void *rawBlock = (__bridge_retained void *)[^{
NSLog(@"Async C Task");
} copy];
C. 容器类的边缘情况(非 Property)
虽然向 NSArray 添加对象会触发 retain,但对于 Block 来说,retain 不等于 copy。对栈上的 Block 执行 retain 是无效的,它依然在栈上。 虽然现代编译器在 [array addObject:stackBlock] 时通常会自动处理,但在处理一些底层集合(如 CFArray)或者自定义内存管理的容器时,手动 copy 是确保其进入堆区的唯一稳妥手段。
3. Block 类型的演变逻辑
你可以通过以下代码直观感受手动 copy 的物理变化:
| 代码片段 | 类型 (ARC 下) |
|---|---|
^{ ... } | _NSConcreteStackBlock (中间临时态) |
[^{ ... } copy] | _NSConcreteMallocBlock (堆态) |
void (^b)(void) = ^{ ... }; | _NSConcreteMallocBlock (被变量持有后自动提升) |
总结
在 ARC 环境下,[block copy] 已经从“常规武器”变成了**“特种武器”**。
- 业务层: 几乎没用,直接赋值给强引用变量即可。
- 底层层: 在处理 C 桥接、非强引用容器、或需要精确控制副本生成时,它依然是你的保命符。