iOS基础(九) - load和initialize的实现原理

之前在写《Category你真的懂吗?》那篇收集资料的时候,看了很多load和initialize的资料,加深了了解,打算写一篇记录一下。

load函数

1.load函数的加载时机

我们来看一下苹果官方文档的描述:

Invoked whenever a class or category is added to the Objective-C runtime.

当class或者category添加到runtime的时候会被唤醒。对于动态库和静态库中的class和category都有效。程序代码加载的顺序为:

1.调用所有Framework中的初始化函数
2.调用所有+load函数
3.调用C++静态初始化函数和C/C++ __attribute__(constructor)函数
4.调用所有链接到目标文件的Framework中的初始化函数

换句话来时,load方法是在这个文件被程序装载时调用,因此load方法是在main方法之前调用,看代码:

@implementation ClassA
+ (void)load{
    NSLog(@"load_class_a");
}
@end

@implementation ClassA (Addition)
+ (void)load {
    NSLog(@"load_class_a_addition");
}
@end

2017-04-07 10:17:26.298950+0800 BasicTest[27006:1840166] load_class_a
2017-04-07 10:17:26.299157+0800 BasicTest[27006:1840166] load_class_a_addition
2017-04-07 10:17:26.299199+0800 BasicTest[27006:1840166] main

2.load函数的调用顺序

看一下官方文档描述:

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.

也就是load函数的加载顺序为:superClass -> class -> category,我们用代码验证一下:

定义了一下类:
ClassA     
ClassASub    //ClassASub -> ClassA
ClassASub2    //ClassASub2 -> ClassA
ClassASubSub    //ClassASubSub -> ClassASub -> ClassA
ClassACategory    //ClassA(Category)
ClassASubCategory     //ClassASub(Category)

//输出
2017-04-07 14:07:33.481562+0800 BasicTest[32286:2163063] load_class_a
2017-04-07 14:07:33.481885+0800 BasicTest[32286:2163063] load_class_a_sub_2
2017-04-07 14:07:33.481907+0800 BasicTest[32286:2163063] load_class_a_sub
2017-04-07 14:07:33.481918+0800 BasicTest[32286:2163063] load_class_a_sub_sub
2017-04-07 14:07:33.481929+0800 BasicTest[32286:2163063] load_class_a_category
2017-04-07 14:07:33.481954+0800 BasicTest[32286:2163063] load_class_a_sub_category
2017-04-07 14:07:33.481999+0800 BasicTest[32286:2163063] main

很明显系统会先调用所有类的+load()方法,然后再根据类调用相应的category,并且也是父类的+load()方法先被调用。

3.load函数的作用和使用场景

由于load的调用时机比较早,通常是在App启动加载的时候开始,这时候并不能保证所有的类都被加载完成并且可以使用。并且load加载自身也存在不确定性,因为在有依赖关系的两个库中,被依赖的类的load方法会先调用,但是在一个库之内调用的顺序是不确定的。除此之外,load方法是线程安全的,因为内部实现加上了锁,但是也带来了一定的性能开销,所以不适合处理很复杂的事情。一般,会在load方法实现Method Swizzle(方法交换实现)

4.load函数的实现原理

在分析load函数实现原理之前,我们先打个断点,看一下load函数的加载过程,断点如下:

iOS基础(九) - load和initialize的实现原理_第1张图片
load断点

运行时,我们看一下函数调用栈:
iOS基础(九) - load和initialize的实现原理_第2张图片
函数调用栈

从调用栈中可以看得出 load_image函数开始加载,在 call_load_methods调用所有类的load方法。打开runtime源码,这里下载的是最新的 objc4-709,打开 objc-runtime-new.mm文件,找到load_image函数:

load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

我们可以看到,调用了prepare_load_methods()函数,提前准备需要调用的所欲load函数,看一下prepare_load_methods()函数内部实现,只是列出的关键代码:

