在iOS开发中,Objective-C类的加载是Runtime机制的核心环节,而map_images、load_images、initialize这三个关键流程,直接决定了类从"二进制文件"到"可使用实例"的转化过程。很多开发者只熟悉+load和+initialize方法,却对其底层依赖的map_images、load_images知之甚少,导致在处理方法交换、类初始化、启动优化等场景时踩坑。
本文将从"底层流程→调用时机→核心区别→实战示例"四个维度,彻底讲透iOS类加载的三个关键环节,结合可直接复制运行的代码示例,帮你精准掌握每个流程的作用与使用场景,避开常见陷阱。
前置说明:本文基于Objective-C Runtime源码(objc4-818.2)展开,聚焦OC类的加载流程,Swift类加载因底层有额外优化(如dyld缓存),逻辑略有差异,本文暂不展开;所有示例均适配iOS 13+,可直接在Xcode中运行。
一、先理清核心逻辑:类加载的整体流程
iOS APP启动时,类的加载并非一蹴而就,而是分阶段、按顺序执行的,整体流程可简化为:
APP启动 → dyld(动态链接器)加载可执行文件 → 调用Runtime的map_images(映射二进制文件中的类) → 调用load_images(触发类的+load方法) → 类首次使用时调用initialize(初始化类)
这三个流程(map_images、load_images、initialize)的调用时机有严格的先后顺序,且各自承担着不同的职责,缺一不可。下面逐一拆解每个流程的核心作用与调用时机。
二、map_images:类的"映射初始化"(最早执行)
1. 调用时机(核心重点)
map_images是Runtime初始化的第一个关键流程,在APP启动时,dyld加载完可执行文件(.mach-o)后,会立即调用map_images ,且只执行一次。
具体时机:早于所有类的+load方法,甚至早于APP的main函数(dyld在调用main前,会完成Runtime的初始化,而map_images是Runtime初始化的核心步骤)。
2. 核心作用
map_images的核心职责是"解析二进制文件中的类信息,并将其映射到内存中",具体做了3件事:
- 解析.mach-o文件中的
__objc_classlist段,提取所有类(Class)和分类(Category)的信息; - 将类的信息(如类名、父类、方法列表、属性列表等)初始化并存储到内存中,生成对应的Class结构体;
- 处理分类的加载,将分类中的方法、属性合并到主类的方法列表、属性列表中(这也是分类能"覆盖"主类方法的底层原因)。
简单来说:map_images负责"把二进制文件中的类,变成内存中可被Runtime识别的Class对象",是类加载的"第一步",没有map_images,后续的load_images和initialize都无法执行。
3. 实战示例:监听map_images流程
map_images是Runtime的底层函数,我们无法直接重写,但可以通过Runtime提供的_dyld_objc_notify_register函数,监听map_images的执行时机,验证其调用顺序。
objectivec
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
#import <dyld/dyld.h>
// 监听map_images、load_images、unmap_image的回调
static void notify_handler(const struct mach_header* mh, intptr_t vmaddr_slide) {
// 此回调会在map_images执行后触发
NSLog(@"map_images 执行完成,已完成类的映射");
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// 注册监听,监听map_images流程
_dyld_objc_notify_register(notify_handler, NULL, NULL);
// 后续执行main函数逻辑
appDelegateClassName = NSStringFromClass([AppDelegate class]);
NSLog(@"main函数执行");
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
运行结果(重点看顺序):
css
map_images 执行完成,已完成类的映射
main函数执行
结论:map_images确实在main函数之前执行,是类加载的第一个流程,完成了类的内存映射。
三、load_images:类的"加载触发"(中间执行)
1. 调用时机(核心重点)
load_images是紧随map_images之后执行的流程,在map_images完成类的映射后,立即调用load_images ,同样只执行一次。
具体时机:晚于map_images,早于main函数;核心作用是"触发所有类和分类的+load方法",因此+load方法的调用时机,本质就是load_images的执行时机。
补充细节:
- load_images会先调用所有类的
+load方法,再调用所有分类的+load方法; - 类的
+load方法调用顺序:父类 → 子类(先加载父类,再加载子类); - 分类的
+load方法调用顺序:按编译顺序执行(谁先编译,谁先执行)。
2. 核心作用
load_images的核心职责是"触发类和分类的+load方法",同时做一些类加载后的初始化工作,具体包括:
- 遍历所有已映射(通过map_images)的类,依次调用其
+load方法; - 遍历所有已映射的分类,依次调用其
+load方法; - 处理类的关联关系,确保类的方法、属性、协议已正确合并(分类的方法已合并到主类)。
注意:load_images本身不做类的初始化逻辑,只负责"触发+load方法",而+load方法是开发者唯一能在类加载阶段自定义逻辑的入口(如方法交换)。
3. 实战示例:验证load_images与+load的调用顺序
通过定义父类、子类、分类,重写它们的+load方法,验证load_images的执行顺序和+load的调用规则。
objectivec
#import <UIKit/UIKit.h>
// 父类
@interface ParentClass : NSObject
@end
@implementation ParentClass
+ (void)load {
NSLog(@"父类 ParentClass +load 执行");
}
@end
// 子类(继承自ParentClass)
@interface ChildClass : ParentClass
@end
@implementation ChildClass
+ (void)load {
NSLog(@"子类 ChildClass +load 执行");
}
@end
// 父类的分类
@interface ParentClass (Category)
@end
@implementation ParentClass (Category)
+ (void)load {
NSLog(@"父类分类 ParentClass+Category +load 执行");
}
@end
// 子类的分类
@interface ChildClass (Category)
@end
@implementation ChildClass (Category)
+ (void)load {
NSLog(@"子类分类 ChildClass+Category +load 执行");
}
@end
运行结果(重点看顺序):
lua
map_images 执行完成,已完成类的映射
父类 ParentClass +load 执行
子类 ChildClass +load 执行
父类分类 ParentClass+Category +load 执行
子类分类 ChildClass+Category +load 执行
main函数执行
结论:load_images在map_images之后、main函数之前执行,且+load调用顺序严格遵循"父类→子类→分类"的规则,与我们之前的结论一致。
延伸:方法交换通常在+load方法中执行,就是因为+load在类加载阶段只执行一次,且此时类已完成映射,能安全获取方法并交换(这也是之前方法交换博客中强调"在+load中执行交换"的底层原因)。
四、initialize:类的"首次使用初始化"(最后执行)
1. 调用时机(核心重点)
initialize与map_images、load_images的最大区别是:它不是在APP启动时执行,而是在"类首次被使用时"执行 ,且每个类只执行一次。
具体时机:
- 晚于map_images、load_images和main函数;
- 触发条件:当类被首次使用时(如创建实例、调用类方法、访问类属性);
- 调用顺序:父类 → 子类(若子类未重写initialize,会继承父类的initialize方法,但父类的initialize只会执行一次)。
关键陷阱:initialize不是"类加载时执行",而是"类首次使用时执行",这是很多开发者容易混淆的点(比如误以为initialize和+load一样在启动时执行)。
2. 核心作用
initialize的核心职责是"初始化类的静态变量、静态方法,或做类级别的准备工作",具体特点:
- 只执行一次:无论创建多少个类的实例,initialize只会执行一次;
- 懒加载特性:若类从未被使用,initialize不会执行(节省启动性能);
- 继承性:若子类未重写initialize,会调用父类的initialize,但父类的initialize只会执行一次(即使多个子类继承自父类,父类的initialize也只执行一次)。
3. 实战示例:验证initialize的调用时机
通过创建类、调用类方法、创建实例,验证initialize的触发条件和调用顺序。
objectivec
#import <UIKit/UIKit.h>
// 父类
@interface ParentClass : NSObject
+ (void)classMethod; // 类方法
@end
@implementation ParentClass
+ (void)initialize {
NSLog(@"父类 ParentClass initialize 执行");
}
+ (void)classMethod {
NSLog(@"父类 ParentClass 类方法执行");
}
@end
// 子类(继承自ParentClass)
@interface ChildClass : ParentClass
@end
@implementation ChildClass
// 子类未重写initialize
@end
// 测试代码(在main函数或ViewController中执行)
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
NSLog(@"main函数执行");
// 1. 首次调用父类的类方法(触发父类initialize)
NSLog(@"--- 首次调用父类类方法 ---");
[ParentClass classMethod];
// 2. 首次创建子类实例(触发子类initialize,因子类未重写,调用父类initialize,但父类已执行过,不再执行)
NSLog(@"--- 首次创建子类实例 ---");
ChildClass *child = [[ChildClass alloc] init];
// 3. 再次创建父类实例(父类initialize已执行,不再执行)
NSLog(@"--- 再次创建父类实例 ---");
ParentClass *parent = [[ParentClass alloc] init];
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
运行结果(重点看initialize的触发时机):
lua
map_images 执行完成,已完成类的映射
父类 ParentClass +load 执行
子类 ChildClass +load 执行
main函数执行
--- 首次调用父类类方法 ---
父类 ParentClass initialize 执行
父类 ParentClass 类方法执行
--- 首次创建子类实例 ---
子类 ChildClass 实例创建成功(未触发initialize,因父类已执行)
--- 再次创建父类实例 ---
父类 ParentClass 实例创建成功(未触发initialize)
结论:
- initialize在类首次使用时(调用类方法、创建实例)触发,晚于main函数;
- 父类的initialize先执行,子类未重写时,不会重复执行父类的initialize;
- initialize只执行一次,无论创建多少个实例,都不会重复执行。
五、三大流程核心区别(必记)
为了方便大家快速区分和记忆,整理了map_images、load_images、initialize的核心区别,用表格清晰呈现:
| 流程名称 | 调用时机 | 执行次数 | 核心作用 | 开发者可操作入口 |
|---|---|---|---|---|
| map_images | APP启动时,dyld加载可执行文件后,main函数前 | 全局只执行一次 | 映射二进制文件中的类到内存,解析类信息 | 无法直接重写,可通过_dyld_objc_notify_register监听 |
| load_images | map_images执行后,main函数前 | 全局只执行一次 | 触发所有类和分类的+load方法 | 重写类或分类的+load方法,执行自定义逻辑(如方法交换) |
| initialize | 类首次被使用时(创建实例、调用类方法),main函数后 | 每个类只执行一次 | 初始化类的静态变量、类级别的准备工作 | 重写类的+initialize方法,执行类初始化逻辑 |
六、常见陷阱与避坑建议(实战重点)
陷阱1:混淆initialize与+load的调用时机
错误认知:认为initialize和+load一样,在APP启动时执行。
后果:若在initialize中做启动相关的初始化(如注册服务),会导致初始化时机延迟,甚至无法触发(若类未被使用)。
避坑建议:启动时需要执行的逻辑(如方法交换、全局配置)放在+load中;类首次使用时需要执行的逻辑(如静态变量初始化)放在initialize中。
陷阱2:子类未重写initialize,导致父类initialize被重复调用
错误示例:子类未重写initialize,多次创建子类实例,误以为父类initialize会重复执行。
避坑建议:若子类需要自定义初始化逻辑,必须重写initialize;若不需要,无需重写,父类的initialize只会执行一次。
陷阱3:在initialize中创建类的实例,导致死循环
错误示例:在initialize中创建当前类的实例,会触发initialize再次调用,导致死循环。
objectivec
+ (void)initialize {
NSLog(@"initialize 执行");
// 错误:创建当前类实例,触发initialize再次调用,死循环
self *obj = [[self alloc] init];
}
避坑建议:initialize中禁止创建当前类的实例,也禁止调用会触发当前类初始化的方法。
陷阱4:认为map_images和load_images可以重写
错误认知:试图重写map_images或load_images方法,自定义类加载逻辑。
后果:map_images和load_images是Runtime的底层函数,开发者无法直接重写,强行重写会导致APP崩溃。
避坑建议:若需监听类加载流程,使用_dyld_objc_notify_register监听map_images;若需执行类加载时的自定义逻辑,重写+load方法。
七、总结:类加载流程的核心逻辑
iOS类的加载流程,本质是"从二进制文件到可使用类"的转化过程,三个核心流程的先后顺序和职责可总结为:
- map_images:"映射者",负责将二进制文件中的类映射到内存,完成类的基础初始化,是类加载的"基础";
- load_images :"触发者",负责触发类和分类的
+load方法,是开发者执行类加载阶段自定义逻辑的"入口"; - initialize:"初始化者",负责类首次使用时的初始化,是类级别的"懒加载初始化入口"。
掌握这三个流程的调用时机和核心作用,不仅能帮你避开类加载相关的坑,还能在做启动优化、方法交换、类初始化等场景时,做出更合理的技术选型(比如启动优化时,尽量减少+load中的耗时操作,将非必要逻辑延迟到initialize或类首次使用时执行)。