App的啓動通常是指從用戶點擊App開始到AppDelegate
的didFinishLaunching
方法執行完成爲止,通常又將啓動分爲冷啓動和熱啓動。html
上文也說了通常啓動優化主要優化的是冷啓動的過程,熱啓動作的事情也很是少。因此這裏只講解冷啓動過程的優化。冷啓動過程又被分爲main
函數執行以前和main
函數執行以後node
main
函數執行以前DYLD_PRINT_STATISTICS
來查看main
函數執行以前都作了什麼,同時也能夠看出對應消耗的時間 不難發現main
函數執行以前主要作了如下幾種事情
dylib loading time
能夠發現加載時間爲48.41毫秒rebase/binding time
,耗時9.18毫秒
ASLR
安全機制下文中會有講解),因此此時函數、方法的地址就是 隨機分配的數值+偏移地址 這個過程就是偏移修正mach-o
文件,因此此時使用到的靜態庫的方法、函數其實就和自定義的方法、函數差很少了,可以直接獲取到對應的地址,可是動態庫在編譯階段是不會被打包進mach-o
文件的,可是此時又用到了動態庫中的方法,例如用到了NSLog
方法,此時就會生成一個!NSLog
符號此時這個符號會隨機指向一個地址,當運行時,此時動態庫被加載到內存,此時就能夠拿到動態庫對應的方法、函數的地址,因此此時就須要將!NSLog
這個符號綁定到相應的地址上去(dyld
作的),這個過程就叫作符號綁定ObjC setup time
,耗時10.86毫秒initializer time
,耗時110.79毫秒load
方法相應的能夠將load
中的實現放在+initialize()
方法中去,應爲通常一個load
方法的執行須要耗時4毫秒,並且若是類中實現了load
那麼相對應類的加載就要提早到read_image
方法中去執行,若是沒有實現load
類的加載則會方法第一次發送消息的時候加載,main
函數執行以後上文主要是針對特定的階段作一些優化處理,除了刪除的優化方案還有一種優化,就是二進制重排,在講解二進制重排以前先將幾個概念性的東西:ios
從上文的知識中能夠知道,ios程序在加載到虛擬內存的時候會被分紅不少不少頁,若是此時訪問的虛擬地址的一個page,對應的物理地址不存在,則會缺頁異常,此時會阻塞進程將這一頁加載到物理內存而後在訪問。這裏能夠經過instruments
的System Trace
來查看你的項目的缺頁異常的數量以下: 步驟:先點擊啓動->首頁加載完成後暫停->而後找到你的項目找到主線程 發現啓動以前有兩百多個缺頁異常,此時咱們再看項目在編譯時期的默認排列順序,此時咱們寫一個簡單的demo以下圖: 就是寫了幾個簡單的方法,而後項目中選擇Build-setting
搜索link map
而後配置此時會發現對應配置的文件夾中生成了對應的link-map
文件,發現方法、函數等都是按照在文件中的實現順序來的,而文件的順序是按照comple source
中的順序來的如圖: 這種狀況就形成了每一個頁有可能只有一個方法是有用的,其餘方法、函數等都不是在啓動階段調用的,這就形成了在啓動時期缺頁異常的數量會不少,也就形成了啓動時間變長的狀況。這也就是須要進行二進制重排的緣由算法
上文分析了二進制重排的緣由,就是應爲頁中空間的浪費沒有充分利用每一個頁的空間形成缺頁異常數量增多,二進制重排的原理其實就是將啓動階段用到的方法、函數所有排在最前面,這樣就能充分利用每一個頁的空間,與此同時也下降了缺頁異常的數量。以下圖所示:
明顯減小了一大半的缺頁異常的數量swift
經過上面的原理分析能夠知道,若是作二進制重排只須要改變編譯時期方法、函數等的排列順序就行。其本質就是就是對啓動加載的符號進行從新排列。數組
Xcode
是用的連接器叫作ld
,ld
有一個參數叫Order File
, 咱們能夠經過這個參數配置一個 order
文件的路徑 .Build Settings
-> Order File
配置一個後綴爲order
的文件路徑。在這個order
文件中,將所須要的符號按照順序寫在裏面,在項目編譯時,會按照這個文件的順序進行加載,以此來達到咱們的優化,因此二進制重排的關鍵點在於Order File
文件的生成Order File
文件的方法
Order File
文件。hook
objc_msgSend
,可是因爲objc_msgSend
的參數是可變的,須要經過彙編獲取,使用門檻比較高。並且也只能拿到OC和swift中@objc
後的方法Mach-O
特定段和節裏面所存儲的符號以及函數數據Clang
插樁:即批量hook,能夠實現100%符號覆蓋,即徹底獲取swift、OC、C、block
函數Clang
插樁llvm
內置了一個簡單的代碼覆蓋率檢測(SanitizerCoverage
)。它在函數級、基本塊級和邊緣級插入對用戶定義函數的調用,相應文檔SanitizerCoverage
,在build setting
中搜索Other C Flags
,以下圖若是是OC項目則添加-fsanitize-coverage=func,trace-pc-guard
,若是是swift項目則添加-sanitize-coverage=func
和-sanitize=undefined
hook
方法 void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//guard 是一個哨兵,告訴咱們是第幾個被調用的
// 這個地方 是過濾掉了load方法,因此這裏須要註釋掉
if (!*guard) return;
/*
- PC 當前函數返回上一個調用的地址
- 0 當前這個函數地址,即當前函數的返回地址
- 1 當前函數調用者的地址,即上一個函數的返回地址
*/
void *PC = __builtin_return_address(0);
char PcDescr[1024];
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
複製代碼
主要的方法在於__sanitizer_cov_trace_pc_guard
方法,在這裏咱們能夠取到對應方法的地址,爲何方法執行以前會先調用__sanitizer_cov_trace_pc_guard
方法呢,可經過斷點調試查看,在一個方法或者函數的起始處大斷點,再看彙編代碼以下圖: 發如今方法執行以前插入了__sanitizer_cov_trace_pc_guard
方法,全部的函數執行都會限制性__sanitizer_cov_trace_pc_guard
方法,在block前面也打個斷點發現 block
執行前也會被插入__sanitizer_cov_trace_pc_guard
方法,繼續查看swift-oc
混編是swift
方法是否會被hook
也會被hook
,因此也驗證了clang插樁的方法能覆蓋全部方法、函數。hook
方法中咱們知道能夠拿到當前方法或者函數的地址,拿到地址以後咱們能夠經過dladdr
方法去除對應方法或者函數的信息具體代碼以下圖: 發現dli_sname
就是咱們想要的符號,接下來的操做主要就是把這些符號存儲下來而後生成order而後工程再配置對應的Order file
就算完成了。__sanitizer_cov_trace_pc_guard
將函數地址信息存儲下來而後給app
添加一個點擊屏幕的監聽事件,等到首屏加載完畢說明啓動完成全部所須要加載的方法也就加載完成,此時咱們再在這個方法遍歷地址信息,輸出符號。OSQueueHead
建立原子隊列,其目的是保證讀寫安全。OSAtomicEnqueue
方法將node
入隊,經過鏈表的next
指針能夠訪問下一個符號 此刻地址的儲存完成下一步就是讀取寫入order文件:具體代碼以下 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//定義數組
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (YES) {//一次循環!也會被HOOK一次!!
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if (node == NULL) {
break;
}
Dl_info info = {0};
dladdr(node->pc, &info);
// printf("%s \n",info.dli_sname);
NSString * name = @(info.dli_sname);
free(node);
BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
//須要注意若是不是OC方法須要添加下劃線
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
}
//反向數組
NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
//建立一個新數組
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
//去重!
while (name = [enumerator nextObject]) {
if (![funcs containsObject:name]) {//數組中不包含name
[funcs addObject:name];
}
}
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//數組轉成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
//字符串寫入文件
//文件路徑
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tudou.order"];
//文件內容
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
複製代碼
運行完成發現生成了order文件