iOS 底層探索 - 類的加載

在這裏插入圖片描述

一、應用加載回顧

上一章我們對應用的加載有了初步的認識,我們知道了

  • 系統調用 exec() 會我們的應用映射到新的地址空間
  • 然後通過 dyld 進行加載、鏈接、初始化主程序和主程序所依賴的各種動態庫
  • 最後在 initializeMainExecutable 方法中經過一系列初始化調用 notifySingle 函數,該函數會執行一個 load_images 的回調
  • 然後在 doModinitFuntions 函數內部會調用 __attribute__((constructor))c 函數
  • 然後 dyld 返回主程序的入口函數,開始進入主程序的 main 函數

main 函數執行執行,其實 dyld 還會在流程中初始化 libSystem,而 libSystem 又會去初始化 libDispatch,在 libDispatch 初始化方法裏面又會有一步 _os_object_init,在 _os_object_init 內部就會調起 _objc_init。而對於 _objc_init 我們還需要繼續探索,因爲這裏面會進行類的加載等一系列重要的工作。

image.png

二、探索 _objc_init

首先來到 libObjc 源碼的 _objc_init 方法處,你可以直接添加一個符號斷點 _objc_init 或者全局搜索關鍵字來到這裏:

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

我們接着進行分析:

image.png

  • 判斷是否已經初始化了,如果初始化過了,直接返回。

2.1 environ_init

接着來到 environ_init 方法內部:

image.png

我們可以看到,這裏主要是讀取影響 Runtime 的一些環境變量,如果需要,還可以打印環境變量幫助提示。

我們可以在終端上測試一下,直接輸入 export OBJC_HELP=1:

image.png

可以看到不同的環境變量對應的內容都被打印出來了。

2.2 tls_init

接着來到 tls_init 方法內部:

void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
    _objc_pthread_key = TLS_DIRECT_KEY;
    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}

這裏執行的是關於線程 key 的綁定,比如每線程數據的析構函數。

2.3 static_init

接着來到 static_init 方法內部:

/***********************************************************************
* static_init
* Run C++ static constructor functions.
* libc calls _objc_init() before dyld would call our static constructors, 
* so we have to do it ourselves.
**********************************************************************/
static void static_init()
{
    size_t count;
    auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        inits[i]();
    }
}

這裏會運行 C++ 的靜態構造函數,在 dyld 調用我們的靜態構造函數之前,libc 會調用 _objc_init,所以這裏我們必須自己來做,並且這裏只會初始化系統內置的 C++ 靜態構造函數,我們自己代碼裏面寫的並不會在這裏初始化。

2.4 lock_init

接着來到 lock_init 方法內部:

void lock_init(void)
{
}

我們可以看到,這是一個空的實現。也就是說 objc 的鎖是完全採用的 C++ 那一套的鎖邏輯。

2.5 exception_init

接着來到 exception_init 方法內部:

/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
void exception_init(void)
{
    old_terminate = std::set_terminate(&_objc_terminate);
}

這裏是初始化 libobjc 的異常處理系統,我們程序觸發的異常都會來到:

image.png

我們可以看到 _objc_terminate 是未處理異常的回調函數,其內部邏輯如下:

  • 檢查是否是一個活躍的異常
  • 如果是活躍的異常,檢查是否是 OC 拋出的異常
  • 如果是 OC 拋出的異常,調用 uncaught_handeler 回調函數指針
  • 如果不是 OC 拋出的異常,則繼續 C++ 終止操作

2.6 _dyld_objc_notify_register

接下來使我們今天探索的重點了: _dyld_objc_notify_register ,我們先看下它的定義:

image.png

注意:僅供 objc 運行時使用
objc 鏡像被映射(mapped)、**卸載(unmapped)初始化(initialized)**的時候,註冊的回調函數就會被調用。
這個方法是 dlyd 中聲明的,一旦調用該方法,調用結果會作爲該函數的參數回傳回來。比如,當所有的 images 以及 sectionobjc-image-info 被加載之後會回調 mapped 方法。
load 方法也將在這個方法中被調用。

_dyld_objc_notify_register 方法的三個參數 map_imagesload_imagesunmap_image 其實都是函數指針:

image.png

這三個函數指針是在 dyld 中回調的,我們打開 dyld 的源碼即可一探究竟,我們直接搜索 _dyld_objc_notify_register :

image.png

接着來到 dyldregisterObjCNotifiers 方法內部:

image.png

image.png