void prepare_load_methods(const headerType *mhdr)
{
    Module mods;
    unsigned int midx;
    header_info *hi;
    ...

    // Major loop - process all modules in the image
    mods = hi->mod_ptr;
    for (midx = 0; midx < hi->mod_count; midx += 1)
    {
        unsigned int index;
        ...
        // Minor loop - process all the classes in given module
        for (index = 0; index < mods[midx].symtab->cls_def_cnt; index += 1)
        {
            // Locate the class description pointer
            Class cls = (Class)mods[midx].symtab->defs[index];
            if (cls->info & CLS_CONNECTED) {
                schedule_class_load(cls);
            }
        }
    }


    // Major loop - process all modules in the header
    mods = hi->mod_ptr;

    // NOTE: The module and category lists are traversed backwards 
    // to preserve the pre-10.4 processing order. Changing the order 
    // would have a small chance of introducing binary compatibility bugs.
    midx = (unsigned int)hi->mod_count;
    while (midx-- > 0) {
        unsigned int index;
        unsigned int total;
        Symtab symtab = mods[midx].symtab;
        ...
        // Minor loop - register all categories from given module
        index = total;
        while (index-- > mods[midx].symtab->cls_def_cnt) {
            old_category *cat = (old_category *)symtab->defs[index];
            add_category_to_loadable_list((Category)cat);
        }
    }
}

prepare_load_methods()函数里面首先获取的是class的load方法,最后才获取module里所有类的category的load方法,所以class和category中load函数记载的顺序是:class -> category。现在,我们来看schedule_class_load()函数:

static void schedule_class_load(Class cls)
{
    if (cls->info & CLS_LOADED) return;
    if (cls->superclass) schedule_class_load(cls->superclass);
    add_class_to_loadable_list(cls);
    cls->info |= CLS_LOADED;
}

这个方法是一个递归函数,保证父类先被添加进列表,然后才是类本身,所以superClass和class的load方法加载顺序是:superClass -> class。下面,我们看一下add_class_to_loadable_list()函数做了一些什么事:

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    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[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

loadable_classes_used是已经使用的内存,loadable_classes_allocated是分配的内存,loadable_classes则是一个全局的结构体,存放模块里所有的class的load函数。很明显这个方法就是将所有的load函数加入loadable_classes结构体存放。关于category的处理也一样,将所有的load函数存放在loadable_categories全局的结构体里。load函数加载完了,我们看一下load函数的调用,查找call_load_methods()函数:

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

    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
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

关键的方法call_class_loads()call_category_loads(),下面看一下call_class_loads()函数的实现:

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.
    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; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

上面代码其实就是从loadable_classes把load函数取出来,然后调用。值得注意的是 (*load_method)(cls, SEL_load),load方法是直接使用函数指针调用,也就是走C语言函数调用的流程,不是发送消息,并不会走消息转发的流程,也就是说,如果一个类实现了load函数就会调用,如果没有实现也不会调用该类的父类load函数实现,如果父类实现了load函数的话。category调用load方法也是一样的道理。

initialize

1. initialize函数的加载时机

苹果官网描述:

Initializes the class before it receives its first message.

这意味着名,这个函数是懒加载,只有当类接收了第一个消息的时候才会调用initialize函数,否则一直不会调用。

2.initialize函数的调用顺序

来自苹果官网的描述:

Superclasses receive this message before their subclasses.
The superclass implementation may be called multiple times if subclasses do not implement initialize.

initialize函数的调用顺序为:superClass -> class。这里没有分类,因为一个类的initialize函数只会调用一次,如果需要实现独立的class和category的初始化就需要实现load函数。还需要注意的一点就是,如果subClass没有实现initialize函数,则父类的initialize函数会被调用两次,代码如下:

//ClassA
@implementation ClassA
+ (void)initialize {
    NSLog(@"initial_class_a");
}
@end

//ClassASub
@implementation ClassASub
+ (void)initialize {
    NSLog(@"initial_class_a_sub");
}
@end

//ClassA (Addition)
@implementation ClassA (Addition)
+ (void)initialize {
    NSLog(@"initial_class_a_addition");
}
@end

//ClassB
@implementation ClassB
+ (void)initialize {
    NSLog(@"initial_class_b");
}
@end

//ClassBSub
@implementation ClassBSub
@end

//result
2017-04-09 16:15:21.919597+0800 BasicTest[44479:3187122] initial_class_a_addition
2017-04-09 16:15:21.919742+0800 BasicTest[44479:3187122] initial_class_a_sub
2017-04-09 16:15:21.919756+0800 BasicTest[44479:3187122] initial_class_b
2017-04-09 16:15:21.919763+0800 BasicTest[44479:3187122] initial_class_b

由于initialize函数可能会被调用多次,所以,如果想保证initialize函数只被调用一次,苹果建议这样做:

+ (void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}

3.initialize函数的使用场景

苹果官方文档:

The runtime sends the initialize message to classes in a thread-safe manner. 
That is, initialize is run by the first thread to send a message to a class, and any other thread that tries to send a message to that class will block until initialize completes.
Because initialize is called in a blocking manner, it’s important to limit method implementations to the minimum amount of work necessary possible. 
Specifically, any code that takes locks that might be required by other classes in their initialize methods is liable to lead to deadlocks. 
Therefore, you should not rely on initialize for complex initialization, and should instead limit it to straightforward, class local initialization.

initialize是线程安全的,有可能阻塞线程,所以,initialize函数应该限制做一些简单,不复杂的类初始化的前期准备工作。

4.initialize函数实现原理

我们先给ClassA的initialize函数打上断点标志,如下:

iOS基础(九) - load和initialize的实现原理_第3张图片
ClassA+initialize.png

函数调用栈如下:
iOS基础(九) - load和initialize的实现原理_第4张图片
函数调用栈 下午4.48.54.png

从函数调用栈可以看出,在调用 _class_initialize函数之前,调用了 lookUpImpOrForward函数,我们看一下 lookUpImpOrForward函数的实现:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();
    ...
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }
    ...    
}

