为什么在 ARC 下,Block 属性建议使用 copy(或者 strong)修饰?
在 ARC(Automatic Reference Counting)环境下,Block 属性建议使用 copy 修饰,但使用 strong 也是完全可以的。
要理解其中的原因,我们需要从 Block 的内存位置变化以及 ARC 的工作原理两个方面来解释。
核心原因:从“栈”移动到“堆”
Block 在内存中主要有三种形态:
_NSConcreteGlobalBlock(全局块):不捕获任何外部变量,存在于数据区(.data),类似全局变量,生命周期伴随整个 App。_NSConcreteStackBlock(栈块):捕获了外部变量,默认创建在栈上。_NSConcreteMallocBlock(堆块):当栈块被copy后,会复制到堆上。
1. MRC 时代的遗留习惯 (The Origin)
在 MRC(手动管理内存)时代,Block 创建时默认是在栈上的。
- 如果一个 Block 在栈上,当定义它的函数或方法返回时,栈帧被销毁,Block 的内存也会被回收。
- 如果此时你只是用
retain(相当于 ARC 的strong)指向它,你持有的只是一个指向已销毁栈内存的野指针。当你稍后调用这个 Block 时,程序就会崩溃。 - 解决方案:必须对 Block 发送
copy消息,将其从栈复制到堆上,这样它才能拥有独立的生命周期,在作用域外存活。
因此,在 MRC 时代,Block 属性必须使用 copy 修饰。
2. ARC 的自动优化 (The Modern Way)
到了 ARC 时代,编译器做了大量优化。当 Block 被赋值给一个 strong 类型的对象(包括属性)时,编译器会自动帮你调用 copy 方法。
这意味着:
// 在 ARC 下,这两者在底层行为上几乎是一样的
@property (nonatomic, copy) void (^myBlock)(void);
@property (nonatomic, strong) void (^myBlock)(void);
当你执行 self.myBlock = ^{ ... }; 时,无论修饰符是 copy 还是 strong,编译器都会确保这个 Block 被复制到了堆上。
为什么现在依然建议使用 copy?
虽然 strong 在 ARC 下也能正常工作,但业界规范和 Apple 官方文档依然推荐使用 copy,主要基于以下几点:
语义明确(Semantics):
Block 的行为与普通的 Objective-C 对象(如UIView)不同。普通对象通常是引用传递,而 Block 经常代表一段代码逻辑的“快照”或“闭包”。使用copy显式地表达了行为意图:“我需要一份独立的、在堆上的拷贝,不受原栈帧销毁的影响。” 这符合 Block 必须移至堆上才能安全使用的底层逻辑。遵循传统与习惯:
由于 MRC 时代的强制要求,使用copy修饰 Block 已经成为了 Objective-C 开发者的肌肉记忆和标准惯例。保持这种写法可以让代码风格统一,减少阅读者的困惑。防止可变性问题(类似 NSString):
虽然 Block 不像NSString那样有NSMutableString子类,但copy属性的一般原则是“赋值时生成不可变副本”。对于 Block 来说,copy操作不仅是内存位置的移动(栈->堆),也是所有权的确立。
总结
- 本质原因:Block 默认在栈上,为了在作用域外使用,必须
copy到堆上。 - ARC 现状:ARC 会自动处理 Block 的
copy,所以strong和copy效果一样,都不会 Crash。 - 最佳实践:建议继续使用
copy。这是一种“显式编程”,清晰地表达了将 Block 移至堆上的意图,也符合 Objective-C 的编码规范。
代码示例:
// 推荐写法
@property (nonatomic, copy) void (^completionHandler)(BOOL success);
// 可行写法(ARC下安全,但不那么“地道”)
@property (nonatomic, strong) void (^completionHandler)(BOOL success);