+load 和 +initialize 的区别?
+load 和 +initialize 是 Objective-C Runtime 中两个非常特殊的类方法,它们都用于类的初始化,但在调用时机、调用方式、继承规则以及分类(Category)处理上有显著的区别。
以下是详细的对比分析:
核心区别总结表
| 特性 | +load | +initialize |
|---|---|---|
| 调用时机 | 被添加到 Runtime 时(App 启动,main 函数之前) | 类接收到第一条消息前(懒加载) |
| 调用方式 | Runtime 直接获取函数指针调用 | objc_msgSend 消息发送机制 |
| 父类与子类 | 父类先调,子类后调。若子类未实现,不会调用父类的 load | 父类先调,子类后调。若子类未实现,会再次调用父类的 initialize |
| 分类 (Category) | 主类先调,分类后调。所有分类的 load 都会被执行 | 分类会覆盖主类的 initialize(只执行一个) |
| 执行次数 | 仅一次 | 理论上一次,但若子类未实现,父类代码可能运行多次 |
| 使用场景 | Method Swizzling (方法交换) | 初始化静态变量、全局状态 |
1. 详细解析 +load
调用时机:
当类或分类被加载到 Objective-C Runtime 时调用。通常发生在 App 启动阶段(pre-main),由 dyld 引导加载。
继承与调用顺序:
- 父类 -> 子类:Runtime 会自动保证父类的
+load先于子类执行。 - 主类 -> 分类:类的
+load先于分类的+load执行。 - 分类之间:取决于编译顺序(Build Phases -> Compile Sources 中的顺序)。
- 无继承性:如果子类没有实现
+load,Runtime 不会去调用父类的+load。
注意事项:
- 不要手动调用:永远不要手动写
[super load]或[self load]。 - 执行耗时:因为是在
main函数之前执行,如果在+load中进行耗时操作,会直接拖慢 App 的启动速度(Launch Time)。 - 依赖安全:在
+load中使用其他类是不安全的(除非是父类),因为其他类可能还没加载完成。 - 自动释放池:
+load方法运行时,系统通常还没有建立自动释放池(Autorelease Pool),如果产生大量临时对象,建议手动加@autoreleasepool。
典型应用:
- Method Swizzling:这是
+load最常用的场景,用于在类加载时 hook 系统方法。
2. 详细解析 +initialize
调用时机:
它是懒加载的。只有当类(或其子类)第一次接收到消息时(即第一次被使用时)才会调用。如果类一直没被用到,+initialize 就永远不会执行。
继承与调用顺序:
- 消息机制:它通过
objc_msgSend调用,遵循标准的 Objective-C 方法查找规则。 - 父类 -> 子类:Runtime 会确保在子类接收第一条消息前,其父类已经初始化完毕(即父类的
+initialize已经执行)。 - 子类未实现的情况:如果子类没有实现
+initialize,根据继承规则,会调用父类的+initialize。这意味着父类的+initialize可能会被执行多次(一次是父类自己初始化,一次是子类借用父类的实现)。
分类(Category)行为:
- 如果分类实现了
+initialize,它会覆盖主类的实现(这是 Objective-C 消息机制的特性)。主类的+initialize将不会被执行(除非分类中显式调用了super,但这在 initialize 中很少见)。
最佳实践代码:
为了防止父类的逻辑被子类重复触发,通常在实现时加上类型判断:
plaintext
+ (void)initialize {
if (self == [MyClass class]) {
// 只在当前类初始化时执行,子类触发时不执行
// 初始化静态变量等
}
}
线程安全:
Runtime 内部使用锁来保证 +initialize 在多线程环境下只被执行一次(针对同一个类),是线程安全的。
3. 面试高频追问
Q: 为什么 Method Swizzling 建议在 +load 中做,而不是 +initialize?
- 一致性:
+load在类加载时必定执行,且只执行一次。这保证了 Swizzling 一定生效且不会重复交换(重复交换会导致方法换回来了)。 - 覆盖风险:如果在
+initialize中做,如果分类重写了+initialize,主类的 Swizzling 逻辑就会被覆盖失效。 - 副作用:如果在
+initialize中做,子类未实现+initialize时会触发父类的逻辑,可能导致莫名其妙的多次交换。
Q: 如果一个类有多个分类,且都实现了 +load,会怎么样?
- 主类的
+load最先执行。 - 所有分类的
+load都会执行。 - 分类之间的执行顺序取决于 Compile Sources 中的文件顺序。
Q: 如果一个类有多个分类,且都实现了 +initialize,会怎么样?
- 只有最后编译的那个分类的
+initialize会被执行。 - 主类和其他分类的
+initialize都会被覆盖(Shadowed)。