load和initialize詳解

Objective-C 中絕大部分的類都繼承自 NSObject 類胞得。而在 NSObject 類中有兩個非常特殊的類方法 +load 和 +initialize 鳞上,用于類的初始化。這兩個看似非常簡單的類方法在許多方面會讓人感到困惑撬槽,比如:

  1. 子類损趋、父類崖面、分類中的相應(yīng)方法什么時候會被調(diào)用术幔?
  2. 需不需要在子類的實現(xiàn)中顯式地調(diào)用父類的實現(xiàn)另萤?
  3. 每個方法到底會被調(diào)用多少次?

下面诅挑,我們將結(jié)合 runtime(文字用的是版本 objc4-646.tar.gz) 的源碼四敞,一起來揭開它們的神秘面紗。

+load

+load 方法是當類或分類被添加到 Objective-C runtime 時被調(diào)用的拔妥,實現(xiàn)這個方法可以讓我們在類加載的時候執(zhí)行一些類相關(guān)的行為忿危。子類的 +load 方法會在它的所有父類的 +load 方法之后執(zhí)行,而分類的 +load 方法會在它的主類的 +load 方法之后執(zhí)行没龙。但是不同的類之間的 +load 方法的調(diào)用順序是不確定的铺厨。

打開 runtime 工程,我們接下來看看與 +load 方法相關(guān)的幾個關(guān)鍵函數(shù)硬纤。首先是文件 objc-runtime-new.mm 中的 void prepare_load_methods(header_info *hi) 函數(shù):

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

    rwlock_assert_writing(&runtimeLock);

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

    category_t **categorylist = _getObjc2NonlazyCategoryList(hi, &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);
    }
}

顧名思義解滓,這個函數(shù)的作用就是提前準備好滿足 +load 方法調(diào)用條件的類和分類,以供接下來的調(diào)用咬摇。其中伐蒂,在處理類時,調(diào)用了同文件中的另外一個函數(shù) static void schedule_class_load(Class cls) 來執(zhí)行具體的操作肛鹏。

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

其中,函數(shù)第 9 行代碼對入?yún)⒌母割愡M行了遞歸調(diào)用恩沛,以確保父類優(yōu)先的順序在扰。void prepare_load_methods(header_info *hi) 函數(shù)執(zhí)行完后,當前所有滿足 +load 方法調(diào)用條件的類和分類就被分別存放在全局變量 loadable_classesloadable_categories 中了雷客。

準備好類和分類后芒珠,接下來就是對它們的 +load 方法進行調(diào)用了。打開文件 objc-loadmethod.m 搅裙,找到其中的 void call_load_methods(void) 函數(shù)皱卓。

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;

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

同樣的裹芝,這個函數(shù)的作用就是調(diào)用上一步準備好的類和分類中的 +load 方法,并且確保類優(yōu)先于分類的順序娜汁。我們繼續(xù)查看在這個函數(shù)中調(diào)用的另外兩個關(guān)鍵函數(shù) static void call_class_loads(void)static BOOL call_category_loads(void) 嫂易。由于這兩個函數(shù)的作用大同小異,下面就以篇幅較小的 static void call_class_loads(void) 函數(shù)為例進行探討掐禁。

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_internal(classes);
}

這個函數(shù)的作用就是真正負責調(diào)用類的 +load 方法了怜械。它從全局變量 loadable_classes 中取出所有可供調(diào)用的類,并進行清零操作傅事。

loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;

其中 loadable_classes 指向用于保存類信息的內(nèi)存的首地址缕允,loadable_classes_allocated 標識已分配的內(nèi)存空間大小,loadable_classes_used 則標識已使用的內(nèi)存空間大小蹭越。

然后障本,循環(huán)調(diào)用所有類的 +load 方法。注意响鹃,這里是(調(diào)用分類的 +load 方法也是如此)直接使用函數(shù)內(nèi)存地址的方式 (*load_method)(cls, SEL_load); 對 +load 方法進行調(diào)用的彼绷,而不是使用發(fā)送消息 objc_msgSend 的方式。

這樣的調(diào)用方式就使得 +load 方法擁有了一個非常有趣的特性茴迁,那就是子類寄悯、父類和分類中的 +load 方法的實現(xiàn)是被區(qū)別對待的。也就是說如果子類沒有實現(xiàn) +load 方法堕义,那么當它被加載時 runtime 是不會去調(diào)用父類的 +load 方法的猜旬。同理,當一個類和它的分類都實現(xiàn)了 +load 方法時倦卖,兩個方法都會被調(diào)用洒擦。因此,我們常撑绿牛可以利用這個特性做一些“邪惡”的事情熟嫩,比如說方法混淆(Method Swizzling)。

+initialize

+initialize 方法是在類或它的子類收到第一條消息之前被調(diào)用的褐捻,這里所指的消息包括實例方法和類方法的調(diào)用掸茅。也就是說 +initialize 方法是以懶加載的方式被調(diào)用的,如果程序一直沒有給某個類或它的子類發(fā)送消息柠逞,那么這個類的 +initialize 方法是永遠不會被調(diào)用的昧狮。那這樣設(shè)計有什么好處呢?好處是顯而易見的板壮,那就是節(jié)省系統(tǒng)資源逗鸣,避免浪費。

