由 布多(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 (); prepare_load_methods ((const headerType *)info->mh, info->sectionLocationMetadata); } call_load_methods (); }
从上面的源码分析可以看出,+load 方法的实现过程主要分为两个步骤:
调用 prepare_load_methods
函数,将类和分类所有的 +load 方法准备好(其实就是将类和分类中的 +load 方法添加到一个全局数组中)。
调用 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); 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); 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); } } static void schedule_class_load (Class cls) { if (!cls) return ; if (cls->data ()->flags & RW_LOADED) return ; schedule_class_load (cls->getSuperclass ()); add_class_to_loadable_list (cls); cls->setInfo (RW_LOADED); } struct loadable_class { Class cls; IMP method; }; static struct loadable_class *loadable_classes = nil;void add_class_to_loadable_list (Class cls) { IMP method; method = cls->getLoadMethod (); if (!method) return ; 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; loadable_classes[loadable_classes_used].method = method; loadable_classes_used++; } struct loadable_category { Category cat; IMP method; }; static struct loadable_category *loadable_categories = nil;void add_category_to_loadable_list (Category cat) { IMP method; method = _category_getLoadMethod(cat); if (!method) return ; 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; loadable_categories[loadable_categories_used].method = method; loadable_categories_used++; }
通过分析源码,我们可以总结出 +load 方法的加载顺序规则:
系统会按照编译顺序遍历所有的类和分类;
对于类的 +load 方法,系统通过 schedule_class_load
函数递归处理,确保了父类的 +load 方法先于子类调用,这保证了继承链上的 +load 方法调用顺序是从父类到子类;
对于分类的 +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 { while (loadable_classes_used > 0 ) { call_class_loads (); } more_categories = call_category_loads (); } while (loadable_classes_used > 0 || more_categories); objc_autoreleasePoolPop (pool); loading = NO; } 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 ; 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_method)(cls, @selector (load)); } if (classes) free (classes); } 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 ; 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); if (cls && cls->isLoadable ()) { (*load_method)(cls, @selector (load)); cats[i].cat = nil; } } shift = 0 ; 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 ; } return new_categories_added; }
通过分析源码,我们可以总结出 +load 方法的调用顺序规则:
系统会先调用类中的 load 方法,然后再调用分类中的 load 方法;
系统在调用分类的 +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 方法。但实际运行结果却显示:
首先调用 Person 分类的 +load 方法
然后调用 Student 类的 +load 方法
最后调用 Student 分类的 +load 方法
更奇怪的是,当 Person 类有多个分类都实现了 +load 方法时,调用顺序又会回归到预期:先调用 Student 类的 +load 方法,然后按照编译顺序依次调用所有分类的 +load 方法。
这种特殊现象可能与 Runtime 在处理单个分类时的优化策略有关。当类只有一个分类时,系统可能采用了不同的处理路径,导致调用顺序发生变化。不过,由于这种差异并不影响程序的正确性,且在多分类场景下表现正常,所以这很可能是 Runtime 的一个实现细节,而非 bug。以上观点仅是我的个人猜测,如果大家有更深入的理解,欢迎在评论区分享你的见解。
+initialize 方法的实现机制 与 +load 方法不同,+initialize 方法是在类第一次接收到消息时才会被调用。这种延迟调用的机制使得 +initialize 方法更适合用于类的初始化工作,因为它可以确保类在被实际使用前完成必要的设置。在 Runtime 源码中,我们可以找到 +initialize 方法的具体实现细节,整理后的代码如下所示:
static ClassrealizeAndInitializeIfNeeded_locked (id inst, Class cls, bool initialize) { if (slowpath (!cls->isRealized ())) { cls = realizeClassMaybeSwiftAndLeaveLocked (cls, runtimeLock); } if (!cls || !cls->ISA ()) return nil; if (slowpath (initialize && !cls->isInitialized ())) { cls = initializeAndLeaveLocked (cls, inst, runtimeLock); } return cls; } static ClassinitializeAndLeaveLocked (Class cls, id obj, mutex_t & lock) { return initializeAndMaybeRelock (cls, obj, lock, true ); } static ClassinitializeAndMaybeRelock (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); } initializeNonMetaClass (nonmeta); if (leaveLocked) runtimeLock.lock (); return cls; } void initializeNonMetaClass (Class cls) { Class supercls = cls->getSuperclass (); if (supercls && !supercls->isInitialized ()) { initializeNonMetaClass (supercls); } lockClass (cls); if (cls->isInitialized ()) { unlockClass (cls); return ; } 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 { callInitialize (cls); } @catch (...) { @throw ; } @finally { lockAndFinishInitializing (cls, supercls); } } void callInitialize (Class cls) { objc_msgSend (cls, @selector (initialize)); }
通过分析 Runtime 源码,我们可以总结出 +initialize 方法的调用机制和特点:
调用顺序:系统会先调用父类的 +initialize 方法,再调用子类的 +initialize 方法,这保证了继承链上的初始化顺序是从父类到子类;
调用机制:+initialize 方法是通过 objc_msgSend 消息机制调用的,这意味着:
如果子类未实现 +initialize 方法,会调用父类的实现;
如果分类实现了 +initialize 方法,会覆盖类本身的实现;
由于是消息机制,所以支持运行时动态修改方法实现。
调用时机:+initialize 方法是在类第一次接收到消息时才会调用,而不是在类被加载到内存时就调用,这与 +load 方法有明显区别。
+load 和 +initialize 的异同 +load 和 +initialize 的相同点:
它们都是由系统自动调用,且正常情况下每个类只会执行一次;
它们在继承关系中的调用顺序是一致的,都是先调用父类方法,再调用子类方法;
+load 和 +initialize 的不同点:
基于以上特点,我们可以得出它们各自的最佳实践场景:
+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 的启动速度,因此在使用时需要权衡利弊。