load和initialize詳解

Objective-C 中絕大部分的類(lèi)都繼承自 NSObject 類(lèi)。而在 NSObject 類(lèi)中有兩個(gè)非常特殊的類(lèi)方法 +load 和 +initialize ,用于類(lèi)的初始化。這兩個(gè)看似非常簡(jiǎn)單的類(lèi)方法在許多方面會(huì)讓人感到困惑,比如:

  1. 子類(lèi)、父類(lèi)、分類(lèi)中的相應(yīng)方法什么時(shí)候會(huì)被調(diào)用?
  2. 需不需要在子類(lèi)的實(shí)現(xiàn)中顯式地調(diào)用父類(lèi)的實(shí)現(xiàn)?
  3. 每個(gè)方法到底會(huì)被調(diào)用多少次?

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

+load

+load 方法是當(dāng)類(lèi)或分類(lèi)被添加到 Objective-C runtime 時(shí)被調(diào)用的,實(shí)現(xiàn)這個(gè)方法可以讓我們?cè)陬?lèi)加載的時(shí)候執(zhí)行一些類(lèi)相關(guān)的行為。子類(lèi)的 +load 方法會(huì)在它的所有父類(lèi)的 +load 方法之后執(zhí)行,而分類(lèi)的 +load 方法會(huì)在它的主類(lèi)的 +load 方法之后執(zhí)行。但是不同的類(lèi)之間的 +load 方法的調(diào)用順序是不確定的。

打開(kāi) runtime 工程,我們接下來(lái)看看與 +load 方法相關(guān)的幾個(gè)關(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);
    }
}

顧名思義,這個(gè)函數(shù)的作用就是提前準(zhǔn)備好滿(mǎn)足 +load 方法調(diào)用條件的類(lèi)和分類(lèi),以供接下來(lái)的調(diào)用。其中,在處理類(lèi)時(shí),調(diào)用了同文件中的另外一個(gè)函數(shù) static void schedule_class_load(Class cls) 來(lái)執(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 行代碼對(duì)入?yún)⒌母割?lèi)進(jìn)行了遞歸調(diào)用,以確保父類(lèi)優(yōu)先的順序。void prepare_load_methods(header_info *hi) 函數(shù)執(zhí)行完后,當(dāng)前所有滿(mǎn)足 +load 方法調(diào)用條件的類(lèi)和分類(lèi)就被分別存放在全局變量 loadable_classesloadable_categories 中了。

準(zhǔn)備好類(lèi)和分類(lèi)后,接下來(lái)就是對(duì)它們的 +load 方法進(jìn)行調(diào)用了。打開(kāi)文件 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;
}

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

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

這個(gè)函數(shù)的作用就是真正負(fù)責(zé)調(diào)用類(lèi)的 +load 方法了。它從全局變量 loadable_classes 中取出所有可供調(diào)用的類(lèi),并進(jìn)行清零操作。

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

其中 loadable_classes 指向用于保存類(lèi)信息的內(nèi)存的首地址,loadable_classes_allocated 標(biāo)識(shí)已分配的內(nèi)存空間大小,loadable_classes_used 則標(biāo)識(shí)已使用的內(nèi)存空間大小。

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

這樣的調(diào)用方式就使得 +load 方法擁有了一個(gè)非常有趣的特性,那就是子類(lèi)、父類(lèi)和分類(lèi)中的 +load 方法的實(shí)現(xiàn)是被區(qū)別對(duì)待的。也就是說(shuō)如果子類(lèi)沒(méi)有實(shí)現(xiàn) +load 方法,那么當(dāng)它被加載時(shí) runtime 是不會(huì)去調(diào)用父類(lèi)的 +load 方法的。同理,當(dāng)一個(gè)類(lèi)和它的分類(lèi)都實(shí)現(xiàn)了 +load 方法時(shí),兩個(gè)方法都會(huì)被調(diào)用。因此,我們常常可以利用這個(gè)特性做一些“邪惡”的事情,比如說(shuō)方法混淆(Method Swizzling)。

+initialize

+initialize 方法是在類(lèi)或它的子類(lèi)收到第一條消息之前被調(diào)用的,這里所指的消息包括實(shí)例方法和類(lèi)方法的調(diào)用。也就是說(shuō) +initialize 方法是以懶加載的方式被調(diào)用的,如果程序一直沒(méi)有給某個(gè)類(lèi)或它的子類(lèi)發(fā)送消息,那么這個(gè)類(lèi)的 +initialize 方法是永遠(yuǎn)不會(huì)被調(diào)用的。那這樣設(shè)計(jì)有什么好處呢?好處是顯而易見(jiàn)的,那就是節(jié)省系統(tǒng)資源,避免浪費(fèi)。

同樣的,我們還是結(jié)合 runtime 的源碼來(lái)加深對(duì) +initialize 方法的理解。打開(kāi)文件 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 
    ...
}

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

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 行代碼對(duì)入?yún)⒌母割?lèi)進(jìn)行了遞歸調(diào)用,以確保父類(lèi)優(yōu)先于子類(lèi)初始化。另外,最關(guān)鍵的是第 36 行代碼(暴露了 +initialize 方法的本質(zhì)),runtime 使用了發(fā)送消息 objc_msgSend 的方式對(duì) +initialize 方法進(jìn)行調(diào)用。也就是說(shuō) +initialize 方法的調(diào)用與普通方法的調(diào)用是一樣的,走的都是發(fā)送消息的流程。換言之,如果子類(lèi)沒(méi)有實(shí)現(xiàn) +initialize 方法,那么繼承自父類(lèi)的實(shí)現(xiàn)會(huì)被調(diào)用;如果一個(gè)類(lèi)的分類(lèi)實(shí)現(xiàn)了 +initialize 方法,那么就會(huì)對(duì)這個(gè)類(lèi)中的實(shí)現(xiàn)造成覆蓋。

因此,如果一個(gè)子類(lèi)沒(méi)有實(shí)現(xiàn) +initialize 方法,那么父類(lèi)的實(shí)現(xiàn)是會(huì)被執(zhí)行多次的。有時(shí)候,這可能是你想要的;但如果我們想確保自己的 +initialize 方法只執(zhí)行一次,避免多次執(zhí)行可能帶來(lái)的副作用時(shí),我們可以使用下面的代碼來(lái)實(shí)現(xiàn):

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

總結(jié)

通過(guò)閱讀 runtime 的源碼,我們知道了 +load 和 +initialize 方法實(shí)現(xiàn)的細(xì)節(jié),明白了它們的調(diào)用機(jī)制和各自的特點(diǎn)。下面我們繪制一張表格,以更加直觀(guān)的方式來(lái)鞏固我們對(duì)它們的理解:

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容