啟動的過程一般是指從用戶點擊app圖標開始到AppDelegate 的didFinishLaunching方法執(zhí)行完成為止,其中,啟動也分為冷啟動和熱啟動
冷啟動:內存中不包含app相關數據的啟動,一般我們可以通過重啟手機來實現冷啟動
- 1
pre-main階段,即main函數之前,操作系統(tǒng)加載App可執(zhí)行文件到內存,執(zhí)行一系列的加載&鏈接等工作,簡單來說,就是dyld加載過程 - 2:
main函數之后,即從main函數開始,到Appdelegate 的didFinishLaunching方法執(zhí)行完成為止,主要是構建第一個界面,并完成渲染
-熱啟動:是指殺掉app進程后,數據仍然存在時的啟動
其中1,2 兩過程 就是 從用戶點擊App圖標到用戶能看到app主界面的過程,即需要啟動優(yōu)化的部分
pre-main階段的優(yōu)化
統(tǒng)計main函數啟動時間
- 查看系統(tǒng)給的反饋需要增加一個
環(huán)境變量, - 增加路徑:在
Xcode -> Edit Scheme -> Run -> Arguments -> Environment Variables中, - 增加一個環(huán)境變量
DYLD_PRINT_STATISTICS:1。

-dylib loading time(動態(tài)庫耗時):主要是加載動態(tài)庫,系統(tǒng)的動態(tài)庫做過優(yōu)化,耗時較少。蘋果官方推薦最多不要超過6個外部動態(tài)庫,多余6個,需要考慮合并動態(tài)庫,合并動態(tài)庫對于啟動時期的優(yōu)化,非常有效
-rebase/binding time(偏移修正/符號綁定耗時)
-rebase(偏移修正):任何一個app生成的二進制文件,在二進制文件內部所有的方法、函數調用,都有一個地址,這個地址是在當前二進制文件中的偏移地址。一旦在運行時刻(即運行到內存中),每次系統(tǒng)都會隨機分配一個ASLR(Address Space Layout Randomization,地址空間布局隨機化)地址值(是一個安全機制,會分配一個隨機的數值,插入在二進制文件的開頭),例如,二進制文件中有一個 test方法,偏移值是0x0001,而隨機分配的ASLR是0x1f00,如果想訪問test方法,其內存地址(即真實地址)變?yōu)?ASLR+偏移值 = 運行時確定的內存地址(即0x1f00+0x0001 = 0x1f01)
-binding(綁定):,例如NSLog方法,在編譯時期生成的mach-o文件中,會創(chuàng)建一個符號!NSLog(目前指向一個隨機的地址),然后在運行時(從磁盤加載到內存中,是一個鏡像文件),會將真正的地址給符號(即在內存中將地址與符號進行綁定,是dyld做的,也稱為動態(tài)庫符號綁定),一句話概括:綁定就是給符號賦值的過程
-
Objc setup time:注冊所有OC類耗時,類越多耗時越多,有人統(tǒng)計過2萬個自定義的OC的類,大概耗時800毫秒。刪除不用的類,可以減少耗時 -
initializer time:load方法 和C++構造函數的耗時.減少重寫load方法,盡量將事情延遲到 main 方法以后,可以減少耗時。
優(yōu)化建議
- 減少
外部動態(tài)庫的數量
-減少OC類,因為OC類越多,越耗時
-將不必須在+load方法中做的事情延遲到+initialize中,盡量不要用C++虛函數 - 啟動階段能使用
多線程來初始化的,就使用多線程 - 使用
純代碼。不用xib storyboard(要額外進行代碼解析轉換和頁面的渲染)
二進制重排
原理
當進程訪問一個虛擬內存page,而對應的物理內存不存在時,會觸發(fā)缺頁中斷(Page Fault),因此阻塞進程。此時就需要先加載數據到物理內存,然后再繼續(xù)訪問
App在冷啟動過程中,會有大量的類、分類、三方等需要加載和執(zhí)行,此刻需要調用的方法,處于不同的Page導致的此時的產生的Page Fault所帶來的的耗時是很大的
將所有啟動時刻需要調用的方法,排列在一起,即放在一個頁中,這樣就從多個Page Fault變成了一個Page Fault,優(yōu)化了啟動時間
監(jiān)測
查看一下項目的缺頁異常數量。注意需要卸載 APP 或者重啟手機,來保證這個APP完全沒有被加載到內存中
打開Instrument -> System Trace

- 點擊啟動,第一個界面出來后,停掉,
!Page Faul](https://upload-images.jianshu.io/upload_images/9660710-21b0a88923b7e8a7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
從圖中可以看出發(fā)生的PageFault有427次

優(yōu)化后項目的缺頁遺產數量是286, 減少了啟動時大概40%的缺頁異常~
通過linkMap也可以查看啟動方法的順序
-
在
Build Setting -> Write Link Map File設置為YES
設置 -
編譯,然后在對應的路徑下查找
link map文件,如下所示,可以發(fā)現 類中函數的加載順序是從上到下的,而文件的順序是根據Build Phases -> Compile Sources中的順序加載的
加載順序
二進制重排的方法
1.方法的重排序
xcode已經為我們提供了這個機制,它使用的鏈接器叫做ld, ld有一個參數叫做Order File, 我們可以通過配置order文件,來使編譯時生成的二進制的文件的Link Map種的符號順序,按照我們指定的順序排列生成。而且libobjc 實際上也做了二進制重排。

【第一步】在項目根目錄下建一個xxx.order的文件,里面寫上按照自己想排列的順序,寫上方法或者函數的名字。(如果寫了一個不存在的符號,也不會報錯,會被自動過濾掉~)
- 內容).png
【第三步】重新編譯,查看 Link Map文件的順序,果然,按照我們指定的順序排列啦!

