专注、坚持

iOS 中的 +load 方法

2019.09.13 by kingcos

0

Preface

在 iOS 开发中,我们经常会使用 +load 方法来做一些在 main 函数之前的操作,比如方法交换(Method Swizzle)等。那么本文就来简单了解下 iOS 中 +load 方法。

What

iOS 中的 +load 方法指的是 NSObject 中的 + (void)load 类方法:

// NSObject.h
@interface NSObject <NSObject> {
+ (void)load;
}

官方文档中的 +load 如下:

Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.

The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.

The order of initialization is as follows:

  1. All initializers in any framework you link to.
  2. All +load methods in your image.
  3. All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.
  4. All initializers in frameworks that link to you.

In addition:

  • A class’s +load method is called after all of its superclasses’ +load methods.
  • A category +load method is called after the class’s own +load method.

In a custom implementation of load you can therefore safely message other unrelated classes from the same image, but any load methods implemented by those classes may not have run yet.

Important

Custom implementations of the load method for Swift classes bridged to Objective-C are not called automatically.

—— Documentation, Apple Developer

译:

当类或分类被添加到 Obj-C 运行时的时候被调用;可以实现该方法用来在加载时刻执行特定类的操作。

动态加载和静态链接都能将 load 消息发送到类和分类,但前提是新加载类或分类实现了要响应的方法。

初始化的顺序如下:

  1. 链接的所有框架(Framework)中全部的构造器。
  2. 镜像(Image)中所有的 +load 方法。
  3. 镜像中所有的 C++ 静态构造器,以及 C/C++ 的 __attribute__(constructor) 函数。
  4. 框架中链接的所有构造器。

另外:

  • 类的 +load 方法在其所有父类的 +load 方法调用之后调用。
  • 分类的 +load 方法在其主类的 +load 方法调用之后调用。

load 的自定义实现中,可以安全地从同一镜像中发送其他不相关的类,但这些类中实现的 load 方法可能还没有运行。

重点

Swift 类中桥接到 Obj-C 的 load 方法自定义实现将不会自动调用。

—— 文档,苹果开发者

How

根据官方文档的描述,我们可以尝试定义一个继承自 NSObjectPerson 类,并对其添加两个分类 LifeWork;再定义一个 Student 类继承自 Person,并对其添加 School 分类。在以上所有类和分类中,均实现 +load:

// Person.m
#import "Person.h"

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

// Person+Life.m
#import "Person+Life.h"

@implementation Person (Life)
+ (void)load {
    NSLog(@"Person+Life %s", __func__);
}
@end

// Person+Work.m
#import "Person+Work.h"

@implementation Person (Work)
+ (void)load {
    NSLog(@"Person+Work %s", __func__);
}
@end

// Student.m
#import "Student.h"

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

// Student+School.m
#import "Student+School.h"

@implementation Student (School)
+ (void)load {
    NSLog(@"Student+School %s", __func__);
}
@end

// OUTPUT:
// Person +[Person load]
// Student +[Student load]
// Person+Life +[Person(Life) load]
// Student+School +[Student(School) load]
// Person+Work +[Person(Work) load]
// Hello, World!

不需要更改 main.m,尝试运行程序,结果正如官方所述,即 +load 方法会在 main 函数之前被调用;且调用顺序总是先父类再子类再分类

Why

实现原理

Obj-C 运行时的入口是哪里呢?

在 objc4 源码中有一个「libobjc.order」的文件,列举了该库方法符号的调用顺序,我们能看到第一个就是 __objc_init。编译时,C/C++ 方法前会被自动加上 _ 前缀作为方法符号(这一点可以在分析 Link Map 或者 Mach-O 时证明),因此这里第一个本质就是 _objc_init

1

为了证明上述结论,我们可以在 objc4 的源码中,从 Obj-C 运行时初始化的入口着手,即 _objc_init

// objc-os.mm
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* 引导初始化。使用 dyld 注册镜像通知器。
* Called by libSystem BEFORE library initialization time
* 在库初始化时间之前由 libSystem 调用
**********************************************************************/

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;

    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    // ➡️ dyld 注册通知;map_images:映射镜像,load_images:加载镜像,unmap_image:反映射镜像
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

