在 WWDC 2016 和 2017 都有提到啓動這塊的原理和性能優化思路,可見啓動時間,對於開發者和用戶們來講是多麼的重要,本文就談談如何精確的度量 App 的啓動時間,啓動時間由 main 以前的啓動時間和 main 以後的啓動時間兩部分組成。git
圖是 Apple 在 WWDC 上展現的 PPT,是對 main 以前啓動所作事的一個簡單總結。main 以後的啓動時間如何考量呢?這個更多靠你們本身定義,有的人把 main 到 didFinishLaunching 結束的這一段時間做爲指標,有的人把 main 到第一個 ViewController 的 viewDidAppear 做爲考量指標。無論如何,我以爲都是必定程度上能夠反映問題的。github
對於如何測試啓動時間,Xcode 提供了一個很讚的方法,只須要在 Edit scheme -> Run -> Arguments 中將環境變量 DYLD_PRINT_STATISTICS 設爲 1,就能夠看到 main 以前各個階段的時間消耗。swift
Total pre-main time: 341.32 milliseconds (100.0%)
dylib loading time: 154.88 milliseconds (45.3%)
rebase/binding time: 37.20 milliseconds (10.8%)
ObjC setup time: 52.62 milliseconds (15.4%)
initializer time: 96.50 milliseconds (28.2%)
slowest intializers :
libSystem.dylib : 4.07 milliseconds (1.1%)
libMainThreadChecker.dylib : 30.75 milliseconds (9.0%)
AFNetworking : 19.08 milliseconds (5.5%)
LDXLog : 10.06 milliseconds (2.9%)
Bigger : 7.05 milliseconds (2.0%)複製代碼
還有一個方法獲取更詳細的時間,只需將環境變量 DYLD_PRINT_STATISTICS_DETAILS 設爲 1 就能夠。數組
total time: 1.0 seconds (100.0%)
total images loaded: 243 (0 from dyld shared cache)
total segments mapped: 721, into 93608 pages with 6173 pages pre-fetched
total images loading time: 817.51 milliseconds (78.3%)
total load time in ObjC: 63.02 milliseconds (6.0%)
total debugger pause time: 683.67 milliseconds (65.5%)
total dtrace DOF registration time: 0.07 milliseconds (0.0%)
total rebase fixups: 2,131,938
total rebase fixups time: 37.54 milliseconds (3.5%)
total binding fixups: 243,422
total binding fixups time: 29.60 milliseconds (2.8%)
total weak binding fixups time: 1.75 milliseconds (0.1%)
total redo shared cached bindings time: 29.32 milliseconds (2.8%)
total bindings lazily fixed up: 0 of 0
total time in initializers and ObjC +load: 93.76 milliseconds (8.9%)
libSystem.dylib : 2.58 milliseconds (0.2%)
libBacktraceRecording.dylib : 3.06 milliseconds (0.2%)
CoreFoundation : 1.85 milliseconds (0.1%)
Foundation : 2.61 milliseconds (0.2%)
libMainThreadChecker.dylib : 42.73 milliseconds (4.0%)
ModelIO : 1.93 milliseconds (0.1%)
AFNetworking : 18.76 milliseconds (1.7%)
LDXLog : 9.46 milliseconds (0.9%)
libswiftCore.dylib : 1.16 milliseconds (0.1%)
libswiftCoreImage.dylib : 1.51 milliseconds (0.1%)
Bigger : 3.91 milliseconds (0.3%)
Reachability : 1.48 milliseconds (0.1%)
ReactiveCocoa : 1.56 milliseconds (0.1%)
SDWebImage : 1.41 milliseconds (0.1%)
SVProgressHUD : 1.23 milliseconds (0.1%)
total symbol trie searches: 133246
total symbol table binary searches: 0
total images defining weak symbols: 30
total images using weak symbols: 69複製代碼
若是不依靠 Xcode 咱們也是能夠對 main 以前的時間進行一個考量的。固然,這個時間的度量更多關注的是開發者可控的啓動段。也就是第一個圖展現的 Initializer 段,在這段時間裏處理 C++ 靜態對象的 initializer、ObjC Load 方法的執行。xcode
如何計算這一段時間呢?最容易想到的就是攔截打點,如何攔截成爲難點。這裏把目光轉向 dyld 源碼,看看有什麼發現。整個初始化過程都是從 initializeMainExecutable 方法開始的。dyld 會優先初始化動態庫,而後初始化 App 的可執行文件。緩存
void initializeMainExecutable()
{
// record that we've reached this step gLinkContext.startedInitializingMainExecutable = true; // run initialzers for any inserted dylibs ImageLoader::InitializerTimingList initializerTimes[allImagesCount()]; initializerTimes[0].count = 0; const size_t rootCount = sImageRoots.size(); if ( rootCount > 1 ) { for(size_t i=1; i < rootCount; ++i) { sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]); } } // run initializers for main executable and everything it brings up sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);複製代碼
那麼不難想到,只要在動態庫的 load 函數中 Hook App 中全部的 Load 函數,而後打點就能夠啦。可是,如今不少項目庫都是使用 Cocoapods 管理的,而且不少都使用了 use_frameworks,那麼也就是說咱們的 App 並非一個 單一的可執行文件,它是有主 image 文件和不少動態庫共同組成的。按照剛纔那種方法,是沒辦法統計到本身引入的動態庫的 load 函數的執行時間的。下一步要考慮的就是,如何找到最先加載的動態庫呢?而後在其 load 函數中作 Hook 就能夠。性能優化
動態庫的 load 順序是與 Load Commands 順序和依賴關係息息相關的。如圖所示:bash
就拿咱們引入的動態庫來講, AFNetworking 會優先 load ,被依賴的動態庫會優先 load。下面是我本身打點測試的結果,LDXlog 被 Bigger 依賴,因此 AFNetworking 最先 load ,而後是 LDXlog,依次按照 Load Commands 順序加載。app
2017-09-23 13:45:01.683817+0800 AAALoadHook[27267:1585198] AFNetworking
2017-09-23 13:45:01.696816+0800 AAALoadHook[27267:1585198] LDXLog
2017-09-23 13:45:01.707312+0800 AAALoadHook[27267:1585198] Bigger
2017-09-23 13:45:01.708875+0800 AAALoadHook[27267:1585198] Reachability
2017-09-23 13:45:01.710732+0800 AAALoadHook[27267:1585198] REACtive
2017-09-23 13:45:01.712066+0800 AAALoadHook[27267:1585198] SDWE
2017-09-23 13:45:01.713650+0800 AAALoadHook[27267:1585198] SVProgressHUD
2017-09-23 13:45:01.714499+0800 AAALoadHook[27267:1585198] 我是主工程複製代碼
上面的測試讓我產生一個錯覺,覺得動態庫加載是和字母順序相關的,其實並非這樣,由於我使用的都是 pod 管理的動態庫,這個順序被 CocoaPods 排序過了,因此纔會有如此結果。在此感謝@冬瓜@monkey的乾貨解答。異步
也就是說,只要把咱們的統計庫命名爲 A 開頭的庫(咱們的庫目前均使用 pod 管理),並在內部加入打點就能夠啦。再次總結下總體的思路:
因爲代碼比較多,粘貼過來的話博客太長了,因此想了解源碼的話,能夠點擊這個連接:github.com/joy0304/Joy…
剛纔的統計還有一些要注意的事項,就是不能爲了統計性能,本身卻形成了性能問題,獲取全部的類而且 Hook load 函數仍是比較耗時的,控制很差反而增長了啓動時間。
剛纔提到了初始化的入口是 initializeMainExecutable,該函數會執行 ImageLoader::runInitializers 方法,而後會調用 ImageLoader::doInitialization,最後會執行到 doModInitFunctions 方法。
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
if ( fHasInitializers ) {
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) {
if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = §ionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
const uint8_t type = sect->flags & SECTION_TYPE;
if ( type == S_MOD_INIT_FUNC_POINTERS ) {
Initializer* inits = (Initializer*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uintptr_t);
for (size_t j=0; j < count; ++j) {
Initializer func = inits[j];
// <rdar://problem/8543820&9228031> verify initializers are in image
if ( ! this->containsAddress((void*)func) ) {
dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
}
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
}
}
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}複製代碼
這段代碼實在是長,它會從 mod_init_func 這個 section 中讀取全部的函數指針,而後執行函數調用,這些函數指針對應的正是咱們的 C++ Static Initializers 和 __attribute__((constructor))
修飾的函數。
由於它們的執行順序在 load 函數以後,因此能夠在 load 函數中把 mod_init_func 中的地址都替換成咱們的 hook 函數指針,而後再把原函數指針保存到一個全局數據中,當執行咱們的 hook 函數時,從全局數組中取出原函數地址執行。在這裏張貼下主要代碼,更多能夠參考這個連接:github.com/everettjf/Y…
void myInitFunc_Initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct MyProgramVars* vars){
++g_cur_index;
OriginalInitializer func = (OriginalInitializer)g_initializer->at(g_cur_index);
CFTimeInterval start = CFAbsoluteTimeGetCurrent();
func(argc,argv,envp,apple,vars);
CFTimeInterval end = CFAbsoluteTimeGetCurrent();
}
static void hookModInitFunc(){
Dl_info info;
dladdr((const void *)hookModInitFunc, &info);
#ifndef __LP64__
const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;
unsigned long size = 0;
MemoryType *memory = (uint32_t*)getsectiondata(mhp, "__DATA", "__mod_init_func", & size);
#else
const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;
unsigned long size = 0;
MemoryType *memory = (uint64_t*)getsectiondata(mhp, "__DATA", "__mod_init_func", & size);
#endif
for(int idx = 0; idx < size/sizeof(void*); ++idx){
MemoryType original_ptr = memory[idx];
g_initializer->push_back(original_ptr);
memory[idx] = (MemoryType)myInitFunc_Initializer;
}
}複製代碼
剛纔 hook load 函數時遇到的問題,對於 C++ Static Initializers 會不會存在呢?是存在的,我想要在一個動態庫中統計 App 中全部可執行文件的 C++ Static Initializers 的執行時間,可是 dyld 中有這麼一段代碼:
if ( type == S_MOD_INIT_FUNC_POINTERS ) {
Initializer* inits = (Initializer*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uintptr_t);
for (size_t j=0; j < count; ++j) {
Initializer func = inits[j];
// <rdar://problem/8543820&9228031> verify initializers are in image
if ( ! this->containsAddress((void*)func) ) {
dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
}
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
}複製代碼
if ( ! this->containsAddress((void*)func) ) 這裏會作一個判斷,判斷函數地址是否在當前 image 的地址空間中,由於咱們是在一個獨立的動態庫中作函數地址替換,替換後的函數地址都是咱們動態庫中的,並無在其餘 image 中,因此當其餘 image 執行到這個判斷時,就拋出了異常。這個問題好像無解,因此咱們的 C++ Static Initializers 時間統計稍有不足。
Apple 在 developer.apple.com/videos/play… 中公佈了一個新的追蹤 Static Initializers 時間消耗的方案, Instruments 增長了一個叫作 Static Initializer Tracing 的工具,能夠方便排查每一個 Static Initializer 的時間消耗。(我還沒更新最新版本,暫不實踐)
main 到 didFinishLaunching 結束或者第一個 ViewController 的viewDidAppear 都是做爲 main 以後啓動時間的一個度量指標。這個時間統計直接打點計算就能夠,不過當遇到時間較長鬚要排查問題時,只統計兩個點的時間其實不方便排查,目前見到比較好用的方式就是爲把啓動任務規範化、粒子化,針對每一個任務都有打點統計,這樣方便後期問題的定位和優化。
其實優化的,不少公司都有博客寫道。既然談到了啓動監控就稍微寫一點我的以爲比較使用的優化方案吧。