你真的瞭解load方法麼?

本文受權轉載,做者:左書祺(關注倉庫,及時得到更新:iOS-Source-Code-Analyzehtml

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

寫在前面git

文章的標題與其說是問各位讀者,不如說是問筆者本身:我真的瞭解 + load 方法麼?github

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

  • 文件剛加載安全

  • main 函數以前架構

  • 鉤子方法app

我在閱讀 ObjC 源代碼以前,曾經一度感受本身對 + load 方法的做用很是瞭解,直到看了源代碼中的實現,才知道之前的覺得,只是本身的覺得罷了。框架

這篇文章會假設你知道:函數

  • 使用過 + load 方法

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

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

  • + load 方法是如何被調用的

  • + load 方法爲何會有這種調用順序

load 方法的調用棧

首先來經過 load 方法的調用棧,分析一下它究竟是如何被調用的。

下面是程序的所有代碼:

1

2

3

4

5

6

7

8

9

10

11

12

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

}</foundation>

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

objc-load-print-load.png

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

使用符號斷點

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

注意這裏 + 和 [ 之間沒有空格

objc-load-symbolic-breakpoint.png

爲何要加一個符號斷點呢?由於這樣看起來比較高級。

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

objc-load-break-after-add-breakpoint.png

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

1

2

3

4

5

6

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 註冊的(會在以後的文章中具體介紹):

1

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

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.png

從控制檯輸出的結果大概就是這樣的,咱們能夠看到鏡像並非一個 Objective-C 的代碼文件,它應該是一個 target 的編譯產物。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

(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.png

準備 + load 方法

咱們從新回到 load_images 方法,若是在掃描鏡像的過程當中發現了 + load 符號:

1

2

3

4

5

6

for (uint32_t i = 0; i < infoCount; i++) {

    if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) {

        found = true;

        break;

    }

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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_methods 對 load 方法的調用進行準備(將須要調用 load 方法的類添加到一個列表中,後面的小節中會介紹):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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 父類進入列表。

1

2

3

4

5

6

7

8

9

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 方法:

1

2

3

4

5

6

7

8

9

10

11

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-diagram.png

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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_classes 和 loadable_categories。

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

objc-load-producer-consumer-diagram.png

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 數組中:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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

}

方法剛被調用時:

  • 會從 class 中獲取 load 方法: method = cls->getLoadMethod();

  • 判斷當前 loadable_classes 這個數組是否已經被所有佔用了:loadable_classes_used == loadable_classes_allocated

  • 在當前數組的基礎上擴大數組的大小:realloc

  • 把傳入的 class 以及對應的方法的實現加到列表中

另一個用於保存分類的列表 loadable_categories 也有一個相似的方法 add_category_to_loadable_list。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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 方法。

1

2

3

4

5

6

do {

    while (loadable_classes_used > 0) {

        call_class_loads();

    }

    more_categories = call_category_loads();

while (loadable_classes_used > 0  ||  more_categories);

調用順序以下:

  • 不停調用類的 + load 方法,直到 loadable_classes 爲空

  • 調用一次 call_category_loads 加載分類

  • 若是有 loadable_classes 或者更多的分類,繼續調用 load 方法

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

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;

}

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

  • 獲取當前能夠加載的分類列表

  • 若是當前類是可加載的 cls && cls->isLoadable() 就會調用分類的 load 方法

  • 將全部加載過的分類移除 loadable_categories 列表

  • 爲 loadable_categories 從新分配內存,並從新設置它的值

調用的順序

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

  • 父類先於子類調用

  • 類先於分類調用

這種現象是很是符合咱們的直覺的,咱們來分析一下這種現象出現的緣由。

第一條規則是因爲 schedule_class_load 有以下的實現:

1

2

3

4

5

6

7

8

9

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 中實現:

1

2

3

4

5

6

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 方法):

1

2

3

4

if (cls  &&  cls->isLoadable()) {

    (*load_method)(cls, SEL_load);

    cats[i].cat = nil;

}

這裏,檢查了類是否存在而且是否能夠加載,若是都爲真,那麼就能夠調用分類的 load 方法了。

load 的應用

load 能夠說咱們在平常開發中能夠接觸到的調用時間最靠前的方法,在主函數運行以前,load 方法就會調用。

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

可是因爲 load 方法的運行時間過早,因此這裏可能不是一個理想的環境,由於某些類可能須要在在其它類以前加載,可是這是咱們沒法保證的。不過在這個時間點,全部的 framework 都已經加載到了運行時中,因此調用 framework 中的方法都是安全的。

相關文章
相關標籤/搜索