// objc-runtime-new.mm
/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
* 由 dyld 处理给定将要映射的镜像中的 +load。
*
* Locking: write-locks runtimeLock and loadMethodLock
* 锁:写锁 runtimeLock 和 loadMethodLock
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    // ➡️ 如果没有 +load 方法则返回
    if (!hasLoadMethods((const headerType *)mh)) return;

    // 递归锁
    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    // 发现 load 方法
    {
        // 互斥锁
        mutex_locker_t lock2(runtimeLock);
        // ➡️ 准备加载 load 方法
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    // ➡️ 调用 +load 方法(不带 runtimeLock - 可重入的(线程安全的))
    call_load_methods();
}

// objc-runtime-new.mm
// Quick scan for +load methods that doesn't take a lock.
// 不需要加锁地快速搜索 +load 方法
bool hasLoadMethods(const headerType *mhdr)
{
    size_t count;
    // 非懒加载类列表(取 Mach-O 中 __objc_nlclslist 节)
    // GETSECT(_getObjc2NonlazyClassList,    classref_t,      "__objc_nlclslist");
    if (_getObjc2NonlazyClassList(mhdr, &count)  &&  count > 0) return true;
    // 非懒加载分类列表(取 Mach-O 中 __objc_nlcatlist 节)
    // GETSECT(_getObjc2NonlazyCategoryList, category_t *,    "__objc_nlcatlist");
    if (_getObjc2NonlazyCategoryList(mhdr, &count)  &&  count > 0) return true;
    return false;
}

// objc-runtime-new.mm
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    // 断言 runtimeLock 已锁
    runtimeLock.assertLocked();

    // 1. 非懒加载类列表
    classref_t *classlist =
        _getObjc2NonlazyClassList(mhdr, &count);
    // 遍历所有类
    for (i = 0; i < count; i++) {
        // ➡️ 计划 +load 方法调用
        schedule_class_load(remapClass(classlist[i]));
    }

    // 2. 非懒加载分类列表
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    // 遍历所有分类
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        // 重映射分类的主类
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class 忽略弱链接主类的分类
        // 实化类(为 cls 执行一次性初始化等操作)
        realizeClass(cls);
        // 断言类的 isa(元类)已实化
        assert(cls->ISA()->isRealized());
        // ➡️ 添加分类到可加载列表
        add_category_to_loadable_list(cat);
    }
}

// objc-runtime-new.mm
static Class remapClass(classref_t cls)
{
    ➡️ 重映射类
    return remapClass((Class)cls);
}

// objc-runtime-new.mm
/***********************************************************************
* remapClass
* Returns the live class pointer for cls, which may be pointing to
* a class struct that has been reallocated.
* 为参数 cls 返回可能指向已经重新分配类结构到活类指针。
* Returns nil if cls is ignored because of weak linking.
* 如果类是弱链接,将返回 nil。
* Locking: runtimeLock must be read- or write-locked by the caller
* 锁:runtimeLock 必须由调用者读取或写入锁定。
**********************************************************************/
static Class remapClass(Class cls)
{
    // 断言 runtimeLock 已锁
    runtimeLock.assertLocked();

    Class c2;

    if (!cls) return nil;

    NXMapTable *map = remappedClasses(NO);
    // 从 map 中取 cls 对应的值
    // void *NXMapMember(NXMapTable *table, const void *key, void **value)
    if (!map  ||  NXMapMember(map, cls, (void**)&c2) == NX_MAPNOTAKEY) {
        return cls;
    } else {
        return c2;
    }
}

// objc-runtime-new.mm
/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed
* superclasses in other images, and any categories in this image.
* 为当前镜像中的类、其它镜像中所有未调用过 +load 的父类、以及该镜像中的所有的分类计划调用 +load。
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// 为 cls 以及所有未调用过 +load 的父类递归计划调用 +load。
// cls must already be connected.
// cls 必须已被连接。
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    // 断言类是实化的
    assert(cls->isRealized());  // _read_images should realize

    // 判断类的 +load 是否已经被调用
    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    // (递归)确保父类优先调用 +load
    schedule_class_load(cls->superclass);

    // ➡️ 添加类到可加载列表
    add_class_to_loadable_list(cls);
    // 设置本类 +load 已调用过
    cls->setInfo(RW_LOADED);
}



