在寫 《iOS:load方法能不能被hook?》 和 《iOS啟動優(yōu)化:App啟動耗時在線監(jiān)控與AppDelegate管控》 兩篇文章時都提到了動態(tài)庫的加載,由于主題的原因,沒有詳細(xì)介紹,有同學(xué)對這個比較感興趣,今天我們就來研究下在iOS中動態(tài)庫的加載順序是什么樣子的。
1.實驗篇
我們先通過demo看下幾種Case:
-
沒有依賴關(guān)系:
情形一
我們制作dylibA
、dylibB
、dylibB
這三個動態(tài)庫(不了解動態(tài)庫的制作的請問度娘...),且它們沒有依賴關(guān)系,同時我們在每個庫中添加一個Class,暫且以Class的load方法的調(diào)用順序當(dāng)做是動態(tài)庫的加載順序(先挖一個坑),比如dylibA
:@implementation ClassA + (void)load { NSLog(@"dylibA loaded"); } @end
將這三個庫加到demo工程中,并且保證
Build Phases
-Link Binary With Libraries
中的順序:dylibA
>dylibB
>dylibC
,運行結(jié)果:Demo[53199:17384949] dylibA loaded Demo[53199:17384949] dylibB loaded Demo[53199:17384949] dylibC loaded
我們調(diào)整下
Link Binary With Libraries
中的順序:dylibC
>dylibB
>dylibA
,運行結(jié)果:Demo[53265:17397552] dylibC loaded Demo[53265:17397552] dylibB loaded Demo[53265:17397552] dylibA loaded
通過實驗我們知道:在沒有依賴關(guān)系的情況下,動態(tài)庫的加載順序由
Link Binary With Libraries
中的順序決定,當(dāng)然我們可以通過Link Binary With Libraries
來控制動態(tài)庫的加載順序。 -
單一依賴關(guān)系
情形二
dylibA
依賴dylibB
,dylibB
依賴dylibC
,我們簡單改造下這三個庫,如在dylibB
中import下dylibC
的頭文件,dylibA
中同理:#import "ClassB.h" #import <dylibC/dylibC.h> @implementation ClassB + (void)load { NSLog(@"dylibB loaded"); } @end
Link Binary With Libraries
中的順序:dylibA
>dylibB
>dylibC
,運行結(jié)果:Demo[53570:17450857] dylibC loaded Demo[53570:17450857] dylibB loaded Demo[53570:17450857] dylibA loaded
這次我們發(fā)現(xiàn)三個庫的加載順序是反的,我們修改下順序:
dylibC
>dylibA
>dylibB
,運行結(jié)果不變。由實驗結(jié)果可知:動態(tài)庫的加載順序還受依賴關(guān)系影響,被依賴的子節(jié)點優(yōu)先加載。
-
組合依賴關(guān)系
組合關(guān)系1其中,
dylibA
、dylibB
、dylibB
沒有依賴關(guān)系,dylibA
依賴了dylibD
、dylibE
、dylibF
。Demo[97898:19286936] dylibD loaded Demo[97898:19286936] dylibF loaded Demo[97898:19286936] dylibE loaded Demo[97898:19286936] dylibA loaded Demo[97898:19286936] dylibB loaded Demo[97898:19286936] dylibC loaded
通過修改
dylibA
-Link Binary With Libraries
中dylibD
、dylibE
的順序調(diào)整為:
組合關(guān)系2Demo[97982:19305235] dylibF loaded Demo[97982:19305235] dylibE loaded Demo[97982:19305235] dylibD loaded Demo[97982:19305235] dylibA loaded Demo[97982:19305235] dylibB loaded Demo[97982:19305235] dylibC loaded
通過上面的嘗試,我們可以看出動態(tài)庫的加載順序為:先根據(jù)配置的鏈接順序加載,如有依賴的先遞歸式加載依賴。
2.源碼篇
上面我們通過幾個實驗對動態(tài)庫的加載有個大概的影響,下面我們通過源碼進(jìn)一步了解動態(tài)庫的加載(dyld源碼,本文使用版本:dyld-635.2,篇幅問題,只展示部分代碼)。
為了便于的過程,我們在上面demo中的位于子節(jié)點的dylibF
庫的load方法添加一個符號斷點,獲取下這個load方法的調(diào)用棧:
#0 +[ClassF load]
#1 load_images ()
#2 dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) ()
#3 ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#4 ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#5 ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#6 ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#7 ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
#8 ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) ()
#9 dyld::initializeMainExecutable()
#10 dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) ()
#11 dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*) ()
#12 _dyld_start ()
_dyld_start
和 dyldbootstrap::start
使用匯編實現(xiàn),我們重點看下dyld::_main
開始的實現(xiàn)。
-
dyld::_main
,動態(tài)鏈接器的入口函數(shù),代碼較多,只展示部分代碼:// // Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which // sets up some registers and call this function. // // Returns address of main() in target program which __dyld_start jumps to // uintptr_t _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[], uintptr_t* startGlue) { //1.配置環(huán)境 ...很多代碼 //2.加載共享緩存 checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide); //3.實例化主程序 sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath); gLinkContext.mainExecutable = sMainExecutable; gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH); //4.插入動態(tài)庫(使用動態(tài)庫注入代碼就是通過這個實現(xiàn)的) if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) { for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) loadInsertedDylib(*lib); } //5.鏈接主程序 gLinkContext.linkingMainExecutable = true; link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1); //6.鏈接插入的動態(tài)庫 ...很多代碼 //7.初始化主程序 initializeMainExecutable(); //...快結(jié)束了 }
和我們今天主題相關(guān)的為
link()
和initializeMainExecutable()
兩個過程,分別負(fù)責(zé)鏈接(符號rebase
,binding
)和初始化(調(diào)用load
,__attribute__((constructor)
) 修飾的c函數(shù)等)的工作。 -
link()
void link(ImageLoader* image, bool forceLazysBound, bool neverUnload, const ImageLoader::RPathChain& loaderRPaths, unsigned cacheIndex) { // add to list of known images. This did not happen at creation time for bundles if ( image->isBundle() && !image->isLinked() ) addImage(image); // we detect root images as those not linked in yet if ( !image->isLinked() ) addRootImage(image); // process images try { const char* path = image->getPath(); #if SUPPORT_ACCELERATE_TABLES if ( image == sAllCacheImagesProxy ) path = sAllCacheImagesProxy->getIndexedPath(cacheIndex); #endif image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path); } catch (const char* msg) { garbageCollectImages(); throw; } }
link()
中又調(diào)用ImageLoader::link
方法,ImageLoader是一個基類負(fù)責(zé)加載image文件(主程序,動態(tài)庫)每個image對應(yīng)一個ImageLoader子類的實例,我們繼續(xù)看下它的實現(xiàn): -
ImageLoader::link
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath) { //dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload); // clear error strings (*context.setErrorStrings)(0, NULL, NULL, NULL); uint64_t t0 = mach_absolute_time(); this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath); context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly); // we only do the loading step for preflights if ( preflightOnly ) return; uint64_t t1 = mach_absolute_time(); context.clearAllDepths(); this->recursiveUpdateDepth(context.imageCount()); //省略符號rebase 和binding過程... }
ImageLoader::link
中使用ImageLoader::recursiveLoadLibraries
加載動態(tài)庫,截取部分代碼: -
ImageLoader::recursiveLoadLibraries
void ImageLoader::recursiveLoadLibraries(const LinkContext& context, bool preflightOnly, const RPathChain& loaderRPaths, const char* loadPath) { //...省略很多代碼 // 獲取當(dāng)前image依賴的動態(tài)庫 DependentLibraryInfo libraryInfos[fLibraryCount]; this->doGetDependentLibraries(libraryInfos); //...省略很多代碼 for(unsigned int i=0; i < fLibraryCount; ++i){ //加載動態(tài)庫 DependentLibraryInfo& requiredLibInfo = libraryInfos[i]; dependentLib = context.loadLibrary(requiredLibInfo.name, true, this->getPath(), &thisRPaths, enforceIOSMac, cacheIndex); //...省略很多代碼 } //保存加載完成的動態(tài)庫 setLibImage(i, dependentLib, depLibReExported, requiredLibInfo.upward); // 告訴此image依賴的動態(tài)庫們遞歸的加載各自依賴的動態(tài)庫 for(unsigned int i=0; i < libraryCount(); ++i) { ImageLoader* dependentImage = libImage(i); if ( dependentImage != NULL ) { dependentImage->recursiveLoadLibraries(context, preflightOnly, thisRPaths, libraryInfos[i].name); } } //...省略很多代碼 }
此方法也比較長,它主要做的事情是:1)獲取當(dāng)前image依賴的的動態(tài)庫;2)循環(huán)加載當(dāng)前image依賴的動態(tài)庫;3)告訴當(dāng)前image依賴的動態(tài)庫們遞歸的加載各自依賴的動態(tài)庫;
具體的加載方法在dyld.cpp
的ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex)
,我們不多做介紹。從上面的代碼可以知道,dyld通過doGetDependentLibraries
獲取了image依賴的動態(tài)庫信息,然后循環(huán)加載,這個函數(shù)的實現(xiàn)會影響動態(tài)庫的加載順序,我們看下doGetDependentLibraries
的實現(xiàn)(doGetDependentLibraries
是個虛函數(shù),只有在ImageLoaderMachO.cpp
中找到了它的實現(xiàn)): -
ImageLoaderMachO::doGetDependentLibraries
void ImageLoaderMachO::doGetDependentLibraries(DependentLibraryInfo libs[]) { if ( needsAddedLibSystemDepency(libraryCount(), (macho_header*)fMachOData) ) { DependentLibraryInfo* lib = &libs[0]; lib->name = LIBSYSTEM_DYLIB_PATH; lib->info.checksum = 0; lib->info.minVersion = 0; lib->info.maxVersion = 0; lib->required = false; lib->reExported = false; lib->upward = false; } else { uint32_t index = 0; const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds; const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)]; const struct load_command* cmd = cmds; for (uint32_t i = 0; i < cmd_count; ++i) { switch (cmd->cmd) { case LC_LOAD_DYLIB: case LC_LOAD_WEAK_DYLIB: case LC_REEXPORT_DYLIB: case LC_LOAD_UPWARD_DYLIB: { const struct dylib_command* dylib = (struct dylib_command*)cmd; DependentLibraryInfo* lib = &libs[index++]; lib->name = (char*)cmd + dylib->dylib.name.offset; //lib->name = strdup((char*)cmd + dylib->dylib.name.offset); lib->info.checksum = dylib->dylib.timestamp; lib->info.minVersion = dylib->dylib.compatibility_version; lib->info.maxVersion = dylib->dylib.current_version; lib->required = (cmd->cmd != LC_LOAD_WEAK_DYLIB); lib->reExported = (cmd->cmd == LC_REEXPORT_DYLIB); lib->upward = (cmd->cmd == LC_LOAD_UPWARD_DYLIB); } break; } cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize); } } }
dyld通過讀取當(dāng)前Mach-O文件的
Load Commonds
段來獲取到它依賴的動態(tài)庫,涉及到的字段有LC_LOAD_DYLIB
、LC_LOAD_WEAK_DYLIB
、LC_REEXPORT_DYLIB
、LC_LOAD_UPWARD_DYLIB
,遍歷時通過&libs[]
記錄。而影響Load Commonds
中動態(tài)庫順序的是Xcode中Link Binary With Libraries
的順序。下圖是上文Demo工程的Mach-O中Load Commonds
的示例截圖:
Load Commonds
好了,我們通過實驗和源碼的方式了解到了動態(tài)庫的加載順序:1)先從當(dāng)前image文件(可執(zhí)行文件/動態(tài)庫)的Load Commonds
中獲取動態(tài)庫的信息(包括順序,名字,路徑等);2)然后循環(huán)加載它依賴的動態(tài)庫;3)加載完后,再根據(jù)當(dāng)前image依賴的動態(tài)庫列表遞歸加載各自依賴的動態(tài)庫。
3.填坑篇
文章開始我們就挖了一個坑:實驗的方式是建立在“l(fā)oad方法的調(diào)用順序當(dāng)做是動態(tài)庫的加載順序”這一假設(shè)上的,那么這一假設(shè)成立嗎?
從上文中+[ClassF load]
的調(diào)用??芍?code>load 方法的調(diào)用是在initializeMainExecutable()
中,在link()
之后,initializeMainExecutable()
后又調(diào)用ImageLoader::runInitializers()
,ImageLoader::processInitializers()
,我們看下代碼:
-
ImageLoader::processInitializers
中調(diào)用了ImageLoader::recursiveInitialization
,從名字看進(jìn)行了遞歸方式的初始化。void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread, InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images) { uint32_t maxImageCount = context.imageCount()+2; ImageLoader::UninitedUpwards upsBuffer[maxImageCount]; ImageLoader::UninitedUpwards& ups = upsBuffer[0]; ups.count = 0; // Calling recursive init on all images in images list, building a new list of // uninitialized upward dependencies. for (uintptr_t i=0; i < images.count; ++i) { images.images[i]->recursiveInitialization(context, thisThread, images.images[i]->getPath(), timingInfo, ups); } // If any upward dependencies remain, init them. if ( ups.count > 0 ) processInitializers(context, thisThread, timingInfo, ups); }
-
ImageLoader::recursiveInitialization
,截取部分代碼:
從上面的代碼可以看出,初始化的動態(tài)庫是從void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize, InitializerTimingList& timingInfo, UninitedUpwards& uninitUps) { //優(yōu)先初始化fDepth深的庫 for(unsigned int i=0; i < libraryCount(); ++i) { ImageLoader* dependentImage = libImage(i); if ( dependentImage != NULL ) { // don't try to initialize stuff "above" me yet if ( libIsUpward(i) ) { uninitUps.images[uninitUps.count] = dependentImage; uninitUps.count++; } else if ( dependentImage->fDepth >= fDepth ) { dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps); } } } // 告訴objc我們準(zhǔn)備初始化image了 uint64_t t1 = mach_absolute_time(); fState = dyld_image_state_dependents_initialized; oldState = fState; context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo); // 初始化image bool hasInitializers = this->doInitialization(context); // 告訴objc初始化完成 fState = dyld_image_state_initialized; oldState = fState; context.notifySingle(dyld_image_state_initialized, this, NULL); //... }
libImage()
中獲取,而libImage()
的數(shù)據(jù)是在加載動態(tài)庫的ImageLoader::recursiveLoadLibraries
中的setLibImage()
進(jìn)行保存的。通過比較fDepth
,優(yōu)先初始化fDepth
大的庫,我們再看下fDepth
的賦值: -
ImageLoader::recursiveUpdateDepth
在整個依賴樹的關(guān)系中,層級越深,unsigned int ImageLoader::recursiveUpdateDepth(unsigned int maxDepth) { // the purpose of this phase is to make the images sortable such that // in a sort list of images, every image that an image depends on // occurs in the list before it. if ( fDepth == 0 ) { // break cycles fDepth = maxDepth; // get depth of dependents unsigned int minDependentDepth = maxDepth; for(unsigned int i=0; i < libraryCount(); ++i) { ImageLoader* dependentImage = libImage(i); if ( (dependentImage != NULL) && !libIsUpward(i) ) { unsigned int d = dependentImage->recursiveUpdateDepth(maxDepth); if ( d < minDependentDepth ) minDependentDepth = d; } } // make me less deep then all my dependents fDepth = minDependentDepth - 1; } return fDepth; }
fDepth
的值越大,ImageLoader::recursiveUpdateDepth
的調(diào)用在ImageLoader::recursiveLoadLibraries
之后,通過上面的分析過程我們知道:在加載動態(tài)庫時將動態(tài)庫的信息通過setLibImage()
保存,加載完成后dyld根據(jù)依賴關(guān)系遞歸地給各個庫都計算了依賴深度:fDepth
,在初始化的時候遞歸地根據(jù)fDepth
進(jìn)行初始化,每初始化一個image都會使用context.notifySingle()
通知Objc
通過load_images()
調(diào)用load方法。從分析結(jié)果看:不同庫中的load方法的調(diào)用順序可以當(dāng)做是動態(tài)庫的加載順序。(dyld過程比較復(fù)雜,本人水平有限,如有錯誤敬請指出~)