一、應用加載回顧
上一章我們對應用的加載有了初步的認識,我們知道了
- 系統調用
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
我們還需要繼續探索,因爲這裏面會進行類的加載等一系列重要的工作。
二、探索 _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);
}
我們接着進行分析:
- 判斷是否已經初始化了,如果初始化過了,直接返回。
2.1 environ_init
接着來到 environ_init
方法內部:
我們可以看到,這裏主要是讀取影響 Runtime
的一些環境變量,如果需要,還可以打印環境變量幫助提示。
我們可以在終端上測試一下,直接輸入 export OBJC_HELP=1
:
可以看到不同的環境變量對應的內容都被打印出來了。
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
的異常處理系統,我們程序觸發的異常都會來到:
我們可以看到 _objc_terminate
是未處理異常的回調函數,其內部邏輯如下:
- 檢查是否是一個活躍的異常
- 如果是活躍的異常,檢查是否是
OC
拋出的異常 - 如果是
OC
拋出的異常,調用uncaught_handeler
回調函數指針 - 如果不是
OC
拋出的異常,則繼續C++
終止操作
2.6 _dyld_objc_notify_register
接下來使我們今天探索的重點了: _dyld_objc_notify_register
,我們先看下它的定義:
注意:僅供
objc
運行時使用
當objc
鏡像被映射(mapped)、**卸載(unmapped)和初始化(initialized)**的時候,註冊的回調函數就會被調用。
這個方法是dlyd
中聲明的,一旦調用該方法,調用結果會作爲該函數的參數回傳回來。比如,當所有的images
以及section
爲objc-image-info
被加載之後會回調mapped
方法。
load
方法也將在這個方法中被調用。
_dyld_objc_notify_register
方法的三個參數 map_images
、 load_images
、 unmap_image
其實都是函數指針:
這三個函數指針是在 dyld
中回調的,我們打開 dyld
的源碼即可一探究竟,我們直接搜索 _dyld_objc_notify_register
:
接着來到 dyld
的 registerObjCNotifiers
方法內部:
通過上面兩張截圖的內容說明在 registerObjCNotifiers
內部, libObjc
傳過來的這三個函數指針被 dyld
保存在了本地靜態變量中。換句話來說,最終函數指針是否能被調用,取決於這三個靜態變量:
sNotifyObjCMapped
sNotifyObjCInit
sNotifyObjCUnmapped
我們注意到 registerObjCNotifiers
的 try-catch
語句中的 try
分支註釋如下:
call ‘mapped’ function with all images mapped so far
調用mapped
函數來映射所有的鏡像
那麼也就是說 notifyBatchPartial
裏面會進行真正的函數指針的調用,我們進入這個方法內部:
我們可以看到,在 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
:
這裏進入
_read_images
的條件是hCount
大於 0,hCount
表示的是Mach-O
中header
的數量
OK,我們的主角登場了, _read_images
和 lookupImpOrForward
可以說是我們學習 Runtime
和 iOS
底層裏面非常重要的兩個概念了, lookUpImpOrForward
已經探索過了,剩下的 _read_images
我們也不能落下。
3.1 _read_images 定義
Perform initial processing of the headers in the linked list beginning with headerList.
從headerList
開始,對已經鏈接了的Mach-O
鏡像表中的頭部進行初始化處理
我們可以看到,整個 _read_images
有接近 400 行代碼。我們不妨摺疊一下里面的分支代碼,然後總覽一下:
通過摺疊代碼,以及日誌打印提示信息,我們大致可以將 _read_images
分爲下面幾個流程:
3.2 _read_images 具體流程
doneOnce 流程
**
我們從第一個分支 doneOnce
開始,這個名詞顧名思義,只會執行一次:
- 通過宏
SUPPORT_NONPOINTER_ISA
判斷當前是否支持開啓內存優化的isa
- 如果支持,則在某些條件下需要禁用這個優化
- 通過宏
SUPPORT_INDEXED_ISA
判斷當前是否是將類存儲在isa
作爲類表索引- 如果是的話,再遞歸遍歷所有的
Mach-O
的頭部,並且判斷如果是Swift 3.0
之前的代碼,就需要禁用對isa
的內存優化
- 如果是的話,再遞歸遍歷所有的
- 通過宏
TARGET_OS_OSX
判斷是否是macOS
執行環境 - 判斷
macOS
的系統版本,如果小於10.11
則說明app
太陳舊了,需要禁用掉non-pointer isa
- 然後再遍歷所有的
Mach-O
的頭部,判斷如果有__DATA__,__objc_rawisa
段的存在,則禁用掉non-pointer isa
,因爲很多新的app
加載老的擴展的時候會需要這樣的判斷操作。
預先優化過的類不會加入到
gdb_objc_realized_classes
這個哈希表中來,gdb_objc_realized_classes
哈希表的裝載因子爲 0.75,這是一個經過驗證的效率很高的擴容臨界值。
- 加載所有類到類的
gdb_objc_realized_classes
表中來
我們查看這個表的定義:
// 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
:
- 通過
objc_allocateClassPair
開闢之後的類和元類存儲的表(也就是說需要alloc
)
其實 gdb_objc_realized_classes
對 allocatedClasses
是一種包含的關係,一張是類的總表,一張是已經開闢了內存的類表,
Discover classes 流程
Discover classes. Fix up unresolved future classes. Mark bundle classes.
發現類。修正未解析的future
類,標記bundle
類。
- 先通過
_getObjc2ClassList
來獲取到所有的類,我們可以通過MachOView
來驗證:
- 接着還是遍歷所有的
Mach-O
的header
部分,然後通過mustReadClasses
來判斷哪些條件可以跳過讀取類這一步驟 - 讀取
header
是否是Bundle
- 讀取
header
是否開啓了 預優化 - 遍歷
_getObjc2ClassList
取出的所有的類- 通過
readClass
來讀取類信息 - 判斷如果不相等並且
readClass
結果不爲空,則需要重新爲類開闢內存
- 通過
Fix up remapped classes 流程
修復 重映射類
類表和非懶加載類表沒有被重映射 (也就是 _objc_classlist)
由於消息轉發,類引用和父類引用會被重映射 (也就是 _objc_classrefs)
**
- 通過
noClassesRemapped
方法判斷是否有類引用(_objc_classrefs)需要進行重映射- 如果需要,則遍歷
EACH_HEADER
- 通過
_getObjc2ClassRefs
和_getObjc2SuperRefs
取出當前遍歷到的Mach-O
的類引用和父類引用,然後調用remapClassRef
進行重映射
- 如果需要,則遍歷
Fix up @selector references 流程
修正
SEL
引用
- 操作前先加一個
selLock
鎖 - 然後遍歷
EACH_HEADER
- 如果開啓了預優化,contiue 到下一個
Mach-O
- 通過
_getObjc2SelectorRefs
拿到所有的SEL
引用 - 然後對所有的
SEL
引用調用sel_registerNameNoLock
進行註冊
- 如果開啓了預優化,contiue 到下一個
也就是說這一流程最主要的目的就是註冊 SEL
,我們註冊真正發生的地方: __sel_registerName
,這個函數如果大家經常玩 Runtime
肯定不會陌生:
我們簡單分析一下 __sel_registerName
方法的流程:
- 判斷是否要加鎖
- 如果
sel
爲空,則返回一個空的SEL
- 從
builtins
中搜索,看是否已經註冊過,如果找到,直接返回結果 - 從
namedSelectors
哈希表中查詢,找到了就返回結果 - 如果
namedSelectors
未初始化,則創建一下這個哈希表 - 如果上面的流程都沒有找到,則需要調用
sel_alloc
來創建一下SEL
,然後把新創建的SEL
插入哈希表中進行緩存的填充
Fix up old objc_msgSend_fixup call sites 流程
修正舊的
objc_msgSend_fixup
調用
**
這個流程的執行前提是 FIXUP
被開啓。
- 還是老套路,遍歷
EACH_HEADER
- 通過
_getObjc2MessageRefs
方法來獲取當前遍歷到的Mach-O
鏡像的所有消息引用 - 然後遍歷這些消息引用,然後調用
fixupMessageRef
進行修正
- 通過
Discover protocols 流程
發現協議,並修正協議引用
**
Fix up @protocol references 流程
對所有的協議做重映射
**
Realize non-lazy classes 流程
初始化非懶加載類(
**+load**
方法和靜態實例)
Realize newly-resolved future classes 流程
初始化新解析出來的
future
類
**
Discover categories 流程
處理所有的分類,包括類和元類
**
到這裏, _read_images
的流程就分析完畢,我們可以新建一個文件來去掉一些干擾的信息,只保留核心的邏輯,這樣從宏觀的角度來分析更直觀:
Q & A 環節
Q:dyld
主要邏輯是加載庫,也就是鏡像文件,但是加載完是怎麼讀取的呢?
A:_read_images
是真正讀取的地方Q:
SEL
方法編號何時加載?
A:_read_images
3.3 read_class 分析
我們探索了 _read_images
方法的流程,接下來讓我們把目光放到本文的主題 - 類的加載
既然是類的加載,那麼我們在前面所探索的類的結構中出現的內容都會一一重現。
所以我們不妨直接進行斷點調試,讓我們略過其它干擾信息,聚焦於類的加載。
- 根據上一小節我們探索的結果,
doneOnce
流程中會創建兩個哈希表,並沒有涉及到類的加載,所以我們跳過 - 我們來到第二個流程 - **類處理 **
我們在下圖所示的位置處打上斷點:
**
如上圖所示,從 classList
中取出的 cls
只是一個內存地址,我們嘗試通過 LLDB
打印 cls
的 clas_rw_t
:
可以看到 cls
的屬性、方法、協議以及類名都爲空,說明這裏類並沒有被真正加載完成,我們接着聚焦到 read_class
函數上面,我們進入其內部實現,我們大致瀏覽之後會定位到如下圖所示的代碼:
看起來類的信息在這裏完成了加載,那麼爲了驗證我們的猜想,直接斷點調試一下但發現斷點根本走不進來,原因在於這裏的判斷語句
if (Class newCls = popFutureNamedClass(mangledName))
判斷當前傳入的類的類名是否有 future
類的實現,但是我們剛纔已經打印了,類名是空的,所以肯定不會執行這裏。我們接着往下走:
- addNamedClass 內部其實是將
cls
插入到gdb_objc_realized_classes
表
- addclassTableEntry 內部是將
cls
插入到allocatedClasses
表
分析完 read_class
,我們回到 _read_images
方法
我們可以看到 read_class
返回的 newCls
會進行一個判斷,判斷與傳入 read_class
之前的 cls
是否相等,而在 read_class
內部只有一個地方對類的內容進行了改動,但是我們剛纔測試了是進不去的,所以這個 if
裏面的內容我們可以略過,也就是說 resolvedFutureClasses
的內容我們都可以暫時略過。
總結一下 readClass
:
- 判斷是不是要後期處理的類
- 如果是的話,就取出後期處理的類,讀取這個類的
data()
類設置ro/rw
- 如果是的話,就取出後期處理的類,讀取這個類的
- addNamedClass 插入總表
- addClassTableEntry 插入已開闢內存的類的表
3.4 realizeClassWithoutSwift 分析
通過分析 read_class
,我們可以得知,類已經被註冊到兩個哈希表中去了,那麼現在一切時機都已經成熟了。但是我們還是要略過像 Fix up remapped classes
、 Fix up @selector references
、 fix up old objc_msgSend_fixup call sites
、 Discover protocols. Fix up protocol refs
、 Fix up @protocol references
,因爲我們的重點是類的加載,我們最終來到了 Realize non-lazy classes (for +load methods and static instances)
,略去無關信息之後,我們可以看到我們的
主角 realizeClassWithoutSwift
閃亮登場了:
從方法的名稱以及方法註釋我們可以知道, realizeClassWithoutSwift
是進行類的第一次初始化操作,包括分配讀寫數據也就是我們常說的 rw
,但是並不會進行任何的 Swift
端初始化。我們直接聚焦下面的代碼:
- 通過
calloc
開闢內存空間,返回一個新的rw
- 把
cls
取出來的ro
賦值給這個rw
- 將
rw
設置到cls
身上
那麼是不是說在這裏 rw
就有值了呢,我們 LLDB
打印大法走起:
可以清楚地看到,此時 rw
還是爲空,說明這裏只是對 rw
進行了初始化,但是方法、屬性、協議這些都沒有被添加上。
我們接着往下走:
這裏可以看到父類和元類都會遞歸調用 realizeClassWithoutSwift
來初始化各自的 rw
。爲什麼在類的加載操作裏面要去加載類和元類呢?回憶一下類的結構,答案很簡單,要保證 superclass
和 isa
的完整性,也就是保證類的完整性,
上面的截圖就是最好的證明,初始化完畢的父類和元類被賦值到了類的 superclass
和 isa
上面。
接着往下走可以看到,不光要把父類關聯到類上面,還要讓父類知道子類的存在。
最後一行代碼是 methodizeClass(cls)
,註釋顯示的是 attach categories
,附加分類到類?我們進入其內部實現一探究竟。
在探索 methodizeClass
前,我們先總結一下 realizeClassWithoutSwift
:
- 讀取
class
的data()
ro/rw
賦值- 父類和元類實現
- supercls = realizeClassWithoutSwift(remapClass(cls->superclass))
- metacls = realizeClassWithoutSwift(remapClass(cls->ISA()))
- 父類和元類歸屬關係
- cls->superclass = supercls
- cls->initClassIsa(metacls)
- 將當前類鏈接到其父類的子類列表 addSubclass(supercls, cls)
3.5 methodizeClass 分析
對類的方法列表、協議列表和屬性列表進行修正
附加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
,爲什麼方法、屬性、協議都能調用這個方法呢?
我們可以看到,方法、屬性、協議的數據結構都是一個二維數組,我們深入 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,直接返回
- 判斷當前調用
attachLists
的list_array_tt
二維數組有多個一維數組- 如果是,說明是多對多的關係
- 這裏會通過
realloc
對容器進行重新分配,大小爲原來的大小加上新增的大小 - 然後通過
memmove
把原來的數據移動到容器的末尾 - 最後把新的數據拷貝到容器的起始位置
- 如果調用
attachLists
的list_array_tt
二維數組爲空且新增大小數目爲 1,則直接取addedList
的第一個list
返回 - 如果當前調用
attachLists
的list_array_tt
二維數組只有一個一維數組
四、探索 load_images
我們接着探索 _dyld_objc_notify_register
的第二個參數 load_images
,這個函數指針是在什麼時候調用的呢,同樣的,我們接着在 dyld
源碼中搜索對應的函數指針 sNotifyObjCInit
:
可以看到,在 notifySingle
方法內部, sNotifyObjCInit
函數指針被調用了。根據我們上一篇文章探索 dyld
底層可以知道, _load_images
應該是對於每一個加載進來的 Mach-O
鏡像都會遞歸調用一次。
我們來到 libObjc
源碼中 load_images
的定義處:
處理由
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
裏面
- 遞歸調用遍歷父類的
- 完成
schedule_class_load
之後,通過_getObjc2NonlazyCategoryList
取出所有分類數據 - 然後遍歷這些分類
- 通過
realizeClassWithoutSwift
來防止類沒有初始化,如果已經初始化了則不影響 - 通過
add_category_to_loadable_list
,加載分類中的load
方法到loadable_categories
裏面
- 通過
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
底層有時候探索起來確實很枯燥,但是如果能找到高效的方法以及明確自己的所探索的方向,會讓自己從宏觀上重新審視這門技術。是的,技術只是工具,我們不能被技術所綁架,我們要做到有的放矢的去探索,這樣才能事半功倍。