iOS 中的 +load 和 +initialize 在继承与分类中的不同表现​

由 布多(budo) 发布于 2025-05-14

前言

在 iOS 开发中,+load 和 +initialize 是两个特殊的类方法,它们都是由系统自动调用,且正常情况下每个类只会执行一次。虽然这两个方法看起来相似,但它们在调用时机、调用顺序以及继承和分类中的表现都有显著差异。+load 方法在程序启动时、类被加载到内存时就会调用,而 +initialize 方法则是在类第一次接收到消息时才会调用。本文将深入 Runtime 源码,详细分析这两个方法的底层实现机制,并重点探讨它们在继承关系和分类实现中的不同表现,帮助开发者更好地理解和使用它们。

+load 方法的实现机制

Runtime 源码中我们可以找到 +load 方法的具体实现细节,整理后的代码如下所示:

🔧 我在 这里 维护了一个可以直接运行调试的 Runtime 项目,欢迎大家下载调试源码。

void
load_images(const struct _dyld_objc_notify_mapped_info* info) {
{
mutex_locker_t lock2(runtimeLock);
loadAllCategoriesIfNeeded();
// 将类和分类中的 load 方法添加到全局数组中,等待后续调用。
prepare_load_methods((const headerType *)info->mh, info->sectionLocationMetadata);
}

// 调用所有的 load 方法。
call_load_methods();
}

从上面的源码分析可以看出,+load 方法的实现过程主要分为两个步骤:

  1. 调用 prepare_load_methods 函数,将类和分类所有的 +load 方法准备好(其实就是将类和分类中的 +load 方法添加到一个全局数组中)。
  2. 调用 call_load_methods 函数,遍历前面准备好的数组,并进行调用。

接下来,让我们深入分析 prepare_load_methods 函数的具体实现,看看它是如何收集这些 +load 方法的:

void 
prepare_load_methods(const headerType *mhdr, const _dyld_section_location_info_t info) {
size_t count, i;

classref_t const *classlist = getSectionData<classref_t>(mhdr,
info,
_dyld_section_location_data_non_lazy_class_list,
&count);
// 按照编译顺序遍历所有类并添加 load 方法。
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}

category_t * const *categorylist = getSectionData<category_t *>(mhdr,
info,
_dyld_section_location_data_non_lazy_category_list,
&count);
// 按照编译顺序遍历所有分类并添加 load 方法。
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue;
realizeClassWithoutSwift(cls, nil);
add_category_to_loadable_list(cat);
}
}

// 准备类的 load 方法。
static void schedule_class_load(Class cls) {
if (!cls) return;
// 检查该类的 load 方法是否已经添加过,防止重复添加。
if (cls->data()->flags & RW_LOADED) return;

/*
递归调用,确保父类的 load 方法先添加,
从而保证先调用父类的 load 方法再调用自身的 load 方法。
*/
schedule_class_load(cls->getSuperclass());

// 将当前类的 load 方法添加到全局数组中。
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}

struct loadable_class {
Class cls;
IMP method;
};

// 全局数组,用于存储所有需要调用的类的 load 方法。
static struct loadable_class *loadable_classes = nil;

// 将类的 load 方法添加到全局数组中。
void add_class_to_loadable_list(Class cls) {
IMP method;
method = cls->getLoadMethod();
if (!method) return;

// 第一次进来时先初始化全局数组,以确保它能容纳所有的 load 方法。
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)realloc(loadable_classes,
loadable_classes_allocated * sizeof(struct loadable_class));
}

loadable_classes[loadable_classes_used].cls = cls;
// 将 load 方法添加到全局数组中。
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}

struct loadable_category {
Category cat;
IMP method;
};

// 全局数组,用于存储所有需要调用的分类的 load 方法。
static struct loadable_category *loadable_categories = nil;

// 将分类的 load 方法添加到全局数组中。
void add_category_to_loadable_list(Category cat) {
IMP method;
method = _category_getLoadMethod(cat);
if (!method) return;

// 第一次进来时先初始化全局数组,以确保它能容纳所有的 load 方法。
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)realloc(loadable_categories,
loadable_categories_allocated * sizeof(struct loadable_category));
}

loadable_categories[loadable_categories_used].cat = cat;
// 将分类的 load 方法添加到全局数组中。
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}

通过分析源码,我们可以总结出 +load 方法的加载顺序规则:

  1. 系统会按照编译顺序遍历所有的类和分类;
  2. 对于类的 +load 方法,系统通过 schedule_class_load 函数递归处理,确保了父类的 +load 方法先于子类调用,这保证了继承链上的 +load 方法调用顺序是从父类到子类;
  3. 对于分类的 +load 方法,系统通过 add_category_to_loadable_list 函数按照编译顺序添加。

接下来,让我们继续分析 call_load_methods 函数的具体实现,看看系统是如何调用这些 +load 方法的:

