熱啟動(dòng)與冷啟動(dòng)
-
冷啟動(dòng):App點(diǎn)擊啟動(dòng)前,此時(shí)App的進(jìn)程還不在系統(tǒng)里,內(nèi)存中不包含app相關(guān)數(shù)據(jù),需要系統(tǒng)新創(chuàng)建一個(gè)進(jìn)程分配給App。 -
熱啟動(dòng):App在冷啟動(dòng)后用戶將App退回后臺(tái),此時(shí)App的進(jìn)程還在系統(tǒng)里,數(shù)據(jù)仍然存在,用戶重新返回App的過程。
APP冷啟動(dòng)完整流程
冷啟動(dòng)的整個(gè)過程是指從用戶喚起 App開始到 AppDelegate 中的 didFinishLaunchingWithOptions 方法執(zhí)行完畢為止,并以執(zhí)行main()函數(shù)的時(shí)機(jī)為分界點(diǎn),分為pre-main和 main()兩個(gè)階段。
pre-main階段
pre-main階段,即main函數(shù)之前,操作系統(tǒng)加載App可執(zhí)行文件到內(nèi)存,執(zhí)行一系列的加載&鏈接等工作,簡(jiǎn)單來說,就是dyld加載過程

-
dylib loading time(動(dòng)態(tài)庫(kù)耗時(shí)):主要是加載動(dòng)態(tài)庫(kù) -
rebase/binding time(偏移修正/符號(hào)綁定耗時(shí)):進(jìn)行rebase指針調(diào)整和bind符號(hào)綁定-
rebase:任何一個(gè)app生成的二進(jìn)制文件,所有的方法和函數(shù)調(diào)用都對(duì)應(yīng)一個(gè)地址,這個(gè)地址就是當(dāng)前二進(jìn)制文件中的偏移地址,為了安全,通常會(huì)隨機(jī)分配一個(gè)ASLR隨機(jī)值,只有ASLR+偏移地址才是方法或函數(shù)對(duì)內(nèi)存的真實(shí)地址 -
binding:將編譯期創(chuàng)建的符號(hào)在運(yùn)行時(shí)與地址進(jìn)行綁定,一般是dyld執(zhí)行,也可以叫動(dòng)態(tài)庫(kù)符號(hào)綁定
*ObjC setup time(OC類注冊(cè)的耗時(shí)):ObjC相關(guān)Class、category注冊(cè)、selector唯一性檢查等,OC類越多,時(shí)間越長(zhǎng)
*initializer time(執(zhí)行l(wèi)oad和構(gòu)造函數(shù)的耗時(shí)):+load()方法、attribute修飾的函數(shù)調(diào)用、創(chuàng)建C++靜態(tài)全局變量等
-
pre-main優(yōu)化方案
- 減少外部動(dòng)態(tài)庫(kù)加載,官方建議自定義的動(dòng)態(tài)庫(kù)
不要超過6個(gè),如果超過則需要合并動(dòng)態(tài)庫(kù) - 減少OC類
- 將不必須在
+load方法中做的事情延遲到+initialize中,盡量不要用C++虛函數(shù)
main函數(shù)
main函數(shù)執(zhí)行后的階段,指的是:從 main函數(shù)執(zhí)行開始,到appDelegate 的 didFinishLaunchingWithOptions方法里首屏渲染相關(guān)方法執(zhí)行完成。
即,從main函數(shù)執(zhí)行到設(shè)置self.window.rootViewController執(zhí)行完成的階段。
在didFinishLaunching中的業(yè)務(wù)主要分為三個(gè)類型
- 【第一類】初始化第三方sdk
- 【第二類】app運(yùn)行環(huán)境配置
- 【第三類】自己工具類的初始化等
main函數(shù)階段優(yōu)化方案
- 盡量使用純代碼來進(jìn)行UI框架的搭建,尤其是主UI框架,例如UITabBarController。盡量避免使用Xib或者SB,相比純代碼而言,這種更耗時(shí)
- 將耗時(shí)操作滯后或異步處理。
通常的耗時(shí)操作有:網(wǎng)絡(luò)加載、編輯、存儲(chǔ)圖片和文件等資源,不要占用主線程時(shí)間 - main函數(shù)執(zhí)行后到首屏渲染完成前,只處理首屏渲染相關(guān)業(yè)務(wù)。
首屏渲染外的其他功能放到首屏渲染完成后去初始化 - 減少啟動(dòng)初始化的流程,能懶加載的懶加載,能延遲的延遲,能放后臺(tái)初始化的放后臺(tái),盡量不要占用主線程的啟動(dòng)時(shí)間
- 啟動(dòng)階段能使用多線程來初始化的,就使用多線程
- 刪除廢棄類、方法
二進(jìn)制重排
對(duì)用戶而言,使用App時(shí)第一個(gè)直接體驗(yàn)就是啟動(dòng) App 時(shí)間,而啟動(dòng)時(shí)期會(huì)有大量的類、分類、三方等等需要加載和執(zhí)行,此時(shí)多個(gè) Page Fault 所產(chǎn)生的的耗時(shí)往往是不能小覷的,下面我們就通過二進(jìn)制重排來優(yōu)化啟動(dòng)耗時(shí)。
虛擬內(nèi)存
在
進(jìn)程和物理內(nèi)存之間增加一個(gè)中間層,這個(gè)中間層就是所謂的虛擬內(nèi)存,主要用于解決當(dāng)多個(gè)進(jìn)程同時(shí)存在時(shí),對(duì)物理內(nèi)存的管理。提高了CPU的利用率,使多個(gè)進(jìn)程可以同時(shí)、按需加載。所以虛擬內(nèi)存其本質(zhì)就是一張?zhí)摂M地址和物理地址對(duì)應(yīng)關(guān)系的映射表每個(gè)進(jìn)程都有一個(gè)獨(dú)立的虛擬內(nèi)存,其地址都是從0開始,大小是4G固定的,每個(gè)虛擬內(nèi)存又會(huì)劃分為一個(gè)一個(gè)的頁(yè)(頁(yè)的大小在iOS中是16K,其他的是4K),每次加載都是以頁(yè)為單位加載的,進(jìn)程間是無法互相訪問的,保證了進(jìn)程間數(shù)據(jù)的安全性。一個(gè)進(jìn)程中,只有部分功能是活躍的,所以只需要將進(jìn)程中活躍的部分放入物理內(nèi)存,避免物理內(nèi)存的浪費(fèi)
當(dāng)CPU需要訪問數(shù)據(jù)時(shí),首先是訪問虛擬內(nèi)存,然后通過虛擬內(nèi)存去尋址,即可以理解為在表中找對(duì)應(yīng)的物理地址,然后對(duì)相應(yīng)的物理地址進(jìn)行訪問
如果在訪問時(shí),虛擬地址的內(nèi)容
未加載到物理內(nèi)存,會(huì)發(fā)生缺頁(yè)異常(pagefault),將當(dāng)前進(jìn)程阻塞掉,此時(shí)需要先將數(shù)據(jù)載入到物理內(nèi)存,然后再尋址,進(jìn)行讀取。這樣就避免了內(nèi)存浪費(fèi)
page Fault 調(diào)試
打開Instruments,選擇 System Trace
選擇真機(jī),選擇工程,選擇啟動(dòng)(啟動(dòng)前最好重啟手機(jī),清除緩存數(shù)據(jù),確保是冷啟動(dòng)狀態(tài)),當(dāng)頁(yè)面加載出來的時(shí)候,停止
二進(jìn)制重排實(shí)踐
二進(jìn)制重排就是對(duì)即將生成的可執(zhí)行文件重新排列,即它發(fā)生在鏈接階段。
首先理解幾個(gè)名詞
- Link Map:
Linkmap是iOS編譯過程的中間產(chǎn)物,記錄了二進(jìn)制文件的布局,需要在Xcode的Build Settings里開啟Write Link Map File,Link Map主要包含三部分:
*Object Files生成二進(jìn)制用到的link單元的路徑和文件編號(hào)-
Sections記錄Mach-O每個(gè)Segment/section的地址范圍 -
Symbols按順序記錄每個(gè)符號(hào)的地址范圍
-
- ld :
ld有一個(gè)參數(shù)叫Order File,通過Build Settings -> Order File配置一個(gè) 后綴名 為order的文件路徑。在這個(gè)order文件中,將你需要的符號(hào)按順序?qū)懺诶锩?。?dāng)工程build 的時(shí)候,Xcode會(huì)讀取這個(gè)文件,打的二進(jìn)制包就會(huì)按照這個(gè)文件中的符號(hào)順序進(jìn)行生成對(duì)應(yīng)的mach-O。
如何獲取啟動(dòng)運(yùn)行的函數(shù)呢
hook objc_msgSend:我們知道,函數(shù)的本質(zhì)是發(fā)送消息,在底層都會(huì)來到objc_msgSend,但是由于objc_msgSend的參數(shù)是可變的,需要通過匯編獲取,對(duì)開發(fā)人員要求較高。而且也只能拿到OC 和 swift中@objc后的方法靜態(tài)掃描:掃描
Mach-O特定段和節(jié)里面所存儲(chǔ)的符號(hào)以及函數(shù)數(shù)據(jù)Clang插樁:即批量hook,可以實(shí)現(xiàn)100%符號(hào)覆蓋,即完全獲取swift、OC、C、block函數(shù)
Clang插樁
llvm內(nèi)置了一個(gè)簡(jiǎn)單的代碼覆蓋率檢測(cè)(SanitizerCoverage)。它在函數(shù)級(jí)、基本塊級(jí)和邊緣級(jí)插入對(duì)用戶定義函數(shù)的調(diào)用。我們這里的批量hook,就需要借助于SanitizerCoverage。流程如下:
- 開啟
SanitizerCoverage-
OC項(xiàng)目,需要在:在Build Settings里的“Other C Flags”中添加 -fsanitize-coverage=func,trace-pc-guard - 如果是
Swift項(xiàng)目,還需要額外在“Other Swift Flags”中加入-sanitize-coverage=func和-sanitize=undefined
-
- 重寫方法
-
__sanitizer_cov_trace_pc_guard_init方法- 參數(shù)1
start是一個(gè)指針,指向無符號(hào)int類型,4個(gè)字節(jié),相當(dāng)于一個(gè)數(shù)組的起始位置,即符號(hào)的起始位置(是從高位往低位讀) - 參數(shù)2
stop,由于數(shù)據(jù)的地址是往下讀的(即從高往低讀,所以此時(shí)獲取的地址并不是stop真正的地址,而是標(biāo)記的最后的地址,讀取stop時(shí),由于stop占4個(gè)字節(jié),stop真實(shí)地址 =stop打印的地址-0x4)
*__sanitizer_cov_trace_pc_guard方法
- 參數(shù)1
-
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
//定義原子隊(duì)列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號(hào)結(jié)構(gòu)體
typedef struct{
void *pc;
void *next;
} SYNode;
/*
- start:起始位置
- stop:并不是最后一個(gè)符號(hào)的地址,而是整個(gè)符號(hào)表的最后一個(gè)地址,最后一個(gè)符號(hào)的地址=stop-4(因?yàn)槭菑母叩刂吠偷刂纷x取的,且stop是一個(gè)無符號(hào)int類型,占4個(gè)字節(jié))。stop存儲(chǔ)的值是符號(hào)的
*/
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.
}
/*
可以全面hook方法、函數(shù)、以及block調(diào)用,用于捕捉符號(hào),是在多線程進(jìn)行的,這個(gè)方法中只存儲(chǔ)pc,以鏈表的形式
- guard 是一個(gè)哨兵,告訴我們是第幾個(gè)被調(diào)用的
*/
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
//獲取PC
/*
- PC 當(dāng)前函數(shù)返回上一個(gè)調(diào)用的地址
- 0 當(dāng)前這個(gè)函數(shù)地址,即當(dāng)前函數(shù)的返回地址
- 1 當(dāng)前函數(shù)調(diào)用者的地址,即上一個(gè)函數(shù)的返回地址
*/
void *PC = __builtin_return_address(0);
char PcDescr[1024];
//__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
//創(chuàng)建結(jié)構(gòu)體!
SYNode * node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//加入結(jié)構(gòu)!
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
- 獲取所有符號(hào)并寫入文件
{
//定義數(shù)組
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (YES) {//一次循環(huán)!也會(huì)被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:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
//是否去重??
[symbolNames addObject:symbolName];
/*
if ([name hasPrefix:@"+["]||[name hasPrefix:@"-["]) {
//如果是OC方法名稱直接存!
[symbolNames addObject:name];
continue;
}
//如果不是OC直接加個(gè)_存!
[symbolNames addObject:[@"_" stringByAppendingString:name]];
*/
}
//反向數(shù)組
// symbolNames = (NSMutableArray<NSString *>*)[[symbolNames reverseObjectEnumerator] allObjects];
NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
//創(chuàng)建一個(gè)新數(shù)組
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
//去重!
while (name = [enumerator nextObject]) {
if (![funcs containsObject:name]) {//數(shù)組中不包含name
[funcs addObject:name];
}
}
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//數(shù)組轉(zhuǎn)成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
//字符串寫入文件
//文件路徑
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
//文件內(nèi)容
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
拷貝文件,放入指定位置,并配置路徑
一般將該文件放入主項(xiàng)目路徑下,并在Build Settings -> Order File中配置./hank.order注:
Build Settings -> Other C Flags的如果配置的是-fsanitize-coverage=trace-pc-guard,沒有func,在while循環(huán)部分會(huì)出現(xiàn)死循環(huán),原因是while循環(huán)也會(huì)被__sanitizer_cov_trace_pc_guard中的guard哨兵檢測(cè)到,通過匯編可以查看到流程-
第一次是
bl是touchBegin
2251862-3ab80a2744e85668.jpg -
第二次
bl是因?yàn)?code>while 循環(huán)。 即只要是跳轉(zhuǎn),就會(huì)被hook,即有bl、b的指令,就會(huì)被hook
2251862-b09a285f136e195d.jpg

