5-5.【OC】【Block】Block_copy / [block copy] 在 ARC 下是否还“真的有用”?

2 阅读2分钟

这是一个非常敏锐的问题。在 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 桥接、非强引用容器、或需要精确控制副本生成时,它依然是你的保命符。