// objc-loadmethod.mm
/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
* cls 刚刚被连接。如果其实现了 +load 方法就将计划调用。
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
    IMP method;

    // 断言 loadMethodLock 已锁
    loadMethodLock.assertLocked();

    // ➡️ 获取类 load 方法
    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method 没有 +load 就返回

    // Xcode 中 OBJC_PRINT_LOAD_METHODS 环境变量值为 YES 时,将可在控制台打印该信息
    // OPTION(PrintLoading, OBJC_PRINT_LOAD_METHODS, "log calls to class and category +load methods")
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load",
                     cls->nameForLogging());
    }

    // 如果使用的 == 分配的,需要扩容
    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(将在 call_class_loads 中实际调用)
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

// objc-runtime-new.mm
/***********************************************************************
* objc_class::getLoadMethod
* fixme
* Called only from add_class_to_loadable_list.
* 只在 add_class_to_loadable_list 中调用。
* Locking: runtimeLock must be read- or write-locked by the caller.
* 锁:runtimeLock 必须由调用者读取或写入锁定。
**********************************************************************/
IMP
objc_class::getLoadMethod()
{
    runtimeLock.assertLocked();

    const method_list_t *mlist;

    // 断言
    assert(isRealized());
    assert(ISA()->isRealized());
    assert(!isMetaClass());
    assert(ISA()->isMetaClass());

    // 从只读列表中读取 baseMethods(可参考「iOS 中的 NSObject」)
    mlist = ISA()->data()->ro->baseMethods();
    if (mlist) {
        // 遍历
        for (const auto& meth : *mlist) {
            // 获取 selector 名称
            const char *name = sel_cname(meth.name);
            if (0 == strcmp(name, "load")) {
                // 若为 load 方法,则返回 IMP
                return meth.imp;
            }
        }
    }

    return nil;
}

// objc-loadmethod.mm
/***********************************************************************
* add_category_to_loadable_list
* Category cat's parent class exists and the category has been attached
* to its class. Schedule this category for +load after its parent class
* becomes connected and has its own +load method called.
* 分类 cat 的主类存在,且分类已经连接到主类的类上了。在父类连接且调用了 +load 后计划调用该分类的 +load。
**********************************************************************/
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    // 断言 loadMethodLock 已锁
    loadMethodLock.assertLocked();

    // ➡️ 获取分类 load 方法
    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    // 没有 +load 就返回
    if (!method) return;

    // OPTION(PrintLoading, OBJC_PRINT_LOAD_METHODS, "log calls to class and category +load methods")
    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load",
                     _category_getClassName(cat), _category_getName(cat));
    }

    // 如果使用的 == 分配的,需要扩容
    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(将在 call_category_loads 中实际调用)
    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

// objc-runtime-new.mm
/***********************************************************************
* _category_getLoadMethod
* fixme
* Called only from add_category_to_loadable_list
* 只在 add_category_to_loadable_list 中调用
* Locking: runtimeLock must be read- or write-locked by the caller
* 锁:runtimeLock 必须由调用者读取或写入锁定。
**********************************************************************/
IMP
_category_getLoadMethod(Category cat)
{
    // 断言 loadMethodLock 已锁
    runtimeLock.assertLocked();

    const method_list_t *mlist;

    // 分类中类方法列表
    mlist = cat->classMethods;
    if (mlist) {
        // 遍历
        for (const auto& meth : *mlist) {
            // 获取 selector 名称
            const char *name = sel_cname(meth.name);
            if (0 == strcmp(name, "load")) {
                // 若为 load 方法,则返回 IMP
                return meth.imp;
            }
        }
    }

    return nil;
}

