天天看點

Objective-C的+load方法調用原理分析

Objective-C之Category的底層實作原理

Objective-C的+initialize方法調用原理分析

别人是這麼說的

  • 調用時機:

    +load

    方法會在Runtime加載類對象(

    class

    )和分類(

    category

    )的時候調用
  • 調用頻率: 每個類對象、分類的

    +load

    方法,在工程的整個生命周期中隻調用一次
  • 調用順序:
    先調用類對象(

    class

    )的

    +load

    方法:
    • 類對象的

      load

      調用順序是按照 類檔案的編譯順序 進行先後調用;
    • 調用子類

      +load

      之前會先調用父類的

      +load

      方法
    再調用分類(

    category

    )的+load方法:按照編譯先後順序調用(先編譯的,先被調用)
一、load方法的調用時機和調用頻率

+load

方法是在程式一啟動運作,加載鏡像中的類對象(

class

)和分類(

category

)的時候就會調用,隻會調用一次,不論在項目中有沒有用到該類對象或者該分類,他們統統都會先被加載進記憶體,因為類的加載隻有一次,是以所有的load方法肯定都會被調用而且隻有一次。下面先上一個小demo調試看看:

Objective-C的+load方法調用原理分析
Objective-C的+load方法調用原理分析
Objective-C的+load方法調用原理分析
Objective-C的+load方法調用原理分析
Objective-C的+load方法調用原理分析

上圖裡面,我建立了一個

person

類,以及它的兩個分類–

CLPerson+Test/CLPerson+Test2

,然後給它們都加上兩個類方法(

+load/+test

),

main.h

裡面先不加任何代碼跑跑看。

Objective-C的+load方法調用原理分析

從日志看出,雖然整個工程都沒有import過

CLPerson

以及它的兩個分類,但是他們的

load

方法還是被調用了,并且都發生在

main

函數開始之前,而且

+test

并沒有被調用。是以該現象間接證明了,

load

方法的調用應該和類對象以及分類的加載有關。

main.h

裡面調一下

+test

方法

Objective-C的+load方法調用原理分析
Objective-C的+load方法調用原理分析
接下來通過源碼分析一下(Runtime源碼下載下傳位址)

首先,進入Runtime的初始化檔案

objc-os.mm

,找到

_objc_init

函數,該函數可以看作是Runtime的初始化函數。

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
           

忽略一些與本文主題關聯不太的函數,直接看最後句

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

其中很明顯,

load_images

就是加載鏡像/加載子產品的意思,應該是與我們話題相關的參數,點進去看看它的實作

/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);

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
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

           

蘋果對該函數官方給出的注釋是,處理那些正在進行映射的鏡像(images)的+load方法。該方法的實作裡面,做了兩件事情:

  • prepare_load_methods

    // Discover load methods – 查找并準備load方法,以供後面去調用
  • call_load_methods();

    //Call +load methods – 調用這些load方法

針對上面案例日志中出現的現象,先從結果出發,逆向分析,來看看load方法是如何調用的,進入

call_load_methods();

的實作

/***********************************************************************
* call_load_methods

* Call all pending class and category +load methods.
調用所有的進行中的class和category的+load方法;

* Class +load methods are called superclass-first. 
class的+load方法會被先調用,并且,一個調用一個class的+load方法前,會先對其父類的+load進行調用

* Category +load methods are not called until after the parent class's +load.
category的+load方法的調用,會發生在所有的class的+load方法完成調用之後。
* 
* This method must be RE-ENTRANT, because a +load could trigger 
* more image mapping. In addition, the superclass-first ordering 
* must be preserved in the face of re-entrant calls. Therefore, 
* only the OUTERMOST call of this function will do anything, and 
* that call will handle all loadable classes, even those generated 
* while it was running.
*
* The sequence below preserves +load ordering in the face of 
* image loading during a +load, and make sure that no 
* +load method is forgotten because it was added during 
* a +load call.

* Sequence:調用順序
* 1. Repeatedly call class +loads until there aren't any more
周遊所有的class對象,調用它們的+load方法,知道所有class中的+load都完成了調用

* 2. Call category +loads ONCE.
調用所有category中的+load方法

* 3. Run more +loads if:
這裡我還不太了解,感覺上面都已經把所有的+load調用完了,還不太了解哪裡會産生新的+load方法。有待繼續補充......
*    (a) there are more classes to load, OR
*    (b) there are some potential category +loads that have 
*        still never been attempted.
* Category +loads are only run once to ensure "parent class first" 
* ordering, even if a category +load triggers a new loadable class 
* and a new loadable category attached to that class. 
*
* Locking: loadMethodLock must be held by the caller 
*   All other locks must not be held.
**********************************************************************/
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;
}
           