通過上面兩張截圖的內容說明在 registerObjCNotifiers 內部, libObjc 傳過來的這三個函數指針被 dyld 保存在了本地靜態變量中。換句話來說,最終函數指針是否能被調用,取決於這三個靜態變量:

  • sNotifyObjCMapped
  • sNotifyObjCInit
  • sNotifyObjCUnmapped

我們注意到 registerObjCNotifierstry-catch 語句中的 try 分支註釋如下:

call ‘mapped’ function with all images mapped so far
調用 mapped 函數來映射所有的鏡像

那麼也就是說 notifyBatchPartial 裏面會進行真正的函數指針的調用,我們進入這個方法內部:

image.png

我們可以看到,在 notifyBatchPartial 方法內部,這裏的註釋:

tell objc about new images 告訴 objc 鏡像已經映射完成了

而圖中箭頭所指的地方正是 sNotifyObjCMapped 函數指針真正調用的地方。

弄清楚了三個函數指針是怎麼調用的還不夠,接下來我們要深入各個函數的內部看裏面究竟做了什麼樣的事情。

三、探索 map_images

首先是 map_images ,我們來到它的實現:

/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}C

Process the given images which are being mapped in by dyld.
Calls ABI-agnostic code after taking ABI-specific locks.

處理由 dyld 映射的給定鏡像
取得特定於 ABI 的鎖後,調用與 ABI 無關的代碼。

這裏會繼續往下走到 map_images_nolock

map_images_nolock 內部代碼十分冗長,我們經過分析之後,前面的工作基本上都是進行鏡像文件信息的提取與統計,所以可以定位到最後的 _read_images

image.png

這裏進入 _read_images 的條件是 hCount 大於 0, hCount 表示的是 Mach-Oheader 的數量

OK,我們的主角登場了, _read_imageslookupImpOrForward 可以說是我們學習 RuntimeiOS 底層裏面非常重要的兩個概念了, lookUpImpOrForward 已經探索過了,剩下的 _read_images 我們也不能落下。

image.png

3.1 _read_images 定義

Perform initial processing of the headers in the linked list beginning with headerList.
headerList 開始,對已經鏈接了的 Mach-O 鏡像表中的頭部進行初始化處理

我們可以看到,整個 _read_images 有接近 400 行代碼。我們不妨摺疊一下里面的分支代碼,然後總覽一下:

image.png

image.png

image.png

通過摺疊代碼,以及日誌打印提示信息,我們大致可以將 _read_images 分爲下面幾個流程:

3.2 _read_images 具體流程


doneOnce 流程
**
我們從第一個分支 doneOnce 開始,這個名詞顧名思義,只會執行一次:

image.png

  • 通過宏 SUPPORT_NONPOINTER_ISA 判斷當前是否支持開啓內存優化的 isa
    • 如果支持,則在某些條件下需要禁用這個優化
  • 通過宏 SUPPORT_INDEXED_ISA 判斷當前是否是將類存儲在 isa 作爲類表索引
    • 如果是的話,再遞歸遍歷所有的 Mach-O 的頭部,並且判斷如果是 Swift 3.0 之前的代碼,就需要禁用對 isa 的內存優化

image.png

  • 通過宏 TARGET_OS_OSX 判斷是否是 macOS 執行環境
  • 判斷 macOS 的系統版本,如果小於 10.11 則說明 app 太陳舊了,需要禁用掉 non-pointer isa
  • 然後再遍歷所有的 Mach-O 的頭部,判斷如果有 __DATA__,__objc_rawisa 段的存在,則禁用掉 non-pointer isa ,因爲很多新的 app 加載老的擴展的時候會需要這樣的判斷操作。

image.png

預先優化過的類不會加入到 gdb_objc_realized_classes 這個哈希表中來, gdb_objc_realized_classes 哈希表的裝載因子爲 0.75,這是一個經過驗證的效率很高的擴容臨界值。

  • 加載所有類到類的 gdb_objc_realized_classes 表中來

我們查看這個表的定義:

image.png

// This is a misnomer: gdb_objc_realized_classes is actually a list of
// named classes not in the dyld shared cache, whether realized or not.

這是一個誤稱:gdb_objc_realized_classes 表實際上存儲的是不在 dyld 共享緩存裏面的命名類,無論這些類是否實現

除了 gdb_objc_realized_classes 表之外,還有一張表 allocatedClasses :

