你真的了解 load 方法么?

原文鏈接: http://draveness.me/load/

關注倉庫,及時獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github

因為 ObjC 的 runtime 只能在 Mac OS 下才能編譯,所以文章中的代碼都是在 Mac OS,也就是 x86_64 架構下運行的,對于在 arm64 中運行的代碼會特別說明。

寫在前面

文章的標題與其說是問各位讀者,不如說是問筆者自己:真的了解 + load 方法么?

+ load 作為 Objective-C 中的一個方法,與其它方法有很大的不同。它只是一個在整個文件被加載到運行時,在 main 函數調用之前被 ObjC 運行時調用的鉤子方法。其中關鍵字有這么幾個:

  • 文件剛加載
  • main 函數之前
  • 鉤子方法

我在閱讀 ObjC 源代碼之前,曾經一度感覺自己對 + load 方法的作用非常了解,直到看了源代碼中的實現,才知道以前的以為,只是自己的以為罷了。

這篇文章會假設你知道:

  • 使用過 + load 方法
  • 知道 + load 方法的調用順序(文章中會簡單介紹)

在這篇文章中并不會用大篇幅介紹 + load 方法的作用其實也沒幾個作用,關注點主要在以下兩個問題上:

  • + load 方法是如何被調用的
  • + load 方法為什么會有這種調用順序

load 方法的調用棧

首先來通過 load 方法的調用棧,分析一下它到底是如何被調用的。

下面是程序的全部代碼:

// main.m
#import <Foundation/Foundation.h>

@interface XXObject : NSObject @end

@implementation XXObject

+ (void)load {
    NSLog(@"XXObject load");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool { }
    return 0;
}

代碼總共只實現了一個 XXObject+ load 方法,主函數中也沒有任何的東西:

objc-load-print-load

雖然在主函數中什么方法都沒有調用,但是運行之后,依然打印了 XXObject load 字符串,也就是說調用了 + load 方法。

使用符號斷點

使用 Xcode 添加一個符號斷點 +[XXObject load]

注意這里 +[ 之間沒有空格

objc-load-symbolic-breakpoint

為什么要加一個符號斷點呢?因為這樣看起來比較高級。

重新運行程序。這時,代碼會停在 NSLog(@"XXObject load"); 這一行的實現上:

objc-load-break-after-add-breakpoint

左側的調用棧很清楚的告訴我們,哪些方法被調用了:

0  +[XXObject load]
1  call_class_loads()
2  call_load_methods
3  load_images
4  dyld::notifySingle(dyld_image_states, ImageLoader const*)
11 _dyld_start

dyld 是 the dynamic link editor 的縮寫,它是蘋果的動態鏈接器

在系統內核做好程序準備工作之后,交由 dyld 負責余下的工作。本文不會對其進行解釋

每當有新的鏡像加載之后,都會執行 3 load_images 方法進行回調,這里的回調是在整個運行時初始化時 _objc_init 注冊的(會在之后的文章中具體介紹):

dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);

有新的鏡像被加載到 runtime 時,調用 load_images 方法,并傳入最新鏡像的信息列表 infoList

const char *
load_images(enum dyld_image_states state, uint32_t infoCount,
            const struct dyld_image_info infoList[])
{
    bool found;

    found = false;
    for (uint32_t i = 0; i < infoCount; i++) {
        if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) {
            found = true;
            break;
        }
    }
    if (!found) return nil;

    recursive_mutex_locker_t lock(loadMethodLock);

    {
        rwlock_writer_t lock2(runtimeLock);
        found = load_images_nolock(state, infoCount, infoList);
    }

    if (found) {
        call_load_methods();
    }

    return nil;
}

什么是鏡像

這里就會遇到一個問題:鏡像到底是什么,我們用一個斷點打印出所有加載的鏡像:

objc-load-print-image-info

從控制臺輸出的結果大概就是這樣的,我們可以看到鏡像并不是一個 Objective-C 的代碼文件,它應該是一個 target 的編譯產物。

...
(const dyld_image_info) $52 = {
  imageLoadAddress = 0x00007fff8a144000
  imageFilePath = 0x00007fff8a144168 "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices"
  imageFileModDate = 1452737802
}
(const dyld_image_info) $53 = {
  imageLoadAddress = 0x00007fff946d9000
  imageFilePath = 0x00007fff946d9480 "/usr/lib/liblangid.dylib"
  imageFileModDate = 1452737618
}
(const dyld_image_info) $54 = {
  imageLoadAddress = 0x00007fff88016000
  imageFilePath = 0x00007fff88016d40 "/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation"
  imageFileModDate = 1452737917
}
(const dyld_image_info) $55 = {
  imageLoadAddress = 0x0000000100000000
  imageFilePath = 0x00007fff5fbff8f0 "/Users/apple/Library/Developer/Xcode/DerivedData/objc-dibgivkseuawonexgbqssmdszazo/Build/Products/Debug/debug-objc"
  imageFileModDate = 0
}