// objc-loadmethod.mm
/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.
* 调用所有类和分类的 +load  方法。
* Class +load methods are called superclass-first.
* 类的 +load 方法将先调用父类的。
* Category +load methods are not called until after the parent class's +load.
* 分类的 +load 才需要等到其主类的 +load 调用后才调用。
*
* This method must be RE-ENTRANT, because a +load could trigger
* more image mapping. In addition, the superclass-first ordering
* must be preserved in the face of re-entrant calls. Therefore,
* only the OUTERMOST call of this function will do anything, and
* that call will handle all loadable classes, even those generated
* while it was running.
* 该方法必须是可重入的(线程安全的),因为 +load 可能触发更多镜像映射。
* 另外,面对可重入调用时,必须保留父类优先排序。
* 因此,只有当该函数在最后调用才会执行全部操作,且该调用将处理所有可加载的类,甚至是在运行时生成的类。
*
* The sequence below preserves +load ordering in the face of
* image loading during a +load, and make sure that no
* +load method is forgotten because it was added during
* a +load call.
* 下面的顺序保留了在面对 +load 中加载镜像的 +load 排序,并且因为它是在一个 +load 调用中被添加的,因此可以保证没有 +load 被遗忘,。
* Sequence:
* 顺序
* 1. Repeatedly call class +loads until there aren't any more
* 1. 重复调用类 +load 直到不再有未调用的 +load
* 2. Call category +loads ONCE.
* 2. 调用分类 +load 一次
* 3. Run more +loads if:
* 3. 执行多次 +load 的情况:
*    (a) there are more classes to load, OR
*    (a) 有多个类要加载,或者
*    (b) there are some potential category +loads that have
*        still never been attempted.
*    (b) 有一些潜在的分类 +load 仍未尝试。
* Category +loads are only run once to ensure "parent class first"
* ordering, even if a category +load triggers a new loadable class
* and a new loadable category attached to that class.
* 分类中的 +load 只会被执行一次,并保证「父类优先」排序,尽管一个分类的 +load 触发了一个新的可加载的类以及附加该类的一个新的可加载的分类。
*
* Locking: loadMethodLock must be held by the caller
* 锁:loadMethodLock 必须由调用者持有
*   All other locks must not be held.
*   所有其它锁必须不持有。
**********************************************************************/
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    // 断言 loadMethodLock 已锁
    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    // 重入的调用不做事情,最早的调用将完成任务
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        // 1. 重复调用类 +load,直到全部调用到
        while (loadable_classes_used > 0) {
            // ➡️ 调用类的 +load 方法
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        // 2. ➡️ 调用分类 +load 一次
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
        // 3.如果有类或者更多未尝试的分类则运行更多的 +load
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

// objc-loadmethod.mm
/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* 调用所有类的 +load 方法。
* If new classes become loadable, +load is NOT called for them.
* 如果有新的类变为可加载的,它们的 +load 不会被调用
*
* Called only by call_load_methods().
* 只能由 call_load_methods() 调用。
**********************************************************************/
static void call_class_loads(void)
{
    int i;

    // Detach current loadable list.
    // 分离当前可加载列表。
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;

    // Call all +loads for the detached list.
    // 在分离的表中调用所有 +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;

        // OPTION(PrintLoading, OBJC_PRINT_LOAD_METHODS, "log calls to class and category +load methods")
        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        // 💡 调用 +load
        (*load_method)(cls, SEL_load);
    }

    // Destroy the detached list.
    // 销毁分离列表
    if (classes) free(classes);
}

// objc-loadmethod.mm
/***********************************************************************
* call_category_loads
* Call some pending category +load methods.
* 调用一些分类中未调用的 +load 方法。
* The parent class of the +load-implementing categories has all of
*   its categories attached, in case some are lazily waiting for +initalize.
* 实现 +load 分类的父类拥有所有其附加的分类 ,来防止等待 +initialize 懒加载。
*
* Don't call +load unless the parent class is connected.
* If new categories become loadable, +load is NOT called, and they
*   are added to the end of the loadable list, and we return TRUE.
* Return FALSE if no new categories became loadable.
* 除非父类已经连接,否则不要调用 +load。
* 如果新的分类变成可加载的,+load 未被调用,并且它们被添加到可加载列表的末尾,返回 TRUE。
* 如果没有新的分类变为可加载的,返回 FALSE。
*
* Called only by call_load_methods().
* 只能由 call_load_methods() 调用。
**********************************************************************/
static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;

    // Detach current loadable list.
    // 分离当前可加载列表。
    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;

    // Call all +loads for the detached list.
    // 为分离的列表调用所有的 +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);
        if (cls  &&  cls->isLoadable()) {
            // OPTION(PrintLoading, OBJC_PRINT_LOAD_METHODS, "log calls to class and category +load methods")
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n",
                             cls->nameForLogging(),
                             _category_getName(cat));
            }
            // 💡 调用 +load
            (*load_method)(cls, SEL_load);
            // 置为空
            cats[i].cat = nil;
        }
    }

    // Compact detached list (order-preserving)
    // 压缩分离列表(保留顺序)
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[i];
        } else {
            shift++;
        }
    }
    used -= shift;

    // Copy any new +load candidates from the new list to the detached list.
    // 从新的列表中拷贝所有新的 +load 候选到分离列表。
    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];
    }

    // Destroy the new list.
    // 销毁新列表
    if (loadable_categories) free(loadable_categories);

    // Reattach the (now augmented) detached list.
    // 重新附加(现在已增加的)分离列表。
    // But if there's nothing left to load, destroy the list.
    // 但是如果没有什么要加载的,则销毁该列表。
    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;
    }

    if (PrintLoading) {
        if (loadable_categories_used != 0) {
            _objc_inform("LOAD: %d categories still waiting for +load\n",
                         loadable_categories_used);
        }
    }

    return new_categories_added;
}