image.png

  • 通過 objc_allocateClassPair 開闢之後的類和元類存儲的表(也就是說需要 alloc

其實 gdb_objc_realized_classesallocatedClasses 是一種包含的關係,一張是類的總表,一張是已經開闢了內存的類表,


Discover classes 流程

image.png

Discover classes. Fix up unresolved future classes. Mark bundle classes.
發現類。修正未解析的 future 類,標記 bundle 類。

  • 先通過 _getObjc2ClassList 來獲取到所有的類,我們可以通過 MachOView 來驗證:

image.png

  • 接着還是遍歷所有的 Mach-Oheader 部分,然後通過 mustReadClasses 來判斷哪些條件可以跳過讀取類這一步驟
  • 讀取 header 是否是 Bundle
  • 讀取 header 是否開啓了 預優化
  • 遍歷 _getObjc2ClassList 取出的所有的類
    • 通過 readClass 來讀取類信息
    • 判斷如果不相等並且 readClass 結果不爲空,則需要重新爲類開闢內存

Fix up remapped classes 流程

image.png

修復 重映射類
類表和非懶加載類表沒有被重映射 (也就是 _objc_classlist)
由於消息轉發,類引用和父類引用會被重映射 (也就是 _objc_classrefs)

**

  • 通過 noClassesRemapped 方法判斷是否有類引用(_objc_classrefs)需要進行重映射
    • 如果需要,則遍歷 EACH_HEADER
    • 通過 _getObjc2ClassRefs_getObjc2SuperRefs 取出當前遍歷到的 Mach-O 的類引用和父類引用,然後調用 remapClassRef 進行重映射

image.png


Fix up @selector references 流程

image.png

修正 SEL 引用

  • 操作前先加一個 selLock
  • 然後遍歷 EACH_HEADER
    • 如果開啓了預優化,contiue 到下一個 Mach-O
    • 通過 _getObjc2SelectorRefs 拿到所有的 SEL 引用
    • 然後對所有的 SEL 引用調用 sel_registerNameNoLock 進行註冊

也就是說這一流程最主要的目的就是註冊 SEL ,我們註冊真正發生的地方: __sel_registerName ,這個函數如果大家經常玩 Runtime 肯定不會陌生:

image.png

我們簡單分析一下 __sel_registerName 方法的流程:

  • 判斷是否要加鎖
  • 如果 sel 爲空,則返回一個空的 SEL
  • builtins 中搜索,看是否已經註冊過,如果找到,直接返回結果
  • namedSelectors 哈希表中查詢,找到了就返回結果
  • 如果 namedSelectors 未初始化,則創建一下這個哈希表
  • 如果上面的流程都沒有找到,則需要調用 sel_alloc 來創建一下 SEL ,然後把新創建的 SEL 插入哈希表中進行緩存的填充

Fix up old objc_msgSend_fixup call sites 流程

image.png

修正舊的 objc_msgSend_fixup 調用

**
這個流程的執行前提是 FIXUP 被開啓。

  • 還是老套路,遍歷 EACH_HEADER
    • 通過 _getObjc2MessageRefs 方法來獲取當前遍歷到的 Mach-O 鏡像的所有消息引用
    • 然後遍歷這些消息引用,然後調用 fixupMessageRef 進行修正

Discover protocols 流程

image.png

發現協議,並修正協議引用

**


Fix up @protocol references 流程

image.png

對所有的協議做重映射

**


Realize non-lazy classes 流程

image.png

初始化非懶加載類( **+load** 方法和靜態實例)


Realize newly-resolved future classes 流程

image.png

初始化新解析出來的 future

**


Discover categories 流程

image.png

處理所有的分類,包括類和元類

**


到這裏, _read_images 的流程就分析完畢,我們可以新建一個文件來去掉一些干擾的信息,只保留核心的邏輯,這樣從宏觀的角度來分析更直觀:

image.png

Q & A 環節
Q: dyld 主要邏輯是加載庫,也就是鏡像文件,但是加載完是怎麼讀取的呢?
A: _read_images 是真正讀取的地方

Q: SEL 方法編號何時加載?
A: _read_images

3.3 read_class 分析

我們探索了 _read_images 方法的流程,接下來讓我們把目光放到本文的主題 - 類的加載
既然是類的加載,那麼我們在前面所探索的類的結構中出現的內容都會一一重現。
所以我們不妨直接進行斷點調試,讓我們略過其它干擾信息,聚焦於類的加載。

  • 根據上一小節我們探索的結果, doneOnce 流程中會創建兩個哈希表,並沒有涉及到類的加載,所以我們跳過
  • 我們來到第二個流程 - **類處理 **


我們在下圖所示的位置處打上斷點:

image.png
**
如上圖所示,從 classList 中取出的 cls 只是一個內存地址,我們嘗試通過 LLDB 打印 clsclas_rw_t :

image.png

可以看到 cls 的屬性、方法、協議以及類名都爲空,說明這裏類並沒有被真正加載完成,我們接着聚焦到 read_class 函數上面,我們進入其內部實現,我們大致瀏覽之後會定位到如下圖所示的代碼:

image.png

看起來類的信息在這裏完成了加載,那麼爲了驗證我們的猜想,直接斷點調試一下但發現斷點根本走不進來,原因在於這裏的判斷語句

if (Class newCls = popFutureNamedClass(mangledName))

判斷當前傳入的類的類名是否有 future 類的實現,但是我們剛纔已經打印了,類名是空的,所以肯定不會執行這裏。我們接着往下走:

image.png

  • addNamedClass 內部其實是將 cls 插入到 gdb_objc_realized_classes

image.png

  • addclassTableEntry 內部是將 cls 插入到 allocatedClasses

image.png

分析完 read_class ,我們回到 _read_images 方法

image.png

我們可以看到 read_class 返回的 newCls 會進行一個判斷,判斷與傳入 read_class 之前的 cls 是否相等,而在 read_class 內部只有一個地方對類的內容進行了改動,但是我們剛纔測試了是進不去的,所以這個 if 裏面的內容我們可以略過,也就是說 resolvedFutureClasses 的內容我們都可以暫時略過。

總結一下 readClass

  • 判斷是不是要後期處理的類
    • 如果是的話,就取出後期處理的類,讀取這個類的 data() 類設置 ro/rw
  • addNamedClass 插入總表
  • addClassTableEntry 插入已開闢內存的類的表

3.4 realizeClassWithoutSwift 分析

通過分析 read_class ,我們可以得知,類已經被註冊到兩個哈希表中去了,那麼現在一切時機都已經成熟了。但是我們還是要略過像 Fix up remapped classesFix up @selector referencesfix up old objc_msgSend_fixup call sitesDiscover protocols. Fix up protocol refsFix up @protocol references ,因爲我們的重點是類的加載,我們最終來到了 Realize non-lazy classes (for +load methods and static instances) ,略去無關信息之後,我們可以看到我們的
主角 realizeClassWithoutSwift 閃亮登場了:

image.png

從方法的名稱以及方法註釋我們可以知道, realizeClassWithoutSwift 是進行類的第一次初始化操作,包括分配讀寫數據也就是我們常說的 rw ,但是並不會進行任何的 Swift 端初始化。我們直接聚焦下面的代碼:

image.png

  • 通過 calloc 開闢內存空間,返回一個新的 rw
  • cls 取出來的 ro 賦值給這個 rw
  • rw 設置到 cls 身上

那麼是不是說在這裏 rw 就有值了呢,我們 LLDB 打印大法走起:

image.png

可以清楚地看到,此時 rw 還是爲空,說明這裏只是對 rw 進行了初始化,但是方法、屬性、協議這些都沒有被添加上。

我們接着往下走:

image.png

這裏可以看到父類和元類都會遞歸調用 realizeClassWithoutSwift 來初始化各自的 rw 。爲什麼在類的加載操作裏面要去加載類和元類呢?回憶一下類的結構,答案很簡單,要保證 superclassisa 的完整性,也就是保證類的完整性,

image.png

上面的截圖就是最好的證明,初始化完畢的父類和元類被賦值到了類的 superclassisa 上面。

image.png

接着往下走可以看到,不光要把父類關聯到類上面,還要讓父類知道子類的存在。

最後一行代碼是 methodizeClass(cls) ,註釋顯示的是 attach categories ,附加分類到類?我們進入其內部實現一探究竟。

在探索 methodizeClass 前,我們先總結一下 realizeClassWithoutSwift :

  • 讀取 classdata()
  • ro/rw 賦值
  • 父類和元類實現
    • supercls = realizeClassWithoutSwift(remapClass(cls->superclass))
    • metacls = realizeClassWithoutSwift(remapClass(cls->ISA()))
  • 父類和元類歸屬關係
    • cls->superclass = supercls
    • cls->initClassIsa(metacls)
  • 將當前類鏈接到其父類的子類列表 addSubclass(supercls, cls)

3.5 methodizeClass 分析

image.png

對類的方法列表、協議列表和屬性列表進行修正
附加 category 到類上面來

我們直接往下面走:

    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }
  • ro 中取出方法列表附加到 rw
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
  • ro 中取出屬性列表附加到 rw
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }
  • ro 中取出協議列表附加到 rw
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
  • cls 中取出未附加的分類進行附加操作