很明顯,核心邏輯在do-while循環裡面,循環中面做了兩件事:

  • 首先調用類對象的

    +load

    方法–

    call_class_loads();

    ,直到可加載的類的計數器減到0 –

    loadable_classes_used > 0

  • 然後調用分類的

    +load

    方法--

    call_category_loads();//Call category +loads ONCE

小結A – 程式啟動之後,Runtime會在鏡像加載階段,先調用所有類對象的

+load

方法,然後在調用所有分類的

+load

方法,類對象與分類之間參與編譯順序,不會影響上面的結論。例如下圖的調試,注意編譯順序
Objective-C的+load方法調用原理分析

這裡産生了一個新的疑問:既然是方法調用,為什麼

category

+load

方法沒有“覆寫”類對象的

+load

方法呢?

有關分類(

category

)中的方法對類對象中的同名方法産生的“覆寫”現象如果還不太清楚,請參考我的Objective-C之Category的底層實作原理一文。

接着上面的源碼,繼續看看Runtime對于類對象和分類

+load

到底是如何調用的。我們先檢視

call_class_loads();

,這是對所有類對象(

class

)的

+load

方法的調用邏輯

/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;//首先用局部變量loadable_class儲存loadable_classes清單
    int used = loadable_classes_used;//在用局部變量used儲存loadable_classes_used
    loadable_classes = nil;//将loadable_classes置空
    loadable_classes_allocated = 0;//将loadable_classes_allocated清零
    loadable_classes_used = 0;//将loadable_classes_used清零
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {//周遊classes清單
        Class cls = classes[i].cls;//從清單成員裡面獲得cls
        load_method_t load_method = (load_method_t)classes[i].method;//從清單成員擷取對應cls的+load 的IMP(方法實作)
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);//這裡就是對+load方法的調用,注意哦,這是直接的函數調用,不是消息機制那種哦,這裡跟類的方法清單什麼沒關系,直接就是通過+load的IMP進行調用了
    }
    // Destroy the detached list.
    if (classes) free(classes);
}
           

上面實作的主要邏輯發生在for循環裡面,該for循環周遊了一個叫

classes

的清單,該清單存儲的是一堆

loadable_class

結構體,

loadable_class

的定義如下

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};
           

每一個

struct loadable_class

變量,存儲的應該就是

一個類對象

+

一個與該類相關的方法實作

。從

loadable_class

這個命名,說明它内部的資訊肯定是表示一個可以被加載的類的相關資訊,是以合理推斷,它裡面的

method

應該就是類的

+load

方法,

cls

就是這個

+load

方法所對應的類對象。這個推斷是否正确,我們一會讨論。

我們再看看源碼中對于

classes

這個數組進行周遊時到底做了什麼。很簡單,就是通過函數指針

load_method

loadable_class

中獲得

+load

方法的

IMP

作為其參數,然後就直接對其進行調用

(*load_method)(cls, SEL_load);

,是以,類對象的

+load

方法的調用實際上就發生在這裡。這裡的for循環一旦結束,

classes

所包含的所有類對象的

+load

方法就會被依次調用,這跟一個類是否被在工程項目裡被執行個體化過,是否接受過消息,沒有半毛錢關系。

至此,Runtime對于

+load

方法是如何調用的問題我們分析了一半,弄清楚了類對象的

+load

方法的是怎麼被一個一個調用的,也就是

static void call_class_loads(void)

這個函數,接下來,還有問題的另一半–

static bool call_category_loads(void)

,也就是關于分類的

+load