上面的源码过程比较复杂,其实主要顺序就是先在 prepare_load_methods 中调用 schedule_class_load,先计划类的 +load 方法调用顺序,在其中会递归找到基类,并 add_class_to_loadable_list 将类中 +load 的 IMP 存储在 loadable_classes;之后在 prepare_load_methods 中调用 add_category_to_loadable_list,再将分类中 +load 的 IMP 存储在 loadable_categories;当类和分类的可加载列表确定后,预备(Prepare)工作完成;之后开始 call_load_methods,调用 call_class_loads 将类中的 +load 全部调用完毕,再调用 call_category_loads 将分类中的 +load 全部调用完毕。这也就完全符合了我们上述的结论,至于毫无关系的类之间以及分类相互之间的调用顺序,由于 loadable_classesloadable_categories 的增加都是按顺序追加,即按照编译顺序来决定,先编译先调用。具体的编译顺序可以在 Xcode -「TARGETS」 -「Build Phases」-「Compile Sources」中找到。

2

IMP 与 +load

按照我们之前在「iOS 中的 Category」一篇所提到的,当分类中定义了和主类中相同的方法,那么在调用时将选择最后被编译的分类中的实现。因为在运行时,分类中的方法最终将附加到主类或主类元类的方法列表中,最终将返回第一个找到的方法。那么为什么 +load 如此特殊呢?

// add_class_to_loadable_list
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;

// objc-loadmethod.mm
typedef void(*load_method_t)(id, SEL);

load_method_t load_method = (load_method_t)classes[i].method;
(*load_method)(cls, SEL_load);

我们知道,Obj-C 是一门消息机制语言,所谓方法调用是不严谨的,其本质是消息发送(Message Sending),即 objc_msgSend。当「调用」对象方法时,类对象接收到消息作出响应;当「调用」类方法时,元类对象接收到消息作出响应。而在 +load 最终的调用处只是 (*load_method)(cls, SEL_load),并不存在消息的发送。我们在 add_class_to_loadable_list 中也可以看到,+load 方法本质是存储了方法指针(IMP)并直接调用,因此不会出现类似「覆盖」的情形,而且效率会更高。

#import "Student.h"

@implementation Student
+ (void)load {
    [super load];
    NSLog(@"Student %s", __func__);
}
@end

// OUTPUT:
// Person +[Person load]
// Person+Work +[Person(Work) load]
// Student +[Student load]
// Person+Life +[Person(Life) load]
// Student+School +[Student(School) load]
// Person+Work +[Person(Work) load]
// Hello, World!

那么有了上述基础,当我们在一个类的子类的 +load 中 [super load] 又会调用到到底哪个类中呢(当然,在实际开发中这种情几乎不可能存在)?答案就是 [super load] 将调用到 Person 最后一个被编译的分类(Person+Work)中的 +load 方法,因为这里是消息发送,而不是通过方法指针。

OBJC_PRINT_LOAD_METHODS

在 objc4 的源码中提供了很多环境变量,方便我们 Debug 时输出一些内部源码的执行信息。OBJC_PRINT_LOAD_METHODS 即是 +load 相关的一个环境变量,将 OBJC_PRINT_LOAD_METHODS 在 Xcode 中设置为 YES 后,将会打印出很多 +load 方法执行时的信息。从下面的信息我们也可以得知,Obj-C 内也广泛使用了 +load,而因为我们自定义的类会在最晚被编译,因此也会在最晚去调用:

-> 点击此处即可查看完整内容 <-
objc[63382]: LOAD: class 'NSObject' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_queue' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_source' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_mach' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_queue_serial' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_queue_runloop' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_semaphore' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_group' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_workloop' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_queue_concurrent' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_queue_main' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_queue_global' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_queue_pthread_root' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_queue_mgr' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_queue_attr' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_mach_msg' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_io' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_operation' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_disk' scheduled for +load
objc[63382]: LOAD: class 'OS_voucher' scheduled for +load
objc[63382]: LOAD: class 'OS_dispatch_data_empty' scheduled for +load
objc[63382]: LOAD: +[NSObject load]

