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函数的加载过程,断点如下:

load断点

运行时,我们看一下函数调用栈:
函数调用栈

从调用栈中可以看得出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函数打上断点标志,如下:

ClassA+initialize.png

函数调用栈如下:
函数调用栈 下午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的启动过程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • Objective C类方法load和initialize的区别过去两个星期里,为了完成一个工作,接触到了NSOb...
    亦晴工作室阅读 1,278评论 0 10
  • load 和 initialize 两个方法算是两个特殊的类方法了,今天偶然从草稿箱中看到还有本篇未完成的博文,如...
    RITL阅读 1,414评论 8 13
  • load:当类被引用进程序的时候会执行这个函数。在一个程序开始运行之前(在main函数开始执行之前),在库开始被程...
    蜗牛也有梦想阅读 909评论 0 3
  • 《冬日咏火锅》 我看不到秋日的落叶洒街道, 我听不到夏日的鸣蝉绕耳闹, 我嗅不到春日的花香如年少, 我尝得...
    向阳楼阅读 352评论 0 2