Swift 与 Objective-C

+load 与 +initialize:调用时机与继承陷阱

为什么方法交换代码总是放在 +load 中?不能放进看起来相似的 +initialize 吗?

4 分钟阅读
+load 与 +initialize:调用时机与继承陷阱 封面图

为什么方法交换代码总是放在 +load 中?不能放进看起来相似的 +initialize 吗?

+load+initialize 看起来都像是“类准备好时调用一次的方法”,但从调用时机、调用方式到继承规则,全都不同。这也是 Objective-C 面试中的常见主题;如果理解有误,很容易困惑:“为什么这段代码会执行两次?”这个问题值得整理一次。


快速对比表

+load +initialize
调用时机 运行时加载类时(main 之前) 类在收到第一条消息之前(lazy)
调用方式 直接调用函数指针 经由 objc_msgSend
分类 类和分类的实现都会分别调用 分类实现会覆盖类实现
继承 只调用实际实现它的类 会被继承 — 可能因为子类而调用多次
不使用时? 仍然会调用 不使用该类就永远不会调用

下面逐一说明只看表格不容易理解的部分。


+load:早于 main,无条件执行

+load会在包含该类(或分类)的二进制文件被运行时加载时调用,也就是main 函数开始执行之前。即使应用一次都没有使用该类,也会调用它。

它的调用方式很特殊。不经过 objc_msgSend,而是直接通过函数指针调用。因此,普通的重写规则不适用。

  • 如果子类没有实现 +load,不会改为调用父类的实现。
  • 类的 +load 和分类的 +load分别调用,这不是覆盖。
  • 顺序有保证:父类的 +load 先于子类,类的 +load 先于分类。

这些特性与方法交换完全契合。即使在分类中实现 +load,也不会干扰原始类的 +load,并且能在应用生命周期最早的阶段可靠地执行一次。

但它也有代价。+load会**直接计入应用启动时间。**所有类的+load都会在 main 之前依次执行,因此在这里执行繁重工作会延迟首屏显示。这就是 Apple 多年来建议“尽量避免 +load”的原因。实际上,在+load中,self 所属映像之外的其他类可能尚未加载,因此能执行的操作也受到限制。

展示应用执行时间线上 +load 和 +initialize 调用时机的示意图
分歧点只有一个:调用是否经过 msgSend

+initialize:第一条消息之前的延迟调用

+initialize采用完全相反的策略。运行时会在类收到第一条消息之前调用它。如果应用从未使用该类,就永远不会调用。这是一个不会增加启动负担的 lazy 初始化时机。

这里会经过 objc_msgSend,因此会像普通方法一样应用**继承规则。**著名的陷阱正是在这里出现的。

@implementation Animal
+ (void)initialize {
    NSLog(@"initialize: %@", self);
}
@end

@interface Dog : Animal
@end
@implementation Dog
@end

Dog 发送第一条消息后,日志如下。

initialize: Animal
initialize: Dog

Animal+initialize执行两次。一次是 Animal 自身的部分,另一次是因为没有实现+initialize的 Dog 继承并执行了父类实现。因此,+initialize的惯用实现会加入类检查。

+ (void)initialize {
    if (self == [Animal class]) {
        // 这里只初始化真正属于 Animal 自己的部分
    }
}

顺便一提,+initialize本身就是线程安全的,因为运行时会按类加锁后再调用它。不需要额外叠加dispatch_once


实际选择标准

判断标准很简单。

  • 方法交换、类注册等“无条件、越早越好”的工作+load(但要保持最小化)
  • 只有使用该类时才需要的准备工作+initialize(必须检查 self)
  • 大多数初始化 → 其实两者都不是,使用dispatch_once单例或 lazy 属性就足够了

Swift 根本没有这个困扰。Swift 完全不提供与+load对应的机制,也没有官方途径在 main 之前插入全局可执行代码。相反,类型属性(static let)在语言层面保证 lazy 和线程安全,从而替代+initialize的作用。从应用启动性能来看,这是 Swift 在架构上的优势之一。

将 main 之前的 +load 和 lazy 的 +initialize 表现为赛道的插图
一方在起跑前就开始奔跑,另一方则睡到第一次调用才醒来

总结

  • +load在 main 之前无条件地通过函数指针直接调用 — 类和分类的实现都会分别调用
  • +initialize在第一条消息之前以 lazy 方式经由 msgSend 调用 — 未使用的类永远不会调用
  • +initialize可能因继承而执行多次,因此通常会进行if (self == [MyClass class])检查
  • 方法交换放在+load的原因:时机最早 + 不依赖分类调用 + 确保只执行一次
  • 滥用+load会直接拖慢应用启动时间 — 繁重初始化应延迟到 lazy 执行

结合运行时系列的其他文章(objc_msgSend、消息转发、方法交换、KVO)来看,可以发现+load+initialize的差异最终都归结为一个维度:调用是否经过 msgSend。理解调用路径后,就不需要死记规则。

延伸阅读