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

今天 00:29 字数 12625 阅读 7

前言

在 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://juejin.cn/post/7503913955002941477,如有侵权,请联系删除。

0人点赞>
关注 收藏 改进 举报
0 条评论
排序方式 时间 投票
快来抢占一楼吧
请登录后发表评论