在该函数里面调用了_class_initialize函数,看一下该函数的内部实现:

void _class_initialize(Class cls)
{
    assert(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;

    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }
    ...    
    if (reallyInitialize) {
            callInitialize(cls);
    }
    ...
}

_class_initialize函数中调用了callInitialize函数,其中调用顺序是从父类开始,到子类的。看一下callInitialize函数做了什么:

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

callInitialize函数的工作相当简单,就是发送消息,这是和load函数实现不一样的地方,load函数的调用直接是函数指针的调用,而initialize函数是消息的转发。所以,class的子类就算没有实现initialize函数,也会调用父类的initialize函数,如果子类实现了initialize函数,则子类不会调用父类的initialize函数

总结

通过分别对load和initialize源代码的实现细节,我们知道了它们各自的特点,总的如下:
1.load在被添加到runtime的时候加载,initialize是类第一次收到消息的时候被加载,load是在main函数之前,initialize是在main函数之后。
2.load方法的调用顺序是:superClass -> class -> category;initialize方法的调用顺序是:superClass -> class。都不需要显示调用父类的方法,系统会自动调用,load方法是函数指针调用,initialize是发送消息。子类如果没有实现load函数,子类是不会调用父类的load函数的,但是子类没有实现initialize函数,则会调用父类的initialize函数。
3.load和initialize内部实现都加了线程锁,是线程安全的,因此,这两个函数应该做一些简单的工作,不适合复杂的工作。
4.load函数通常用来进行Method Swizzle,initialize函数则通常初始化一些全局变量,静态变量。

参考:
细说OC中的load和initialize方法
load
initialize
Objective-C +load vs +initialize
Objective-C 深入理解 +load 和 +initialize
NSObject +load and +initialize - What do they do?
Notification Once
objc-runtime-new
iOS 程序 main 函数之前发生了什么
深入理解iOS App的启动过程

你可能感兴趣的:(iOS基础(九) - load和initialize的实现原理)