這里面有很多的動態鏈接庫,還有一些蘋果為我們提供的框架,比如 Foundation、 CoreServices 等等,都是在這個 load_images 中加載進來的,而這些 imageFilePath 都是對應的二進制文件的地址。

但是如果進入最下面的這個目錄,會發現它是一個可執行文件,它的運行結果與 Xcode 中的運行結果相同:

objc-load-image-binary

準備 + load 方法

我們重新回到 load_images 方法,如果在掃描鏡像的過程中發現了 + load 符號:

for (uint32_t i = 0; i < infoCount; i++) {
    if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) {
        found = true;
        break;
    }
}

就會進入 load_images_nolock 來查找 load 方法:

bool load_images_nolock(enum dyld_image_states state,uint32_t infoCount,
                   const struct dyld_image_info infoList[])
{
    bool found = NO;
    uint32_t i;

    i = infoCount;
    while (i--) {
        const headerType *mhdr = (headerType*)infoList[i].imageLoadAddress;
        if (!hasLoadMethods(mhdr)) continue;

        prepare_load_methods(mhdr);
        found = YES;
    }

    return found;
}

調用 prepare_load_methodsload 方法的調用進行準備(將需要調用 load 方法的類添加到一個列表中,后面的小節中會介紹):

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

通過 _getObjc2NonlazyClassList 獲取所有的類的列表之后,會通過 remapClass 獲取類對應的指針,然后調用 schedule_class_load 遞歸地安排當前類和沒有調用 + load 父類進入列表。

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

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

    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

在執行 add_class_to_loadable_list(cls) 將當前類加入加載列表之前,會先把父類加入待加載的列表,保證父類在子類前調用 load 方法。

調用 + load 方法

在將鏡像加載到運行時、對 load 方法的準備就緒之后,執行 call_load_methods,開始調用 load 方法:

void call_load_methods(void)
{
    ...

    do {
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        more_categories = call_category_loads();

    } while (loadable_classes_used > 0  ||  more_categories);

    ...
}

方法的調用流程大概是這樣的:

objc-load-diagra

其中 call_class_loads 會從一個待加載的類列表 loadable_classes 中尋找對應的類,然后找到 @selector(load) 的實現并執行。

static void call_class_loads(void)
{
    int i;
    
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    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;

        (*load_method)(cls, SEL_load);
    }
    
    if (classes) free(classes);
}

這行 (*load_method)(cls, SEL_load) 代碼就會調用 +[XXObject load] 方法。

我們會在下面介紹 loadable_classes 列表是如何管理的。

到現在,我們回答了第一個問題:

Q:load 方法是如何被調用的?

A:當 Objective-C 運行時初始化的時候,會通過 dyld_register_image_state_change_handler 在每次有新的鏡像加入運行時的時候,進行回調。執行 load_images 將所有包含 load 方法的文件加入列表 loadable_classes ,然后從這個列表中找到對應的 load 方法的實現,調用 load 方法。

加載的管理

ObjC 對于加載的管理,主要使用了兩個列表,分別是 loadable_classesloadable_categories

方法的調用過程也分為兩個部分,準備 load 方法和調用 load 方法,我更覺得這兩個部分比較像生產者與消費者:

objc-load-producer-consumer-diagra

add_class_to_loadable_list 方法負責將類加入 loadable_classes 集合,而 call_class_loads 負責消費集合中的元素。

而對于分類來說,其模型也是類似的,只不過使用了另一個列表 loadable_categories

“生產” loadable_class

在調用 load_images -> load_images_nolock -> prepare_load_methods -> schedule_class_load -> add_class_to_loadable_list 的時候會將未加載的類添加到 loadable_classes 數組中:

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;

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

方法剛被調用時:

  1. 會從 class 中獲取 load 方法: method = cls->getLoadMethod();
  2. 判斷當前 loadable_classes 這個數組是否已經被全部占用了:loadable_classes_used == loadable_classes_allocated
  3. 在當前數組的基礎上擴大數組的大小:realloc
  4. 把傳入的 class 以及對應的方法的實現加到列表中

另外一個用于保存分類的列表 loadable_categories 也有一個類似的方法 add_category_to_loadable_list

void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    if (!method) return;
    
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

