彻底搞懂+load和+initialize

一、+load

结论一:

  • +load 方法在 main 函数执行之前调用;

调用栈如图:


load方法通过dyld调用

具体推到和验证详见:iOS类加载流程(一):类加载流程的触发

结论二:

  • 分类和类分别处理,存储在两个全局表中,所有类的 +load 方法调用完毕之后再调用分类的 +load 方法;

源码如下:

// List of classes that need +load called (pending superclass +load)
// This list always has superclasses first because of the way it is constructed
static struct loadable_class *loadable_classes NOBSS = NULL;

// List of categories that need +load called (pending parent class +load)
static struct loadable_category *loadable_categories NOBSS = NULL;

__private_extern__ void prepare_load_methods(header_info *hi)
{
    size_t count, i;

    rwlock_assert_writing(&runtimeLock);

    class_t **classlist = 
        _getObjc2NonlazyClassList(hi, &count);
    for (i = 0; i < count; i++) {
        class_t *cls = remapClass(classlist[i]);
        // 内部会调用add_class_to_loadable_list添加到loadable_classes数组中
        schedule_class_load(cls);
    }

    category_t **categorylist = _getObjc2NonlazyCategoryList(hi, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        // Do NOT use cat->cls! It may have been remapped.
        class_t *cls = remapClass(cat->cls);
        realizeClass(cls);
        assert(isRealized(cls->isa));
       // 这个方法中会存入 loadable_categories
        add_category_to_loadable_list((Category)cat);
    }
}

将类添加到数组的代码如下,分类的就不列出了:

static void schedule_class_load(class_t *cls)
{
    assert(isRealized(cls));  // _read_images should realize

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

    class_t *supercls = getSuperclass(cls);
    if (supercls) schedule_class_load(supercls);

    add_class_to_loadable_list((Class)cls);
    changeInfo(cls, RW_LOADED, 0); 
}

dyld 会分别将类和分类添加到两个全局数组中,以后循环取出这两个数组中的元素,递归执行 +load。正因为这两个数组独立,这也就是分类的 +load 不覆盖原来类的 +load 方法的本质;

结论三:

  • 在(类先于分类调用)、(父类先于子类)的大前提下,分类中 +load 方法的调用顺序取决于符号表中的顺序,也就是编译顺序;

如下:


load方法和macho

结论四:

  • 递归调用类的 +load 方法,因此父类会先于子类调用,分类最后调用;

结论五:

  • dyld 中并没有使用 objc_msgSend 来调用函数,而是直接执行,所以不存在方法查找和消息传递机制,所以子类未实现 +load 方法的情况下不会去调用父类的 +load

源码如下:

__private_extern__ void call_load_methods(void)
{
    static BOOL loading = NO;
    BOOL more_categories;

    recursive_mutex_assert_locked(&loadMethodLock);

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

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

    loading = NO;
}

这里也可以看出,两个 while 循环,先通过 call_class_loads 执行所有类的 load 方法,再通过 call_category_loads 执行分类的 load 方法。

其中,执行类的 load 方法代买如下:

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 = NULL;
    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;
        IMP load_method = classes[i].method;
        if (!cls) continue; 

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

从上面可以看到,直接取出类地址之后执行,并没有调用 objc_msgSend。因此,如果子类没有实现 +load 方法,此时并不会去调用父类的 +load

结论六:实现了 +load 方法的类会被添加到 mach-O 的 __DATA,__objc_nlclslist 表中,调用 +load 方法之前,会通过该表取出类的静态数据,完成对类的初始化;

注意,这个表 MachOView 不会解析,不代表该表中没有内容。内容如下:

__objc_nlclslist

因为表中的数据是 8 字节,所以很容易猜到存储的是指针。另外,因为 __PAGEZERO 段的存在,所以先调整下 MachOView 中的显示方式:

__PAGEZERO

iOS 中是大小端,所以上述三个地址应该是:

0x0100009358
0x01000093A8
0x01000093F8

对应的地址中的数据如下:

__objc_data

而这三个类就是实现了 +load 方法的三个类;

另外,从前面的代码中也可以看出来,先通过 _getObjc2NonlazyClassList_getObjc2NonlazyCategoryList 取出了类的静态数据和分类数据,再通过 realizeClassWithoutSwift 完成了类的初始化,最后才通过 call_load_methods 进行 +load 方法的调用;

这里调用 realizeClassWithoutSwift 只是一个兜底逻辑,其实非懒加载类在 map 时,已经被 objc 完成了初始化。load_image 主要的逻辑是通过非懒加载类的列表来遍历调用 +load 方法;

二、+initalize

先看一眼源码:

__private_extern__ void _class_initialize(Class cls)
{
    Class supercls;
    BOOL reallyInitialize = NO;

    // Get the real class from the metaclass. The superclass chain 
    // hangs off the real class only.
    cls = _class_getNonMetaClass(cls);

    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = _class_getSuperclass(cls);
    if (supercls  &&  !_class_isInitialized(supercls)) {
        _class_initialize(supercls);
    }
    
    // Try to atomically set CLS_INITIALIZING.
    monitor_enter(&classInitLock);
    if (!_class_isInitialized(cls) && !_class_isInitializing(cls)) {
        _class_setInitializing(cls);
        reallyInitialize = YES;
    }
    monitor_exit(&classInitLock);
    
    if (reallyInitialize) {
        // We successfully set the CLS_INITIALIZING bit. Initialize the class.
        
        // Record that we're initializing this class so we can message it.
        _setThisThreadIsInitializingClass(cls);
        
        // Send the +initialize message.
        // Note that +initialize is sent to the superclass (again) if 
        // this class doesn't implement +initialize. 2157218
        if (PrintInitializing) {
            _objc_inform("INITIALIZE: calling +[%s initialize]",
                         _class_getName(cls));
        }

        ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);

        if (PrintInitializing) {
            _objc_inform("INITIALIZE: finished +[%s initialize]",
                         _class_getName(cls));
        }        
        
        // Done initializing. 
        ......
}

代码中有几个关键点:

    1. 初始化自己之前,递归执行父类的初始化操作;
    supercls = _class_getSuperclass(cls);
    if (supercls  &&  !_class_isInitialized(supercls)) {
        _class_initialize(supercls);
    }

这里很明显是先判断父类有没有初始化完成,如果没有则递归执行父类的初始化;

    1. 初始化方法是通过 objc_msgSend 调用的,需要经过方法查找和消息转发的过程;
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);

因此,可以做出几个结论:

  1. +initialize 方法是在这个类第一次被使用到时才调用,具体为第一次调用该类的相关方法;
  2. 父类的 +initialize 先执行;
  3. 如果子类没有实现 +initialize,则会调用父类的 +initialize
  4. 如果子类实现了 +initialize,那么就直接执行子类的 +initialize
  5. 理论上只会调用一次,但是因为采用了 objc_msgSend 来调用,所以如果子类没有实现 +initialize,那么就会多次调用父类的 +initialize,可以通过添加 if (self == [ClassName self]) 来进行判断;
  6. 不像 +load 方法,会区分类和分类保存在两个数组中分别执行, 分类的 +initialize 方法会覆盖原来类的 +initialize,且遵循分类的编译顺序原则,最靠后的分类最终替换掉之前的 +initialize 方法;

说白了,initialize 就是一个普通 OC 方法,其特殊之处就在于第一次调用这个类的相关方法时被调用,相当于类对象/元对象的懒加载;

三、+initialize多次调用

关于 +initialize 被多次调用的场景:

  • 子类未实现 + initialize ,父类的+initialize 会被多次调用;

当第一次使用父类时,父类的 + initialize 被调用,当第一次使用子类时,因为子类无该方法,通过 objcMsgSend 传递机制,调用到父类的 + initialize,所以此时会被多次调用,因此, Apple 建议的做法是:

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

而 [Class self] 的源码如下:

+ (id)self {
    return (id)self;
}

该方法直接返回方法的调用者,也就是类对象。所以以上代码就是在排除掉子类调用 +initialize 方法的情况,确保只被初始化一次;

源码中的 _class_isInitialized 只是确保在调用父类 +initialize 方法之前,父类已经初始化完成了,而不是说父类初始化完成就不调用了;

四、总结

+load+initialize 方法本质都是做初始化的,只不过级别或者说针对的过程不一样。

非懒加载类在动态链接时已经被加载,而懒加载类在运行时被使用到时才加载。加载的过程就是对类的方法、属性、协议等进行装配,以完成类和元类的初始化操作。懒加载类如果实现了 +load 方法,那么该类就被添加到了非懒加载表,在 objc 的 map_image 流程中已经初始化完成了。只不过在后续的 load_image 流程中才触发 +load 方法的调用。这些都发生 dyld 流程中,在 main 函数之前。

+initialize方法针对的是 main 方法之后,而且是懒加载,使用到时才初始化。鉴于 objc_msgSend 的机制,存在多次调用的可能,但是可以使用代码进行判断。

总结:

  1. 目的不同:+load 方法用于添加类在加载完成后的一些逻辑,initialize 方法用于在类第一次被使用到时进行一些初始化操作;
  2. 时机不同:一个处于 dyld 流程中,位于 main 函数之前,一个处于 runloop 开启之后,位于main 函数之后;
  3. 机制不同:+load 方法的调用是根据类继承关系进行符号调用,所以子类如果未实现则不会调用父类的 +load 方法。而 initialize 是通过消息转发机制调用,子类未实现则进入父类的方法查找逻辑最终调用,所以需要添加一些判断来阻断该逻辑。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249