void call_load_methods(void) {
static bool loading = NO;
bool more_categories;

// 防止重复调用。
if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {
// 先调用类的 +load 方法。
while (loadable_classes_used > 0) {
call_class_loads();
}

// 再调用分类的 +load 方法。
more_categories = call_category_loads();

// 如果类和分类中还有没调用的 +load 方法,则继续调用。
} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}

// 调用类所有的 +load 方法。
static void call_class_loads(void) {
int i;

struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;

// 遍历全局数组并调用所有需要调用的 +load 方法。
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
// 拿到 load 方法的函数指针进行调用。
(*load_method)(cls, @selector(load));
}

if (classes) free(classes);
}

// 调用分类所有的 +load 方法。
static bool call_category_loads(void) {
int i, shift;
bool new_categories_added = NO;

struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;

// 遍历全局数组并调用所有需要调用的 +load 方法。
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;

cls = _category_getClass(cat);
// 确保类的 +load 方法已经调用了再调用分类的 +load 方法。
if (cls && cls->isLoadable()) {
(*load_method)(cls, @selector(load));
cats[i].cat = nil;
}
}

shift = 0;
// 将分类中还未调用的 load 方法挪到数组前面。
for (i = 0; i < used; i++) {
if (cats[i].cat) {
cats[i-shift] = cats[i];
} else {
shift++;
}
}
used -= shift;

// 检查是否有通过运行时动态添加的分类。
new_categories_added = (loadable_categories_used > 0);
for (i = 0; i < loadable_categories_used; i++) {
if (used == allocated) {
allocated = allocated*2 + 16;
cats = (struct loadable_category *)realloc(cats,
allocated * sizeof(struct loadable_category));
}
cats[used++] = loadable_categories[i];
}

if (loadable_categories) free(loadable_categories);

// 如果有通过运行时新添加的分类,将它们赋值给全局数组,等待后续调用。
if (used) {
loadable_categories = cats;
loadable_categories_used = used;
loadable_categories_allocated = allocated;
} else {
if (cats) free(cats);
loadable_categories = nil;
loadable_categories_used = 0;
loadable_categories_allocated = 0;
}

// 如果有动态添加的分类,返回 YES,否则返回 NO。
return new_categories_added;
}

通过分析源码,我们可以总结出 +load 方法的调用顺序规则:

  1. 系统会先调用类中的 load 方法,然后再调用分类中的 load 方法;
  2. 系统在调用分类的 +load 方法时,会有诸多判断,比如是否已经调用过类的 +load 方法,是否有动态添加的分类等等。

在调试的过程中,我发现了一个奇怪的现象,当一个类没有实现 +load 方法,但是它的分类(有且只有一个)实现了 +load 方法时,调用顺序会发生变化。例如以下代码:

@implementation Person: NSObject

@end

@implementation Student: Person

+ (void)load {
NSLog(@"%s", __func__);
}

@end

@implementation Person (Category1)

+ (void)load {
NSLog(@"%s", __func__);
}

@end

@implementation Student (Category1)

+ (void)load {
NSLog(@"%s", __func__);
}

@end

在这个场景中,我观察到一个奇怪的调用顺序现象。根据 Runtime 源码的实现,理论上应该是先调用完所有类的 +load 方法,然后再调用分类的 +load 方法。但实际运行结果却显示:

  1. 首先调用 Person 分类的 +load 方法
  2. 然后调用 Student 类的 +load 方法
  3. 最后调用 Student 分类的 +load 方法

更奇怪的是,当 Person 类有多个分类都实现了 +load 方法时,调用顺序又会回归到预期:先调用 Student 类的 +load 方法,然后按照编译顺序依次调用所有分类的 +load 方法。

这种特殊现象可能与 Runtime 在处理单个分类时的优化策略有关。当类只有一个分类时,系统可能采用了不同的处理路径,导致调用顺序发生变化。不过,由于这种差异并不影响程序的正确性,且在多分类场景下表现正常,所以这很可能是 Runtime 的一个实现细节,而非 bug。以上观点仅是我的个人猜测,如果大家有更深入的理解,欢迎在评论区分享你的见解。

+initialize 方法的实现机制

与 +load 方法不同,+initialize 方法是在类第一次接收到消息时才会被调用。这种延迟调用的机制使得 +initialize 方法更适合用于类的初始化工作,因为它可以确保类在被实际使用前完成必要的设置。在 Runtime 源码中,我们可以找到 +initialize 方法的具体实现细节,整理后的代码如下所示:

static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize) {
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}

if (!cls || !cls->ISA()) return nil;

// 检查是否从未调用过 +initialize 方法。
if (slowpath(initialize && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
return cls;
}

static Class
initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock) {
return initializeAndMaybeRelock(cls, obj, lock, true);
}

static Class
initializeAndMaybeRelock(Class cls,
id inst,
mutex_t& lock, bool leaveLocked) {
if (cls->isInitialized()) {
if (!leaveLocked) lock.unlock();
return cls;
}

Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);

