原文鏈接: 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
方法,主函數中也沒有任何的東西:
雖然在主函數中什么方法都沒有調用,但是運行之后,依然打印了 XXObject load
字符串,也就是說調用了 + load
方法。
使用符號斷點
使用 Xcode 添加一個符號斷點 +[XXObject load]
:
注意這里
+
和[
之間沒有空格
為什么要加一個符號斷點呢?因為這樣看起來比較高級。
重新運行程序。這時,代碼會停在 NSLog(@"XXObject load");
這一行的實現上:
左側的調用棧很清楚的告訴我們,哪些方法被調用了:
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;
}
什么是鏡像
這里就會遇到一個問題:鏡像到底是什么,我們用一個斷點打印出所有加載的鏡像:
從控制臺輸出的結果大概就是這樣的,我們可以看到鏡像并不是一個 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 中的運行結果相同:
準備 + 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_methods
對 load
方法的調用進行準備(將需要調用 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);
...
}
方法的調用流程大概是這樣的:
其中 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_classes
和 loadable_categories
。
方法的調用過程也分為兩個部分,準備 load
方法和調用 load
方法,我更覺得這兩個部分比較像生產者與消費者:
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++;
}
方法剛被調用時:
- 會從
class
中獲取load
方法:method = cls->getLoadMethod();
- 判斷當前
loadable_classes
這個數組是否已經被全部占用了:loadable_classes_used == loadable_classes_allocated
- 在當前數組的基礎上擴大數組的大小:
realloc
- 把傳入的
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);
調用順序如下:
- 不停調用類的
+ load
方法,直到loadable_classes
為空 - 調用一次
call_category_loads
加載分類 - 如果有
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;
}
這個方法有些長,我們來分步解釋方法的作用:
- 獲取當前可以加載的分類列表
- 如果當前類是可加載的
cls && cls->isLoadable()
就會調用分類的load
方法 - 將所有加載過的分類移除
loadable_categories
列表 - 為
loadable_categories
重新分配內存,并重新設置它的值
調用的順序
你過去可能會聽說過,對于 load
方法的調用順序有兩條規則:
- 父類先于子類調用
- 類先于分類調用
這種現象是非常符合我們的直覺的,我們來分析一下這種現象出現的原因。
第一條規則是由于 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 中的方法都是安全的。
參考資料
- NSObject +load and +initialize - What do they do?
- Method Swizzling
- Objective-C Class Loading and Initialization
關注倉庫,及時獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github