實現幾乎與 add_class_to_loadable_list 完全相同。

到這里我們完成了對 loadable_classes 以及 loadable_categories 的提供,下面會開始消耗列表中的元素。

“消費” loadable_class

調用 load 方法的過程就是“消費” loadable_classes 的過程,load_images -> call_load_methods -> call_class_loads 會從 loadable_classes 中取出對應類和方法,執行 load

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        more_categories = call_category_loads();

    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

上述方法對所有在 loadable_classes 以及 loadable_categories 中的類以及分類執行 load 方法。

do {
    while (loadable_classes_used > 0) {
        call_class_loads();
    }

    more_categories = call_category_loads();

} while (loadable_classes_used > 0  ||  more_categories);

調用順序如下:

  1. 不停調用類的 + load 方法,直到 loadable_classes 為空
  2. 調用一次 call_category_loads 加載分類
  3. 如果有 loadable_classes 或者更多的分類,繼續調用 load 方法

相比于類 load 方法的調用,分類中 load 方法的調用就有些復雜了:

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    // 1. 獲取當前可以加載的分類列表
    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;

    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()) {
            // 2. 如果當前類是可加載的 `cls  &&  cls->isLoadable()` 就會調用分類的 load 方法
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

    // 3. 將所有加載過的分類移除 `loadable_categories` 列表
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[i];
        } else {
            shift++;
        }
    }
    used -= shift;

    // 4. 為 `loadable_categories` 重新分配內存,并重新設置它的值
    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];
    }

    if (loadable_categories) free(loadable_categories);

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

    return new_categories_added;
}

這個方法有些長,我們來分步解釋方法的作用:

  1. 獲取當前可以加載的分類列表
  2. 如果當前類是可加載的 cls && cls->isLoadable() 就會調用分類的 load 方法
  3. 將所有加載過的分類移除 loadable_categories 列表
  4. loadable_categories 重新分配內存,并重新設置它的值

調用的順序

你過去可能會聽說過,對于 load 方法的調用順序有兩條規則:

  1. 父類先于子類調用
  2. 類先于分類調用

這種現象是非常符合我們的直覺的,我們來分析一下這種現象出現的原因。

第一條規則是由于 schedule_class_load 有如下的實現:

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

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

    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

這里通過這行代碼 schedule_class_load(cls->superclass) 總是能夠保證沒有調用 load 方法的父類先于子類加入 loadable_classes 數組,從而確保其調用順序的正確性。

類與分類中 load 方法的調用順序主要在 call_load_methods 中實現:

do {
    while (loadable_classes_used > 0) {
        call_class_loads();
    }

    more_categories = call_category_loads();

} while (loadable_classes_used > 0  ||  more_categories);

上面的 do while 語句能夠在一定程度上確保,類的 load 方法會先于分類調用。但是這里不能完全保證調用順序的正確。

如果分類的鏡像在類的鏡像之前加載到運行時,上面的代碼就沒法保證順序的正確了,所以,我們還需要在 call_category_loads 中判斷類是否已經加載到內存中(調用 load 方法):

if (cls  &&  cls->isLoadable()) {
    (*load_method)(cls, SEL_load);
    cats[i].cat = nil;
}

這里,檢查了類是否存在并且是否可以加載,如果都為真,那么就可以調用分類的 load 方法了。

load 的應用

load 可以說我們在日常開發中可以接觸到的調用時間最靠前的方法,在主函數運行之前,load 方法就會調用。

由于它的調用不是惰性的,且其只會在程序調用期間調用一次,最最重要的是,如果在類與分類中都實現了 load 方法,它們都會被調用,不像其它的在分類中實現的方法會被覆蓋,這就使 load 方法成為了方法調劑的絕佳時機。

但是由于 load 方法的運行時間過早,所以這里可能不是一個理想的環境,因為某些類可能需要在在其它類之前加載,但是這是我們無法保證的。不過在這個時間點,所有的 framework 都已經加載到了運行時中,所以調用 framework 中的方法都是安全的。

參考資料

關注倉庫,及時獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,751評論 0 9
  • 整個運行時初始化時 _objc_init 注冊的回調 dyld_register_image_state_chan...
    cmhfx1閱讀 644評論 0 1
  • 之前在寫《Category你真的懂嗎?》那篇簡書收集資料的時候,看了很多load和initialize的資料,加深...
    一劍孤城閱讀 2,708評論 3 24
  • 接上一篇 吃了個桂林米粉,我們又開始趕路,去25公里遠的興坪古鎮乘船,游覽漓江。 船在水中行,人在畫中游,大概就這...
    龍初元閱讀 201評論 0 0