objc[63382]: LOAD: +[OS_dispatch_queue load]

objc[63382]: LOAD: +[OS_dispatch_source load]

objc[63382]: LOAD: +[OS_dispatch_mach load]

objc[63382]: LOAD: +[OS_dispatch_queue_serial load]

objc[63382]: LOAD: +[OS_dispatch_queue_runloop load]

objc[63382]: LOAD: +[OS_dispatch_semaphore load]

objc[63382]: LOAD: +[OS_dispatch_group load]

objc[63382]: LOAD: +[OS_dispatch_workloop load]

objc[63382]: LOAD: +[OS_dispatch_queue_concurrent load]

objc[63382]: LOAD: +[OS_dispatch_queue_main load]

objc[63382]: LOAD: +[OS_dispatch_queue_global load]

objc[63382]: LOAD: +[OS_dispatch_queue_pthread_root load]

objc[63382]: LOAD: +[OS_dispatch_queue_mgr load]

objc[63382]: LOAD: +[OS_dispatch_queue_attr load]

objc[63382]: LOAD: +[OS_dispatch_mach_msg load]

objc[63382]: LOAD: +[OS_dispatch_io load]

objc[63382]: LOAD: +[OS_dispatch_operation load]

objc[63382]: LOAD: +[OS_dispatch_disk load]

objc[63382]: LOAD: +[OS_voucher load]

objc[63382]: LOAD: +[OS_dispatch_data_empty load]

objc[63382]: LOAD: class 'OS_os_log' scheduled for +load
objc[63382]: LOAD: class 'OS_os_activity' scheduled for +load
objc[63382]: LOAD: +[OS_os_log load]

objc[63382]: LOAD: +[OS_os_activity load]

objc[63382]: LOAD: class 'OS_xpc_connection' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_service' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_null' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_bool' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_double' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_pointer' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_date' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_data' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_string' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_uuid' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_fd' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_shmem' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_mach_send' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_array' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_dictionary' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_error' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_endpoint' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_serializer' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_pipe' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_mach_recv' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_bundle' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_service_instance' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_activity' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_file_transfer' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_int64' scheduled for +load
objc[63382]: LOAD: class 'OS_xpc_uint64' scheduled for +load
objc[63382]: LOAD: +[OS_xpc_connection load]

objc[63382]: LOAD: +[OS_xpc_service load]

objc[63382]: LOAD: +[OS_xpc_null load]

objc[63382]: LOAD: +[OS_xpc_bool load]

objc[63382]: LOAD: +[OS_xpc_double load]

objc[63382]: LOAD: +[OS_xpc_pointer load]

objc[63382]: LOAD: +[OS_xpc_date load]

objc[63382]: LOAD: +[OS_xpc_data load]

objc[63382]: LOAD: +[OS_xpc_string load]

objc[63382]: LOAD: +[OS_xpc_uuid load]

objc[63382]: LOAD: +[OS_xpc_fd load]

objc[63382]: LOAD: +[OS_xpc_shmem load]

objc[63382]: LOAD: +[OS_xpc_mach_send load]

objc[63382]: LOAD: +[OS_xpc_array load]

objc[63382]: LOAD: +[OS_xpc_dictionary load]

objc[63382]: LOAD: +[OS_xpc_error load]

objc[63382]: LOAD: +[OS_xpc_endpoint load]

objc[63382]: LOAD: +[OS_xpc_serializer load]

objc[63382]: LOAD: +[OS_xpc_pipe load]

objc[63382]: LOAD: +[OS_xpc_mach_recv load]

objc[63382]: LOAD: +[OS_xpc_bundle load]

objc[63382]: LOAD: +[OS_xpc_service_instance load]

objc[63382]: LOAD: +[OS_xpc_activity load]

objc[63382]: LOAD: +[OS_xpc_file_transfer load]

objc[63382]: LOAD: +[OS_xpc_int64 load]

objc[63382]: LOAD: +[OS_xpc_uint64 load]

objc[63382]: LOAD: class '__IncompleteProtocol' scheduled for +load
objc[63382]: LOAD: class 'Protocol' scheduled for +load
objc[63382]: LOAD: class '__NSUnrecognizedTaggedPointer' scheduled for +load
objc[63382]: LOAD: +[__IncompleteProtocol load]

objc[63382]: LOAD: +[Protocol load]