方法的調用。進入其中

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

    // Compact detached list (order-preserving)
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[i];
        } else {
            shift++;
        }
    }
    used -= shift;

    // Copy any new +load candidates from the new list to the detached list.
    new_categories_added = (loadable_categories_used > 0);
    for (i = 0; i < loadable_categories_used; i++) {
        if (used == allocated) {
            allocated = allocated*2 + 16;
            cats = (struct loadable_category *)
                realloc(cats, allocated *
                                  sizeof(struct loadable_category));
        }
        cats[used++] = loadable_categories[i];
    }

    // Destroy the new list.
    if (loadable_categories) free(loadable_categories);

    // Reattach the (now augmented) detached list. 
    // But if there's nothing left to load, destroy the list.
    if (used) {
        loadable_categories = cats;
        loadable_categories_used = used;
        loadable_categories_allocated = allocated;
    } else {
        if (cats) free(cats);
        loadable_categories = nil;
        loadable_categories_used = 0;
        loadable_categories_allocated = 0;
    }

    if (PrintLoading) {
        if (loadable_categories_used != 0) {
            _objc_inform("LOAD: %d categories still waiting for +load\n",
                         loadable_categories_used);
        }
    }
    return new_categories_added;
}
           

我們可以看到,這個方法的實作裡面,通過系統注釋,被劃分如下幾塊:

  • A –

    // Detach current loadable list

    .分離可加載

    category

    清單,也就是把可加載清單的資訊儲存到本函數的局部變量cats數組上。
  • B –

    // Call all +loads for the detached list

    .消費

    cats

    裡面的所有

    +load

    方法(也就是調用它們)
  • C –

    // Compact detached list (order-preserving)

    清理

    cats

    裡面已經被消費過的成員,并且更新

    used

    計數值
  • D –

    // Copy any new +load candidates from the new list to the detached list.

    如果又出現了新的可加載的分類,将其相關内容複制到

    cats

    清單上。
  • E –

    // Destroy the new list.

    銷毀清單(這裡指的是外部的

    loadable_categories

    變量)
  • F –

    // Reattach the (now augmented) detached list. But if there's nothing left to load, destroy the list.

    更新幾個記錄了category+load資訊的幾個全局變量。

    相比較于

    call_class_loads

    方法,這裡多了步驟C、D、F。關于A、B、E這三個步驟,因為跟

    call_class_loads

    方法裡面實作是一樣的,不作重複解釋。且看看多出來的這幾步

    先看C

    Objective-C的+load方法調用原理分析
    對于這個,我畫個圖示範一下,就明白了
    Objective-C的+load方法調用原理分析
    其實我感覺消費完一輪+load方法之後,cats裡面基本上會在這個步驟被清空。

然後我們看看D步驟,如下圖

Objective-C的+load方法調用原理分析

其實主要任務就是把新的可以加載的分類(

category

)資訊(如果此時發現還有的話)添加到本函數的

cats

數組上。

最後看看F步驟

Objective-C的+load方法調用原理分析
小結B –

Runtime

對于

+load

方法的調用,不是走的我們熟悉的“消息發送”路線,而是直接拿到

+load

方法的

IMP

,直接調用。是以不存在所謂“類的方法被

category

的方法覆寫”的問題,是以除了

結論A

的 類與分類的

+load

方法先後調用順序外,我們看到類與它的分類的所有的

+load

全部都被調用了,沒有被覆寫。

目前,我們确定了類對象的

+load

方法會先于分類的

+load

方法被調用,并且不存在覆寫現象。

  • 那麼對于類于類之間

    +load

    調用順序是怎樣的?
  • 同樣的疑問對于分類(

    category

    )又是如何呢?

    這兩個問題,我們就需要進入

    prepare_load_methods

    方法的實作,看看

    +load

    方法被調用前,Runtime是如何準備它們的。
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

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

上面的實作裡,

classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count);

可以看出,利用系統提供的函數

_getObjc2NonlazyClassList

,獲得類對象的清單,因為這是系統級别的函數,應該跟編譯過程的順序有關,這裡先推測

classlist

中類的順序與類的編譯順序相同。

接下來,就是周遊

classlist

,對其每個成員通過函數

schedule_class_load()

進行處理

/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed 
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
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);

    /** <#注釋标題#>✈️✈️✈️✈️✈️
     将cls添加到loadable_classes數組的最後面
     */
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
           

該函數裡面就做兩件事:

  • 先遞歸調用自身(

    schedule_class_load()

    ),對目前類(也就是函數傳入的參數)的父類進行處理
  • 處理完父類之後,将目前類對象加入到可加載類的相關清單當中

    add_class_to_loadable_list(cls);