我們可以看到,這裏有一個操作叫 attachLists ,爲什麼方法、屬性、協議都能調用這個方法呢?

image.png

image.png

image.png

我們可以看到,方法、屬性、協議的數據結構都是一個二維數組,我們深入 attachLists 方法內部實現:

    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;//10
            uint32_t newCount = oldCount + addedCount;//4
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;// 10+4
   
            memmove(array()->lists + addedCount, array()->lists,
                    oldCount * sizeof(array()->lists[0]));
            
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }
  • 判斷要添加的數量是否爲 0,如果爲 0,直接返回
  • 判斷當前調用 attachListslist_array_tt 二維數組有多個一維數組
    • 如果是,說明是多對多的關係
    • 這裏會通過 realloc 對容器進行重新分配,大小爲原來的大小加上新增的大小
    • 然後通過 memmove 把原來的數據移動到容器的末尾
    • 最後把新的數據拷貝到容器的起始位置
  • 如果調用 attachListslist_array_tt 二維數組爲空且新增大小數目爲 1,則直接取 addedList 的第一個 list 返回
  • 如果當前調用 attachListslist_array_tt 二維數組只有一個一維數組
    • 如果是,說明是一對多的關係
    • 這裏會通過 realloc 對容器進行重新分配,大小爲原來的大小加上新增的大小
    • 因爲原來只有一個一維數組,所以直接賦值到新 Array 的最後一個位置
    • 然後把新數據拷貝到容器的起始位置