objc[63382]: LOAD: +[__NSUnrecognizedTaggedPointer load]

objc[63382]: LOAD: category 'NSObject(NSObject)' scheduled for +load
objc[63382]: LOAD: +[NSObject(NSObject) load]

objc[63382]: LOAD: category 'NSObject(NSObject)' scheduled for +load
objc[63382]: LOAD: +[NSObject(NSObject) load]

objc[63382]: LOAD: category 'CIFilter(Interposer)' scheduled for +load
objc[63382]: LOAD: +[CIFilter(Interposer) load]

objc[63382]: LOAD: class 'NSApplication' scheduled for +load
objc[63382]: LOAD: class 'NSBinder' scheduled for +load
objc[63382]: LOAD: class 'NSColorSpaceColor' scheduled for +load
objc[63382]: LOAD: class 'NSNextStepFrame' scheduled for +load
objc[63382]: LOAD: category 'NSBundle(NSNibLoading)' scheduled for +load
objc[63382]: LOAD: +[NSApplication load]

objc[63382]: LOAD: +[NSBinder load]

objc[63382]: LOAD: +[NSColorSpaceColor load]

objc[63382]: LOAD: +[NSNextStepFrame load]

objc[63382]: LOAD: +[NSBundle(NSNibLoading) load]

objc[63382]: LOAD: class 'Person' scheduled for +load
objc[63382]: LOAD: class 'Student' scheduled for +load
objc[63382]: LOAD: category 'Person(Life)' scheduled for +load
objc[63382]: LOAD: category 'Student(School)' scheduled for +load
objc[63382]: LOAD: category 'Person(Work)' scheduled for +load
objc[63382]: LOAD: +[Person load]

2019-04-20 19:25:30.541922+0800 Load_Obj-C_Demo[63382:15374262] Person +[Person load]
objc[63382]: LOAD: +[Student load]

2019-04-20 19:25:30.542367+0800 Load_Obj-C_Demo[63382:15374262] Person+Work +[Person(Work) load]
2019-04-20 19:25:30.542379+0800 Load_Obj-C_Demo[63382:15374262] Student +[Student load]
objc[63382]: LOAD: +[Person(Life) load]

2019-04-20 19:25:30.542409+0800 Load_Obj-C_Demo[63382:15374262] Person+Life +[Person(Life) load]
objc[63382]: LOAD: +[Student(School) load]

2019-04-20 19:25:30.542443+0800 Load_Obj-C_Demo[63382:15374262] Student+School +[Student(School) load]
objc[63382]: LOAD: +[Person(Work) load]

2019-04-20 19:25:30.542473+0800 Load_Obj-C_Demo[63382:15374262] Person+Work +[Person(Work) load]
2019-04-20 19:25:30.542535+0800 Load_Obj-C_Demo[63382:15374262] Hello, World!

Others

开销

@interface Fruit : NSObject

@end

@implementation Fruit
+ (void)load {
    NSLog(@"------");
    sleep(3);
    NSLog(@"------");
}
@end

// OUTPUT:
// 2019-04-20 19:23:38.433375+0800 Load_Obj-C_iOS_Demo[63358:15371945] ------
// 2019-04-20 19:23:41.434564+0800 Load_Obj-C_iOS_Demo[63358:15371945] ------

通过上面的分析,我们了解了 +load 在运行时初始化加载镜像时就会被调用,使得可以有机会预先做很多事情。但正是因为其加载的时机非常靠前,如果在 +load 方法中做比较复杂且在主线程的操作,将会影响 app 启动时间,降低用户体验。我们可以尝试建立一个 iOS app,在 +load 中 sleep(3) 模拟做比较复杂的操作,此时每次 app 冷启动都将会有 3 秒以上的时间才能进入 app 主页的控制器,极大的影响了启动时间。

