關注倉庫,及時得到更新:iOS-Source-Code-Analyze
Follow: Draveness · Githubhtml由於 ObjC 的 runtime 只能在 Mac OS 下才能編譯,因此文章中的代碼都是在 Mac OS,也就是
x86_64
架構下運行的,對於在 arm64 中運行的代碼會特別說明。ios
文章的標題與其說是問各位讀者,不如說是問筆者本身:我真的瞭解
+ load
方法麼?git
+ load
做爲 Objective-C 中的一個方法,與其它方法有很大的不一樣。它只是一個在整個文件被加載到運行時,在 main
函數調用以前被 ObjC 運行時調用的鉤子方法。其中關鍵字有這麼幾個:github
文件剛加載objective-c
main
函數以前數組
鉤子方法安全
我在閱讀 ObjC 源代碼以前,曾經一度感受本身對 + load
方法的做用很是瞭解,直到看了源代碼中的實現,才知道之前的覺得,只是本身的覺得罷了。架構
這篇文章會假設你知道:app
使用過 + 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_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
方法的準備就緒以後,執行 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
。
在調用 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
的提供,下面會開始消耗列表中的元素。
調用 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
方法的運行時間過早,因此這裏可能不是一個理想的環境,由於某些類可能須要在在其它類以前加載,可是這是咱們沒法保證的。不過在這個時間點,全部的 framework 都已經加載到了運行時中,因此調用 framework 中的方法都是安全的。
關注倉庫,及時得到更新:iOS-Source-Code-Analyze
Follow: Draveness · Github