四、探索 load_images

我們接着探索 _dyld_objc_notify_register 的第二個參數 load_images ,這個函數指針是在什麼時候調用的呢,同樣的,我們接着在 dyld 源碼中搜索對應的函數指針 sNotifyObjCInit :

image.png

可以看到,在 notifySingle 方法內部, sNotifyObjCInit 函數指針被調用了。根據我們上一篇文章探索 dyld 底層可以知道, _load_images 應該是對於每一個加載進來的 Mach-O 鏡像都會遞歸調用一次。

我們來到 libObjc 源碼中 load_images 的定義處:

image.png

處理由 dyld 映射的給定鏡像中的 +load 方法

  • 判斷是否有 load 方法,如果沒有,直接返回
  • 搜索 load 方法,具體實現通過 prepare_load_methods
  • 調用 load 方法,具體實現通過 call_load_methods

4.1 prepare_load_methods 分析

從這個方法名稱,我們猜測這裏應該做的是 load 方法的一些預處理工作,讓我們來到源碼進行分析:

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
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

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

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

/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    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++;
}
  • 首先通過 _getObjc2NonlazyClassList 獲取所有已經加載進去的類列表
  • 然後通過 schedule_class_load 遍歷這些類
    • 遞歸調用遍歷父類的 load 方法,確保父類的 load 方法順序排在子類的前面
    • 通過 add_class_to_loadable_list , 把類的 load 方法存在 loadable_classes 裏面
    • image.png
  • 完成 schedule_class_load 之後,通過 _getObjc2NonlazyCategoryList 取出所有分類數據
  • 然後遍歷這些分類
    • 通過 realizeClassWithoutSwift 來防止類沒有初始化,如果已經初始化了則不影響
    • 通過 add_category_to_loadable_list ,加載分類中的 load 方法到 loadable_categories 裏面
    • image.png

4.2 call_load_methods 分析


通過名稱我們可以知道 call_load_methods 應該就是 load 方法被調用的地方了。我們直接看源碼:

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

call_load_methods
調用類和類別中所有未決的 +load 方法
類裏面 +load 方法是父類優先調用的
而在父類的 +load 之後纔會調用分類的 +load 方法

  • 通過 objc_autoreleasePoolPush 壓棧一個自動釋放池
  • do-while 循環開始
    • 循環調用類的 +load 方法直到找不到爲止
    • 調用一次分類中的 +load 方法
  • 通過 objc_autoreleasePoolPop 出棧一個自動釋放池

五、總結

至此, _objc_init_dyld_objc_notify_register 我們就分析完了,我們對類的加載有了更細緻的認知。 iOS 底層有時候探索起來確實很枯燥,但是如果能找到高效的方法以及明確自己的所探索的方向,會讓自己從宏觀上重新審視這門技術。是的,技術只是工具,我們不能被技術所綁架,我們要做到有的放矢的去探索,這樣才能事半功倍。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章