load 与 initialize 方法

+load+initialize 的异同

  1. +load 方法会在 main() 函数之前调用,而 +initialize 是在类第一次使用时才会调用

  2. +load 方法调用优先级:父类>子类>分类,并且不会被覆盖,均会调用

  3. +initialize 调用优先级:分类>父类,父类>子类,父类的分类重写了 +initialize方法会覆盖父类的 +initialize 方法。即:

    • 如果分类和父类均实现了 +initialize,则只有分类的 +initialize 会被调用;
    • 如果父类和子类均实现了 +initialize,第一次引用 子类时,先调用父类的 +initialize,再调用子类的 +initialize
    • 如果父类实现了 +initialize,则第一次引用子类时,会调用两次父类的 +initialize
  4. +load 方法在 main() 函数之前调用,所有的类文件都会加载,分类也会加载

  5. 均无须显式调用 super 方法

+load 方法

0x01 load 方法的加载时机

苹果文档描述如下

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

当 class 或者 category 添加到 runtime 时被唤醒。即 +load 是在这个文件被程序加载时调用,因此 +load 方法是在 main 函数以前调用

0x02 load 方法调用顺序
// 定义了如下的类
// 1. TestModel
// 2. TestModel 的子类 SubTestModel
// 3. TestModel Category 类

// 输出如下
TestModel load
SubTestModel load
TestModel Category load

可以看到系统调用顺序是父类->子类->分类

0x03 load 方法的使用场景

由于 +load 方法在 App 启动加载的时候调用,此时不能保证所有的类被加载完成。

+load 方法是线程安全的,因为内部有锁,但是也带了一定的性能开销。所以一般会在 +load 方法中实现 Method Swizzle

0x04 load 方法实现原理

打断点查看调用栈

打断点可以看到 load_images 函数开始加载,在 call_load_methods 调用所有类的 +load 方法。打开 runtime 源码,这里下载的是 objc4-709,找到 objc-runtime-new.mm 文件,找到 load_images 函数。

void
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)
{
    size_t count, i;

    runtimeLock.assertWriting();

    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    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
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

可以看到 prepare_load_methods 内部先获取 class 的 load 方法,然后才获取 category 中的 load 方法。因此 load 方法的加载顺序为 class->category

继续看内部调用的 schedule_class_load() 函数

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_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 是一个全局的结构体, add_class_to_loadable_list 作用是将模块里所有类的 load 函数存放到loadable_classes 中。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() 。可以看出,在所有类的 load 方法调用完毕,才会调用 category 的 load 方法

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 函数。

+initialize 方法

0x01 initialize 方法加载时机

苹果官网描述

Initializes the class before it receives its first message.

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

0x02 initialize 方法的调用顺序

父类->子类,分类会覆盖类,如果子类没有实现 initialize 方法,父类会调用两次

  1. 子类实现了 initialize,会先调用父类 initialize,再调用子类 initialize
  2. 子类没有实现 initialize,父类 initialize 方法会调用两次
  3. 如果先引用父类的实例对象,再引用子类实例对象,则会在引用父类实例对象时调用父类 initialize 方法;当引用子类实例对象时,由于父类的 initialize 方法已经执行,所以此时只调用子类 initialize 方法
  4. 如果先引用子类的实例对象,再引用父类的实例对象,则会在引用子类的实例对象时,在调用 initialize 方法前,先调用父类 initialize 方法,再调用子类的 initialize 方法;当引用父类实例对象时,由于在引用子类实例对象时已经调用了 initialzie 方法了,此时不再调用 initialize 方法

由于 initialzie 方法可能会被调用多次,所以为保证 initialize 方法只被调用一次,苹果建议

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

或者使用 dispatch_once

0x03 initialize 方法使用场景

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

0x04 initialize 方法实现原理

打断点查看 initialize 方法的调用栈

可以看到在调用 _class_initialize 函数之前,调用了 lookUpImpOrForward 函数,我们先看一下 lookUpImOrForward 函数的实现:

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 函数,看一下 _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);
    }
    
    // Try to atomically set CLS_INITIALIZING.
    {
        monitor_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {
            cls->setInitializing();
            reallyInitialize = YES;
        }
    }   
    if (reallyInitialize) {
            callInitialize(cls);
    }
    ...
}

_class_initialize 函数中调用了 callInitialize 函数,其中的调用顺序是从父类开始,到子类的,并且根据 if (supercls && !supercls->isInitialized()) 来看,如果父类已经调用过 initialize 函数,则父类不会再次调用 initialize 函数,对应了前文方法调用顺序中的 1、3,if (!cls->isInitialized() && !cls->isInitializing()) 对应前文调用顺序中的 4

我们继续看下 callInitialize 函数做了什么:

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

callInitialize 函数的工作简单,就是发送消息。这是和 load 函数实现不一样的地方。load 函数的调用直接是函数指针的调用,而 initialize 函数是消息转发。所以 class 的子类就算没有实现 initialize 函数,也会调用父类的 initialize 函数。对应了前文调用顺序中的 2

参考

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