經過這樣的整理之後,最終整理過的裝載類對象相關資訊的數組中,父類應該排在子類前面。而不同的類對象之間在數組中的位置,就可以參考它們.m的編譯順序來看了。

每個類對象在被加入數組的時候,會通過

cls->setInfo(RW_LOADED);

設定标簽标記一下,這樣,如果該類下次被作為父類進行遞歸調用的時候,就不會重複加入到清單中,保證一個類在數組中隻出現一次。

最後再看一下

add_class_to_loadable_list(cls);

裡面的邏輯

Objective-C的+load方法調用原理分析

每個步驟的作用請看圖中的注釋。請注意其中一個細節,第三句代碼

method = cls->getLoadMethod();

,進一步檢視一下這個

getLoadMethod

Objective-C的+load方法調用原理分析

很明顯這個方法就是從一個類對象裡面尋找load方法實作,找到的話,就傳回load方法的IMP,指派給method。

Objective-C的+load方法調用原理分析

然後把該類對象

cls

和對應的+load方法IMP

method

指派給

loadable_classes

清單最後一個成員,該成員我們前面篇章已經說了,該成員就是

loadable_class

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};
           

我們之前推測的說

loadable_class

裡面存放的

IMP method;

應該就是

+load

方法的

IMP

,通過上面的分析,證明确實如此。

上面的是針對類對象的

+load

的方法所進行的調用前的整理排布。下面我們看一下分類的

+load

方法是如何處理的。回到

prepare_load_methods

方法,這裡我直接貼出相關部分代碼截圖

Objective-C的+load方法調用原理分析

可以看到,并沒像類一樣,用一個

schedule

方法進行遞歸處理,而是直接通過系統函數

_getObjc2NonlazyCategoryList

拿到分類的集合

categorylist

,因為對分類來說,不存在誰是誰的父類,大家都是平級的,而且之前類對象的

+load

方法已經處理過準備好了,是以這裡,隻需将

categorylist

裡面的分類對象一個一個拿出來,通過

add_category_to_loadable_list

方法處理好,一個一個加入到我們後面調用

+load

方法時所用的

loadable_categories

數組裡面。

add_category_to_loadable_list(cat)

方法跟上面

add_class_to_loadable_list(cls);

方法裡面的邏輯完全一緻,不做重複解讀。至此,

+load

方法的調用前的前期準備工作,分析完了。

小結C
  • 那麼對于類于類之間

    +load

    調用順序是怎樣的?

    調用一個類對象的

    +load

    方法之前,會先調用其父類的

    +load

    方法(如果存在的話),類與類之間,會按照編譯的順序,先後調用其

    +load

    方法。一個類對象的

    +load

    方法不會被重複調用,隻可能被調用一次。
  • 同樣的疑問對于分類(

    category

    )又是如何呢?

    分類的

    +load

    方法,會按照分類參與編譯的順序,先編譯的,先被調用。

我們在通過代碼來驗證一波。在開篇案例裡面,我繼續添加幾個類和分類,

CLTeacher

CLPerson

子類)、

CLTree

NSObject

子類)、

CLRiver

NSObject

子類)、以及

CLTeacher

的兩個分類。

Objective-C的+load方法調用原理分析
  • 首先看出,類對象的

    +load

    方法肯定是先與所有分類的

    +load

    方法被調用的。
  • 分類之間是按照編譯的順序,先後調用

    +load

  • CLTeacher

    CLTree

    CLRiver

    也是按照編譯的順序,先後調用

    +load

    ,由于

    CLPerson

    CLTeacher

    的父類,是以會先用它調用

    +load

    至此,完全和上面的小結C吻合。如果你有興趣,可以自己嘗試一下,變換一下源檔案的編譯順序,結果和這裡的結論都是一緻的。

到這裡,關于

+load

方法調用的細節應該就算分析完了~~~

PS:對于蘋果的源碼,我也在不斷的研讀和學習中,如果文中有闡述不對的地方,煩請告知指正,于此與各位共勉~~

參考來源

  • Objective-C Class Loading and Initialization
  • 你真的了解load方法麼
  • MJ小碼哥 OC底層原理班