找到啟動時需要方法 -> 靜態(tài)插樁
接下來,需要做的就是寫入 order 文件里的符號了,我們不可能手寫上所有的啟動時需要的執(zhí)行的符號,這里的所有符號包括,調用的方法、函數、C++構造方法、swift方法、block。
這里使用 LLVM 內置的簡單代碼覆蓋率檢測工具 [圖片上傳失敗...(image-b6c749-1626686913498)]。它在邊緣、函數、基本塊``級別`上插入對用戶定義函數的調用。
edge(默認):檢測邊緣(所有的指令跳轉都會被插入對用戶定義函數的調用, 如循環(huán)、分支判斷、方法函數等)。
bb:檢測基本塊。func:僅將檢測每個功能的輸入塊(這個就是我們要重排序的符號)。
按照文檔,
【第1步】搜索并設置
Other C Flags/ Other C++ Flags為-fsanitize-coverage=func,trace-pc-guard(這里要用func, 不能用默認的edge, 不然會造成死循環(huán))。-
如果有swift ,需要設置
Other Swift Flags設置為-sanitize-coverage=func -sanitize=undefined
設置 【第2步】編譯器將插入對
模塊構造函數的調用,所以我們要實現這個方法:
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);
- 參數1 start 是一個指針,指向無符號int類型,4個字節(jié),相當于一個數組的起始位置,即符號的起始位置(是從高位往低位讀)
- 參數2 stop,由于數據的地址是往下讀的(即從高往低讀,所以此時獲取的地址并不是stop真正的地址,而是標記的最后的地址,讀取stop時,由于stop占4個字節(jié),stop真實地址 = stop打印的地址-0x4)
- 【第3步】
__sanitizer_cov_trace_pc_guard實現: 主要是捕獲所有的啟動時刻的符號,將所有符號入隊
//原子隊列,其目的是保證寫入安全,線程安全
static OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
//定義符號結構體,以鏈表的形式
typedef struct {
void *pc;
void *next;
}XXNode;
/*
- start:起始位置
- stop:并不是最后一個符號的地址,而是整個符號表的最后一個地址,最后一個符號的地址=stop-4(因為是從高地址往低地址讀取的,且stop是一個無符號int類型,占4個字節(jié))。stop存儲的值是符號的
*/
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N;
if (start == stop || *start) return;
printf("INIT: %p - %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++) {
*x = ++N;
}
}
/*
可以全面hook方法、函數、以及block調用,用于捕捉符號,是在多線程進行的,這個方法中只存儲pc,以鏈表的形式
- guard 是一個哨兵,告訴我們是第幾個被調用的
*/
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return;//將load方法過濾掉了,所以需要注釋掉
//獲取PC
/*
- PC 當前函數返回上一個調用的地址
- 0 當前這個函數地址,即當前函數的返回地址
- 1 當前函數調用者的地址,即上一個函數的返回地址
*/
void *PC = __builtin_return_address(0);
//創(chuàng)建node,并賦值
XXNode *node = malloc(sizeof(XXNode));
*node = (XXNode){PC, NULL};
//加入隊列
//符號的訪問不是通過下標訪問,是通過鏈表的next指針,所以需要借用offsetof(結構體類型,下一個的地址即next)
OSAtomicEnqueue(&queue, node, offsetof(XXNode, next));
}
-【第四步:獲取所有符號并寫入文件】
extern void getOrderFile(void(^completion)(NSString *orderFilePath)){
collectFinished = YES;
__sync_synchronize();
NSString *functionExclude = [NSString stringWithFormat:@"_%s", __FUNCTION__];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//創(chuàng)建符號數組
NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
//while循環(huán)取符號
while (YES) {
//出隊
CJLNode *node = OSAtomicDequeue(&queue, offsetof(CJLNode, next));
if (node == NULL) break;
//取出PC,存入info
Dl_info info;
dladdr(node->pc, &info);
// printf("%s \n", info.dli_sname);
if (info.dli_sname) {
//判斷是不是OC方法,如果不是,需要加下劃線存儲,反之,則直接存儲
NSString *name = @(info.dli_sname);
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
}
}
if (symbolNames.count == 0) {
if (completion) {
completion(nil);
}
return;
}
//取反(隊列的存儲是反序的)
NSEnumerator *emt = [symbolNames reverseObjectEnumerator];
//去重
NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *name;
while (name = [emt nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//去掉自己
[funcs removeObject:functionExclude];
//將數組變成字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
NSLog(@"Order:\n%@", funcStr);
//字符串寫入文件
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cjl.order"];
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
if (completion) {
completion(success ? filePath : nil);
}
});
}
- 第五步:在
didFinishLaunchingWithOptions方法最后調用】需要注意的是,這里的調用位置是由你決定的,一般來說,是第一個渲染的界面
- 【第六步:
拷貝文件,放入指定位置,并配置路徑】一般將該文件放入主項目路徑下,并在Build Settings -> Order File中配置./XX.order,下面是配置前后的對比(上邊是配置前的熟悉怒,下邊是配置后符號順序的)