同樣的,我們還是結(jié)合 runtime 的源碼來加深對 +initialize 方法的理解撒璧。打開文件 objc-runtime-new.mm 透葛,找到以下函數(shù):

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                       bool initialize, bool cache, bool resolver)
{
    ...
        rwlock_unlock_write(&runtimeLock);
    }

    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
        // 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
    }

    // The lock is held to make method-lookup + cache-fill atomic 
    // with respect to method addition. Otherwise, a category could 
    ...
}

當我們給某個類發(fā)送消息時,runtime 會調(diào)用這個函數(shù)在類中查找相應(yīng)方法的實現(xiàn)或進行消息轉(zhuǎn)發(fā)卿樱。從第 8-14 的關(guān)鍵代碼我們可以看出僚害,當類沒有初始化時 runtime 會調(diào)用 void _class_initialize(Class cls) 函數(shù)對該類進行初始化。

void _class_initialize(Class cls)
{
    ...
    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_enter(&classInitLock);
    if (!cls->isInitialized() && !cls->isInitializing()) {
        cls->setInitializing();
        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]",
                         cls->nameForLogging());
        }

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

        if (PrintInitializing) {
            _objc_inform("INITIALIZE: finished +[%s initialize]",
    ...
}

其中殿如,第 7-12 行代碼對入?yún)⒌母割愡M行了遞歸調(diào)用贡珊,以確保父類優(yōu)先于子類初始化。另外涉馁,最關(guān)鍵的是第 36 行代碼(暴露了 +initialize 方法的本質(zhì))门岔,runtime 使用了發(fā)送消息 objc_msgSend 的方式對 +initialize 方法進行調(diào)用。也就是說 +initialize 方法的調(diào)用與普通方法的調(diào)用是一樣的烤送,走的都是發(fā)送消息的流程寒随。換言之,如果子類沒有實現(xiàn) +initialize 方法帮坚,那么繼承自父類的實現(xiàn)會被調(diào)用妻往;如果一個類的分類實現(xiàn)了 +initialize 方法,那么就會對這個類中的實現(xiàn)造成覆蓋试和。

因此讯泣,如果一個子類沒有實現(xiàn) +initialize 方法,那么父類的實現(xiàn)是會被執(zhí)行多次的阅悍。有時候好渠,這可能是你想要的;但如果我們想確保自己的 +initialize 方法只執(zhí)行一次节视,避免多次執(zhí)行可能帶來的副作用時拳锚,我們可以使用下面的代碼來實現(xiàn):

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

總結(jié)

通過閱讀 runtime 的源碼,我們知道了 +load 和 +initialize 方法實現(xiàn)的細節(jié)寻行,明白了它們的調(diào)用機制和各自的特點霍掺。下面我們繪制一張表格,以更加直觀的方式來鞏固我們對它們的理解:

+load +initialize
調(diào)用時機 被添加到 runtime 時 收到第一條消息前拌蜘,可能永遠不調(diào)用
調(diào)用順序 父類->子類->分類 父類->子類
調(diào)用次數(shù) 1次 多次
是否需要顯式調(diào)用父類實現(xiàn)
是否沿用父類的實現(xiàn)
分類中的實現(xiàn) 類和分類都執(zhí)行 覆蓋類中的方法杆烁,只執(zhí)行分類的實現(xiàn)

文章轉(zhuǎn)自: 雷純鋒的技術(shù)博客

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拦坠,隨后出現(xiàn)的幾起案子连躏,更是在濱河造成了極大的恐慌,老刑警劉巖贞滨,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡晓铆,警方通過查閱死者的電腦和手機勺良,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來骄噪,“玉大人尚困,你說我怎么就攤上這事×慈铮” “怎么了事甜?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長滔韵。 經(jīng)常有香客問我逻谦,道長,這世上最難降的妖魔是什么陪蜻? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任邦马,我火速辦了婚禮,結(jié)果婚禮上宴卖,老公的妹妹穿的比我還像新娘滋将。我一直安慰自己,他們只是感情好症昏,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布随闽。 她就那樣靜靜地躺著,像睡著了一般肝谭。 火紅的嫁衣襯著肌膚如雪掘宪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天分苇,我揣著相機與錄音添诉,去河邊找鬼。 笑死医寿,一個胖子當著我的面吹牛栏赴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播靖秩,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼须眷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沟突?” 一聲冷哼從身側(cè)響起花颗,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惠拭,沒想到半個月后扩劝,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庸论,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年棒呛,在試婚紗的時候發(fā)現(xiàn)自己被綠了聂示。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡簇秒,死狀恐怖鱼喉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情趋观,我是刑警寧澤扛禽,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站皱坛,受9級特大地震影響编曼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜麸恍,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一灵巧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧抹沪,春花似錦刻肄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至噪馏,卻和暖如春麦到,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欠肾。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工瓶颠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人刺桃。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓粹淋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瑟慈。 傳聞我的和親對象是個殘疾皇子桃移,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容