引子
我們都知道: Objective-C中類Class
的+load
方法會在類第一次加載到內(nèi)存時, 并且APP的整個生命周期只會執(zhí)行一次. 但是知其然最好知其所以然, 今天來分析一下+load
方法執(zhí)行的來龍去脈.
準(zhǔn)備工作, 本文涉及到的Apple 開源源碼如下:
- dyld-635.2
- objc4-750
上一篇文章<<iOS APP啟動前后發(fā)生了什么?>>
開篇, 有如下的調(diào)用棧:
0 +[AppDelegate load]
1 call_load_methods
2 load_images
// 這里是一個斷層
3 dyld::notifySingle(dyld_image_states, ImageLoader const*)
4 ImageLoader::recursiveInitialization(...)
5 ImageLoader::processInitializers(...)
6 ImageLoader::runInitializers(...)
7 dyld::_main(...)
8 dyldbootstrap::start(...)
9 _dyld_start
我們能看到實際最后會調(diào)用+[Class load]
類方法, 我們發(fā)現(xiàn)從調(diào)用棧那里有一個斷層, 3 dyld::notifySingle(dyld_image_states, ImageLoader const*)
-> +[AppDelegate load]
的過程, 明顯不是在dyld, ImageLoader
庫中, 而是在runtime
中的方法, 重要的原因就是dyld::notifySingle
是對外發(fā)送兩個一個通知, 而loadImage
是針對通知注冊的handler.
而前文在講到, 當(dāng)運行到后面會在runtime
初始化時調(diào)用_objc_init
, 這個方法最后會調(diào)用dyld::_dyld_objc_notify_register
方法注冊三個hanlder, 其中有一個方法就是runtime
的load_images
, 因此dyld::notifySingle
實際是發(fā)出了某個通知, 觸發(fā)load_images
.
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中加入一個監(jiān)聽器, 一旦dyld監(jiān)聽到有新的鏡像加載到runtime時, 就調(diào)用 load_images 方法, 并傳入最新鏡像的信息類別 infoList
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
dyld_image
的state的監(jiān)聽與通知
為了證明我們前面的內(nèi)容, 我們需要在源碼中去找到線索.
我們打開dyld
的源碼dyld_priv.h
, 中的關(guān)于dyld_image_states
的定義:
// DEPRECATED
// dyld_image 整個生命周期中會經(jīng)歷的狀態(tài)
enum dyld_image_states {
dyld_image_state_mapped = 10, // No batch notification for this - 是否已經(jīng)映射
dyld_image_state_dependents_mapped = 20, // Only batch notification for this - 依賴是否映射
dyld_image_state_rebased = 30, // rebase
dyld_image_state_bound = 40, // 已經(jīng)bound
dyld_image_state_dependents_initialized = 45, // Only single notification for this
dyld_image_state_initialized = 50, // -- 已經(jīng)初始化!!!!! 重要的狀態(tài)
dyld_image_state_terminated = 60 // Only single notification for this
};
而且dyld_image.state
的狀態(tài)切換都是在dyld::_main(...)
方法中進行的, 我們將該方法簡寫如下:
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[], uintptr_t* startGlue){
...
// dyld::instantiateFromLoadedImage -> ImageLoaderMachOClassic::instantiateMainExecutable(create image for main executable) -> setMapped -> dyld_state = dyld_image_state_mapped-> 發(fā)出notification
// 初始化完成以后, sMainExecutable被push到 sAllImages, 并且將它的關(guān)鍵信息插入到MappedRanges鏈表(這個鏈表中的內(nèi)容已經(jīng)mapped完畢)
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath); // dyld_image_state_mapped 并通知
...
/*
注意, 這里執(zhí)行 link(...) 時, linkingMainExecutable = true!!!
1. recursiveLoadLibraries -> dyld_image_state_dependents_mapped 并通知
2. recursiveRebase -> dyld_image_state_rebased 并通知
(不會執(zhí)行: 3. recursiveBindWithAccounting -> recursiveBind -> dyld_image_state_bound)
(不會執(zhí)行: 4. weakBind -> 通知 dyld_image_state_bound)
*/
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
...
gLinkContext.linkingMainExecutable = false;
// 從這里開始 linkingMainExecutable = false, 也就是 MainImageLoader完成link操作!!! 切換 MainImageLoader.fstate = dyld_image_state_bound, 并發(fā)送 dyld_image_state_bound_notify 通知
// Bind and notify for the main executable now that interposing has been registered
uint64_t bindMainExecutableStartTime = mach_absolute_time();
sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
uint64_t bindMainExecutableEndTime = mach_absolute_time();
ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
gLinkContext.notifyBatch(dyld_image_state_bound, false);
...
/*
這里開始執(zhí)行各個dyld_image的initializers方法, 當(dāng)初始化完成以后, 就MainImageLoader.fstate = dyld_image_state_initialized, 并發(fā)送 dyld_image_state_initialized_notify 通知
1. initializeMainExecutable
2. sMainExecutable->runInitializers
3. sMainExecutable->processInitializers
4. context.notifyBatch(dyld_image_state_initialized, false);
*/
initializeMainExecutable();
...
return Main函數(shù)的入口
}
在梳理之前, 我們需要有一個簡單的概念, 關(guān)于
link(..)
過程中的rebase
和bind
.當(dāng)
mach-o
或dyld_image
二進制文件被加載到內(nèi)存中以后, 由于地址空間加載隨機化(ASLR
, Address Space Layout Randomization)的緣故, 二進制文件最終的加載地址與預(yù)期地址之間會存在偏移, 所以需要進行rebase
操作, 對那些指向文件內(nèi)部符號的指針進行修正, 在link
函數(shù)中該項操作由recursiveRebase
函數(shù)執(zhí)行.rebase
完成之后, 就會進行bind
操作, 修正那些指向其他二進制文件所包含的符號的指針, 由recursiveBind
函數(shù)執(zhí)行呜魄。 當(dāng)rebase
以及bind
結(jié)束時,link
函數(shù)就完成了它的使命.
我們能看到在dyld::_main(...)
函數(shù)中dyld_image
會隨著過程切換自己的state狀態(tài), 并且對外發(fā)出相關(guān)狀態(tài)的通知.
同時我們在源碼中有如下代碼:
// DEPRECATED -- 當(dāng) dyld_image 的state狀態(tài)變化以后, 調(diào)用的回調(diào)函數(shù)callback格式如下
typedef const char* (*dyld_image_state_change_handler)(enum dyld_image_states state, uint32_t infoCount, const struct dyld_image_info info[]);
// 注冊的方法的 函數(shù)指針當(dāng) mapped/ init/ unmapped 狀態(tài)時, 分別調(diào)用的callback格式如下
typedef void (*_dyld_objc_notify_mapped)(unsigned count, const char* const paths[], const struct mach_header* const mh[]);
typedef void (*_dyld_objc_notify_init)(const char* path, const struct mach_header* mh);
typedef void (*_dyld_objc_notify_unmapped)(const char* path, const struct mach_header* mh);
//
// Note: only for use by objc runtime
// 這個方法只有在 runtime 的 _objc_init 方法中調(diào)用
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded.
// 1. During the call to _dyld_objc_notify_register(), dyld will call the "mapped" function with already loaded objc images.
// 2. During any later dlopen() call, dyld will also call the "mapped" function. (每一次調(diào)用dlopen(), 都會調(diào)用'mapped' function)
// 3. Dyld will call the "init" function when dyld would be called initializers in that image. This is when objc calls any +load methods in that image. - 當(dāng) image狀態(tài)變化成 initializer 時候, 會調(diào)用`init` callback, 這個callback在實際代碼中是調(diào)用的 objc4.750 的 `loadImages` 方法, 這個方法內(nèi)部會調(diào)用這個`image`中的每個`Class`的`+load`方法
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped) {
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
注意上面這個方法_dyld_objc_notify_register
是在dyld::_main
方法中的mainImageLoader
的link(...)
方法結(jié)束以后, 由于依賴的庫中有libSystem
和libCloure
從而加載runtime
的_objc_init(...)
方法結(jié)束時候才調(diào)用, 因此在執(zhí)行_dyld_objc_notify_register
以后, 相當(dāng)于runtime
就會監(jiān)聽所有在runtime
之后被加載的dyld_image
, 根據(jù)他們的的狀態(tài), 去調(diào)用注冊的3個回調(diào)函數(shù), 這里我們重點關(guān)注load_images
方法.
load_images
在runtime
的_objc_init(...)
被注冊以后,一旦dyld
中有新的image
狀態(tài)成為init
(也就是dyld_image_state_initialized
, 此時表示該image
已經(jīng)完成link
), 就會調(diào)用load_images
方法, 對這個完全初始化成功的image
中的內(nèi)容做一些處理.
objc中的load_images
load_images
方法的源碼如下:
/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
有新的鏡像image被加載到 runtime 時娇澎,調(diào)用 load_images 方法睹晒,并傳入最新鏡像image的信息列表 infoList:
images 是鏡像的意思: 這里就會遇到一個問題:鏡像到底是什么,我們用一個斷點打印出所有加載的鏡, 從控制臺輸出的結(jié)果大概就是這樣的岔激,我們可以看到鏡像并不是一個 Objective-C 的代碼文件是掰,它應(yīng)該是一個 target 的編譯產(chǎn)物辱匿。這里面有很多的動態(tài)鏈接庫,還有一些蘋果為我們提供的框架匾七,比如 Foundation、 CoreServices 等等丁频,都是在這個 load_images 中加載進來的,而這些 imageFilePath 都是對應(yīng)的二進制文件的地址席里。
+load 的應(yīng)用:
+load 可以說我們在日常開發(fā)中可以接觸到的調(diào)用時間最靠前的方法,在主函數(shù)運行之前改基,load 方法就會調(diào)用咖为。
由于它的調(diào)用不是惰性的,且其只會在程序調(diào)用期間調(diào)用一次躁染,最最重要的是,如果在類與分類中都實現(xiàn)了 load 方法诺舔,它們都會被調(diào)用备畦,不像其它的在分類中實現(xiàn)的方法會被覆蓋,這就使 load 方法成為了方法調(diào)劑的絕佳時機懂盐。
但是由于 load 方法的運行時間過早,所以這里可能不是一個理想的環(huán)境拌喉,因為某些類可能需要在在其它類之前加載俐银,但是這是我們無法保證的。不過在這個時間點捶惜,所有的 framework 都已經(jīng)加載到了運行時中,所以調(diào)用 framework 中的方法都是安全的汽久。
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);
void load_images(const char *path __unused, const struct mach_header *mh) {
// Return without taking locks if there are no +load methods here.
// 如果 沒有 +load 方法, 直接返回
if (!hasLoadMethods((const headerType *)mh)) return;
// 此時表示 mh中有 +load 方法
// 上鎖, 不能同時多個線程執(zhí)行 loadMethod, 鎖1
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
// runtimeLock 兩個鎖, 鎖2
// 這里 write-locks 需要兩個鎖
mutex_locker_t lock2(runtimeLock);
//調(diào)用 prepare_load_methods 對 load 方法的調(diào)用進行準(zhǔn)備, 主要工作就是將Class的所有方法都加載到一個叫l(wèi)oadable_classes的數(shù)組中
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
// 在將鏡像加載到運行時, 對 load 方法的準(zhǔn)備就緒之后踊餐,執(zhí)行 call_load_methods,開始調(diào)用 load 方法
call_load_methods();
}
從load_images
中的源碼游走以后, 我們主要看到兩個重要的步驟 -- 準(zhǔn)備load
和調(diào)用load
:
- 當(dāng)有新的鏡像被dyld加載, runtime就會去該鏡像中對所有的
class/category
進行準(zhǔn)備操作. - 準(zhǔn)備操作是
prepare_load_methods
- 調(diào)用操作是
call_load_methods
objc中如何準(zhǔn)備 -- prepare_load_methods
解析
/**
準(zhǔn)備load methods
*/
void prepare_load_methods(const headerType *mhdr) {
size_t count, i;
// 調(diào)用 load_method 時, 必須是 runtimeLock已經(jīng)上鎖
runtimeLock.assertLocked();
//處理mach-o中的class:
// 通過 _getObjc2NonlazyClassList 獲取二進制文件中所有的類的列表之后三痰,會通過 remapClass 獲取類對應(yīng)的指針,然后調(diào)用 schedule_class_load 遞歸地安排當(dāng)前類的父類和當(dāng)前類加入到一個 loadable_list中
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
// 內(nèi)部處理以后調(diào)用 add_class_to_loadable_list方法
schedule_class_load(remapClass(classlist[i]));
}
//處理mach-o中的categorys:
// 通過 _getObjc2NonlazyCategoryList 方法獲取二進制文件中所有的category的列表, 然后遞歸處理每個單獨的category.
// 單獨處理Category的過程如下: 首先獲取每個category的Class, 然后先調(diào)用一個關(guān)鍵的方法`realizeClass`, 這個方法能夠保證每個類已經(jīng)被runtime進行了`realize`過, 這個過程很重要(后面有專門的文章來解釋整個realize的過程), 然后調(diào)用`add_category_to_loadable_list`將category方法加入loadable_list
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 做的工作就是Class第一次 initiail
realizeClass(cls);
assert(cls->ISA()->isRealized());
// 獲取 category中的+load方法, 然后按照一定順序?qū)?load方法加入到一個loadabel_list中
add_category_to_loadable_list(cat);
}
}
通過源碼注釋, 我們可以看出準(zhǔn)備過程會處理兩塊內(nèi)容, 分別是鏡像中的class
以及category
.
如果處理鏡像中的class
, 過程是:
-
_getObjc2NonlazyClassList
獲取二進制文件中所有的類, 放到一個鏈表中, 然后遞歸處理每個class
- 遍歷這個鏈表, 取出每個節(jié)點, 先調(diào)用
remapClass
, 然后調(diào)用schedule_class_load
-
schedule_class_load
主要是將入?yún)⒌?code>class的繼承鏈的每個+load
方法都加入到loadable_classes
鏈表中. 注意這里的添加+load
方法到鏈表的順序是, 先父類, 然后自己.
如果處理鏡像中的category
, 過程有點不一樣:
-
_getObjc2NonlazyCategoryList
方法獲取二進制中所有的category, 放到一個鏈表中, 然后遞歸處理每個category
- 單獨
category
的過程是: 首先獲取每個category
對應(yīng)的class
, 先對class
進行remapClass
,調(diào)用一個關(guān)鍵的方法realizeClass
(我們可以認(rèn)為這個方法是Class類對象在內(nèi)存中的初始化創(chuàng)建方法),最后調(diào)用add_category_to_loadable_list
方法 -
add_category_to_loadable_list
是將category
按照一定順序?qū)?code>+load方法加入到一個叫做loadable_categories
鏈表中.
realizeClass
方法我們后面專門分析, 這里我們只簡單了解一下. 我們直到Class
在編譯期間, 有很多方法是我們自己在代碼里面定義的, 這些方法在編譯器編譯期間就搞定了, 當(dāng)它加載到內(nèi)存時候, 它的方法列表里面都是編譯期間確定的方法, 我們稱為只讀方法, 但是還有一些方法例如在category
中的方法, 也是與Class
有關(guān)的, 但是并沒有與這個Class
關(guān)聯(lián), 通過realizeClass
來調(diào)整類在內(nèi)存中的結(jié)構(gòu), 例如將category
中的方法都關(guān)聯(lián)到class
上去, 添加到class
的方法列表中, 當(dāng)然, 還有一些其他的作用, 后面再講.
objc中正式調(diào)用每個類的+load
方法 -- call_load_methods
解析
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(); // 這里會調(diào)用 load 方法
}
// 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;
}
static void call_class_loads(void) {
int i;
// Detach current loadable list.
// 用一個便利結(jié)構(gòu)體, 內(nèi)部持有Class對應(yīng)方法的+load IMP
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
// 遍歷所有的 loadable_classes 中的每個 loadable Class, 從中按照順序取出+load方法, 按照 loadable_list 的順序執(zhí)行!!!
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;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
// 某個類的 +load 方法會執(zhí)行!!!
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
簡單來說, 就是將loadable_classes
中之前存儲的class
的+load
調(diào)用, 然后清理, 最后調(diào)用loadable_categories
緩存的category
相關(guān)的+load
方法.
小總結(jié)
-
+load
方法是如何被調(diào)用的舷丹?
runtime
在初始化時, 會注冊一個回調(diào), 去監(jiān)聽鏡像加載, 每次有新的鏡像加載時, 就會調(diào)用注冊的load_images
回調(diào), 這個方法會將鏡像中所有類和分類的+load
方法按照一定順序, 放到loadable_classes
和loadable_categories
兩個鏈表中, 然后遍歷執(zhí)行兩個鏈表中的每個節(jié)點的+load
方法.
-
+load
方法的調(diào)用順序如何?- 父類的
+load
會先調(diào)用, 然后才調(diào)用子類+load
- 類的
+load
會先調(diào)用, 然后調(diào)用分類的+load
- 總得來說, 會先調(diào)用super類的
+load
方法, 然后調(diào)用自身的+load
方法, 最后調(diào)用分類重寫的+load
方法.
- 父類的
并且結(jié)合前面文章我們知道: +load
方法會先于app的啟動方法main
執(zhí)行, 并且它在全局只會調(diào)用一次等特性, +load
方法是讓我們實現(xiàn)的method swizzling
最佳位置!!!!
就算分類重寫了
+load
方法, 通過上面分析, 仍然會按照父類, 本類, 分類的順序執(zhí)行+load
方法. 需要注意這點比較特殊, 與其他方法的執(zhí)行不一樣!!!
參考
iOS程序啟動->dyld加載->runtime初始化(初識)
你真的了解load方法么?
http://www.cocoachina.com/ios/20170716/19876.html