我们可以使用 DYLD_PRINT_STATISTICS_DETAILS 环境变量来控制输出启动统计的相关信息,就可以在 total time in initializers and ObjC +load 中发现 Load_Obj-C_iOS_Demo : 3.0 seconds (74.4%) 占用了大部分时间。

  total time: 4.0 seconds (100.0%)
  total images loaded:  258 (0 from dyld shared cache)
  total segments mapped: 767, into 102430 pages with 7398 pages pre-fetched
  total images loading time: 397.50 milliseconds (9.8%)
  total load time in ObjC:  47.34 milliseconds (1.1%)
  total debugger pause time: 189.60 milliseconds (4.6%)
  total dtrace DOF registration time:   0.14 milliseconds (0.0%)
  total rebase fixups:  2,716,086
  total rebase fixups time: 552.17 milliseconds (13.6%)
  total binding fixups: 286,035
  total binding fixups time:  19.12 milliseconds (0.4%)
  total weak binding fixups time:   0.45 milliseconds (0.0%)
  total redo shared cached bindings time:  26.04 milliseconds (0.6%)
  total bindings lazily fixed up: 0 of 0
  total time in initializers and ObjC +load: 3.0 seconds (74.9%)
                         libSystem.B.dylib :   2.99 milliseconds (0.0%)
                libMainThreadChecker.dylib :  10.24 milliseconds (0.2%)
                       Load_Obj-C_iOS_Demo : 3.0 seconds (74.4%)

Swift

与我们的老朋友 Obj-C 不同的是,Swift 是一门诞生即宣扬安全、快速、强壮的语言,因此其极大地削弱了 Runtime 这一概念,将大部分检查提前到编译时刻。因此 Swift 中普通的类无需继承自 NSObject,自然也不存在 +load。

而当我们尝试在 Swift 5.x 中继承自 NSObject 来实现 +load 和 +initialize 时,却发现编译器现在已经不允许了:

class Foo: NSObject {
    // Method 'load()' defines Objective-C class method 'load', which is not permitted by Swift
    static func load() {}

    // Method 'initialize()' defines Objective-C class method 'initialize', which is not permitted by Swift
    static func initialize() {}
}

与以往不同,这次官方连变通方法也没有提供,看来确实不鼓励在 app 启动的 main 函数调用前做太多开销的事情。

我们首先能想到的是使用 Obj-C 混编来定义分类并实现 +load 来达到类似的效果,然而官方已经料到了这一点:「Swift class extensions and categories on Swift classes are not allowed to have +load methods」,不过 +initialize 目前仍是允许的:

@interface SwiftFoo (SwiftInitialize)
@end

@implementation SwiftFoo (SwiftInitialize)
+ (void)initialize {
    [SwiftFoo swiftInitialize];
    // 或者直接做一些处理
}
@end
class SwiftFoo: NSObject {
    @objc static func swiftInitialize() {
        // Do sth like in +initialize...
    }
}

或者我们也可以根据「Handling the Deprecation of initialize() - JORDAN SMITH」一文中使用响应链并使用运行时兼容所有非 NSObject 类也可支持类似的效果:

protocol SelfAware: class {
    static func awake()
}

extension UIApplication {
    private static let runOnce: Void = {
        let typeCount = Int(objc_getClassList(nil, 0))
        let types = UnsafeMutablePointer<AnyClass>.allocate(capacity: typeCount)
        let safeTypes = AutoreleasingUnsafeMutablePointer<AnyClass>(types)
        objc_getClassList(safeTypes, Int32(typeCount))
        for index in 0 ..< typeCount { (types[index] as? SelfAware.Type)?.awake() }
        types.deallocate()
    }()

    override open var next: UIResponder? {
        // Called before applicationDidFinishLaunching
        UIApplication.runOnce
        return super.next
    }

}

class FooAware: SelfAware {
    static func awake() {
        // Do sth like in +load...
    }
}

Other Linker Flags

在编译型语言中,编译器负责将源代码编译为目标文件,而链接器负责将多个目标文件在 Xcode - Targets - Build Settings 中有一个「Other Linker Flags」的配置项:

3

其使得开发者可以在 Xcode 构建项目时自由添加一些链接器参数,关于其完整的参数列表,可以在终端输入 man ld 来获得:

4

我们这里主要简单介绍下 -ObjC,根据 man ld

Options that control libraries

-ObjC Loads all members of static archive libraries that implement an Objective-C class or category.

-ObjC 加载静态库中实现 Obj-C 类或分类的所有成员。

由于 Obj-C 中的方法调用本质属于消息发送,因此链接的符号只有类,而没有方法。分类可以看作是方法的集合,因此使用分类的方法时并不会产生未定义符号,即链接器并不知晓要加载定义分类的目标文件。当我们使用 -ObjC 参数时,将会把静态库中所有的分类方法实现加载,但坏处是会使得可执行文件变大,并可能包含不必要的目标文件。

而对于在运行时加载的动态库,我们只要保证代码中引入了库,那么其中的 +load 方法就能够得以执行。

Reference