3-6.【OC】【Runtime】+initialize 在多线程环境下是否安全?

0 阅读2分钟

简单直接的答案是:安全。

+initialize 的安全性是由 Objective-C Runtime 在底层通过**线程锁(Lock)**机制严格保障的。即使有多个线程同时第一次访问某个类,Runtime 也能确保该类的初始化逻辑只被执行一次,且其他线程会处于阻塞等待状态。

以下是其底层保障机制的深度解析:

1. 锁的保护机制

libobjc 的源码中,+initialize 的触发位于 lookUpImpOrForward(查找方法实现)的过程中。

  • 全局加锁:当一个类准备初始化时,Runtime 会获取一个全局的 classInitLock
  • 线程等待:如果线程 A 正在执行 [ClassA initialize],此时线程 B 也尝试调用 ClassA 的方法,线程 B 会被挂起(阻塞),直到线程 A 完成初始化并释放锁。
  • 状态标记:一旦初始化完成,该类会被标记为 isRealizedisInitialized。后续访问将直接跳过初始化逻辑,不再有锁开销。

2. 递归死锁的预防

Runtime 的设计考虑到了递归调用的情况。

  • 如果线程 A 在 +initialize 内部又调用了该类自己的另一个方法,Runtime 会识别出“当前线程正在进行初始化”,从而允许直接通行,不会发生死锁

3. 注意事项:虽然“安全”,但仍有“陷阱”

尽管底层机制是线程安全的,但在编写 +initialize 时,如果不注意业务逻辑,仍可能引发死锁或性能问题:

A. 避免耗时操作(阻塞风险)

由于其他线程在类初始化完成前会被阻塞,如果你在 +initialize 里执行了复杂的 IO、网络请求或大量计算,会导致所有并发调用该类的线程全部“卡死”,甚至引发 App 主线程掉帧或看门狗(Watchdog)杀掉进程。

B. 跨类调用的死锁(逻辑风险)

虽然单类初始化是安全的,但跨类相互调用可能导致经典的死锁:

  1. 线程 A 访问 Class A,进入 [Class A initialize]
  2. 线程 B 访问 Class B,进入 [Class B initialize]
  3. 此时,如果 Class A 的初始化逻辑里尝试调用 Class B,而 Class B 的初始化逻辑里尝试调用 Class A
  4. 结果:两个线程互相等待对方释放初始化锁,导致 App 永久卡死。

C. 继承带来的“多次执行”假象

虽然 +initialize 是线程安全的,但你可能会观察到它被“多次调用”。

  • 原因:如果子类没有实现 +initialize,它会继承并调用父类的实现。

  • 对策:为了确保你的初始化逻辑只运行一次,必须使用类型检查:

    Objective-C

    + (void)initialize {
        if (self == [MyClass class]) {
            // 真正的初始化逻辑
        }
    }
    

总结

+initialize线程安全 的。它是执行单例配置、初始数据加载或 Method Swizzling 的理想场所,因为你完全不必担心并发冲突。