为什么方法交换代码总是放在 +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 所属映像之外的其他类可能尚未加载,因此能执行的操作也受到限制。
+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 在架构上的优势之一。
总结
+load会在 main 之前无条件地通过函数指针直接调用 — 类和分类的实现都会分别调用+initialize会在第一条消息之前以 lazy 方式经由 msgSend 调用 — 未使用的类永远不会调用+initialize可能因继承而执行多次,因此通常会进行if (self == [MyClass class])检查- 方法交换放在
+load的原因:时机最早 + 不依赖分类调用 + 确保只执行一次 - 滥用
+load会直接拖慢应用启动时间 — 繁重初始化应延迟到 lazy 执行
结合运行时系列的其他文章(objc_msgSend、消息转发、方法交换、KVO)来看,可以发现+load和+initialize的差异最终都归结为一个维度:调用是否经过 msgSend。理解调用路径后,就不需要死记规则。