if (nonmeta->isRealized()) {
lock.unlock();
} else {
nonmeta = realizeClassMaybeSwiftAndUnlock(nonmeta, lock);
cls = object_getClass(nonmeta);
}

// 调用类的 +initialize 方法。
initializeNonMetaClass(nonmeta);

if (leaveLocked) runtimeLock.lock();
return cls;
}

void
initializeNonMetaClass(Class cls) {
Class supercls = cls->getSuperclass();
// 先调用父类的 +initialize 方法,然后再调用自身的 +initialize 方法。
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}

lockClass(cls);

if (cls->isInitialized()) {
unlockClass(cls);
return;
}

// 如果类正在其他线程调用 +initialize 方法。
if (cls->isInitializing()) {
if (!MultithreadedForkChild || _thisThreadIsInitializingClass(cls)) {
unlockClass(cls);// 避免死锁和重复调用。
return;
} else {
lockClass(cls);
_setThisThreadIsInitializingClass(cls);
performForkChildInitialize(cls, supercls);
}
}

SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
{
mutex_locker_t lock(classInitLock);
cls->setInitializing();
localWillInitializeFuncs.initFrom(willInitializeFuncs);
}

_setThisThreadIsInitializingClass(cls);

if (MultithreadedForkChild) {
performForkChildInitialize(cls, supercls);
return;
}

for (auto callback : localWillInitializeFuncs) {
callback.f(callback.context, cls);
}

@try {
// 调用类的 +initialize 方法。
callInitialize(cls);
} @catch (...) {
@throw;
} @finally {
lockAndFinishInitializing(cls, supercls);
}
}

void callInitialize(Class cls) {
// 通过消息机制向 cls 发送 initialize 消息。
objc_msgSend(cls, @selector(initialize));
}

通过分析 Runtime 源码,我们可以总结出 +initialize 方法的调用机制和特点:

  1. 调用顺序:系统会先调用父类的 +initialize 方法,再调用子类的 +initialize 方法,这保证了继承链上的初始化顺序是从父类到子类;

  2. 调用机制:+initialize 方法是通过 objc_msgSend 消息机制调用的,这意味着:

    • 如果子类未实现 +initialize 方法,会调用父类的实现;
    • 如果分类实现了 +initialize 方法,会覆盖类本身的实现;
    • 由于是消息机制,所以支持运行时动态修改方法实现。
  3. 调用时机:+initialize 方法是在类第一次接收到消息时才会调用,而不是在类被加载到内存时就调用,这与 +load 方法有明显区别。

+load 和 +initialize 的异同

+load 和 +initialize 的相同点:

  • 它们都是由系统自动调用,且正常情况下每个类只会执行一次;
  • 它们在继承关系中的调用顺序是一致的,都是先调用父类方法,再调用子类方法;

+load 和 +initialize 的不同点:

  • 调用时机不同:+load 在类被加载到内存时就会调用,而 +initialize 则是在类第一次接收到消息时才会调用。这个差异导致了两个重要影响:

    1. +load 方法一定会被调用,且是在 APP 启动过程中调用,因此会影响启动性能;而 +initialize 方法只有在类被使用时才会调用,如果类从未被使用则永远不会调用;
    2. +load 方法适合做全局性的初始化工作,而 +initialize 方法适合做类级别的初始化工作。
  • 调用机制不同:+load 方法是通过函数指针直接调用,而 +initialize 方法是通过消息机制调用。这个差异导致了:

    1. +load 方法在分类中的实现会与类本身的实现可以共存,而 +initialize 方法在分类中的实现会覆盖类本身的实现;
    2. 子类未实现 +initialize 方法时会调用父类的实现,而 +load 方法则不会。

基于以上特点,我们可以得出它们各自的最佳实践场景:

+load 方法适用于:

  • 需要在 APP 启动时就必须完成的全局初始化工作;
  • 框架的自动初始化,比如注册通知观察者、注册路由等;
  • 方法交换等运行时操作。

+initialize 方法适用于:

  • 类级别的初始化工作,比如初始化类的静态变量;
  • 需要根据运行时条件动态初始化的场景;
  • 希望延迟到类首次使用时才执行的初始化操作。

下面是一个使用 +load 方法实现框架自动初始化的示例:

+ (void)load {
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(appDidFinishLaunching) name:UIApplicationDidFinishLaunchingNotification object:nil];
}

+ (void)appDidFinishLaunching {
[NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidFinishLaunchingNotification object:nil];

// 在这里执行框架的初始化逻辑。
}

⚠️⚠️⚠️注意:由于 +load 方法是在程序启动前调用,所以它会降低 APP 的启动速度,因此在使用时需要权衡利弊。

文章作者: 布多
文章链接: https://budo.top/2025/05/14/iOS/iOS 中的 +load 和 +initialize 在继承与分类中的不同表现​/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 布多的博客