原創(chuàng):知識點總結(jié)性文章
創(chuàng)作不易,請珍惜瘾晃,之后會持續(xù)更新贷痪,不斷完善
個人比較喜歡做筆記和寫總結(jié),畢竟好記性不如爛筆頭哈哈蹦误,這些文章記錄了我的IOS成長歷程劫拢,希望能與大家一起進步
溫馨提示:由于簡書不支持目錄跳轉(zhuǎn),大家可通過command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容
目錄
- 一强胰、檢測啟動時間
- 二舱沧、為什么需要二進制重排
- 三、二進制重排原理
- 四哪廓、調(diào)試 Page Fault
- 五狗唉、order文件
- 六、靜態(tài)插樁代碼覆蓋工具的機制和原理
- 七涡真、寫入order文件
- 八分俯、驗證插樁前后差距
- 九、全文總結(jié)
- 十哆料、摒棄解釋缸剪,直接使用
一、檢測啟動時間
冷啟動:指APP被后臺kill
后重新啟動APP东亦,這種啟動方式叫做冷啟動杏节。
熱啟動:APP的狀態(tài)由running
切換為suspend
,APP 沒有被kill仍然在后臺運行典阵。再次把APP切換到前臺奋渔,這種啟動方式叫熱啟動。
啟動時間的劃分可以把main()
函數(shù)作為關(guān)鍵點分割成兩塊壮啊。t1階段蚣常,main()
之前的處理所需時間世吨,稱為pre-main
。t2階段,main()
及main()
之后處理所需時間俱尼。t2階段耗時的主要是業(yè)務(wù)代碼推薦 BLStopwatch抖韩,這個工具可以打點統(tǒng)計業(yè)務(wù)耗時 本部分優(yōu)化根據(jù)各自業(yè)務(wù)需求自行處理缤弦。
通過添加環(huán)境變量可以獲取到pre-main
階段的時間谢鹊。Xcode 中提供了測量 pre-main
的時間 檢測。Edit scheme -> Run -> Auguments
添加環(huán)境變量 DYLD_PRINT_STATISTICS
拓萌,value
設(shè)為YES岁钓。
啟動以后可以看到啟動時長:
加載dylib
分析每個dylib
(大部分是系統(tǒng)的),找到其Mach-O
文件,打開并讀取驗證有效性甜紫;找到代碼簽名注冊到內(nèi)核降宅,最后對dylib
的每個segment
調(diào)用mmap()
。
rebase/bind
dylib
加載完成之后囚霸,它們處于相互獨立的狀態(tài),需要綁定起來激才。Rebase
將鏡像讀入內(nèi)存拓型,修正鏡像內(nèi)部的指針,性能消耗主要在IO瘸恼。Bind
是查詢符號表劣挫,設(shè)置指向鏡像外部的指針,性能消耗主要在CPU計算东帅。
Objc setup
runtime
會維護一張類名與類的方法列表的全局表压固。讀取所有類,將類對象其注冊到這個全局表中(class registration
)靠闭。讀取所有分類帐我,把分類加載到類對象中(category registration
)。檢查selector
的唯一性(selector uniquing
)愧膀。
initalizer time
這部分其實就是load
方法的耗時拦键。
優(yōu)化思路
移除不需要用到的動態(tài)庫,盡量使用系統(tǒng)庫檩淋,且蘋果建議數(shù)量控制在 6 個以下
移除不需要用到的類芬为;合并功能類似的類和擴展;經(jīng)測試 20000 個類會增加約 800毫秒
盡量進行懶加載蟀悦,盡量避免在load()
方法里執(zhí)行操作媚朦,把操作推遲到initialize()
方法
二、為什么需要二進制重排
當(dāng)我們向操作系統(tǒng)申請內(nèi)存時日戈,操作系統(tǒng)并不是直接分配給我們物理內(nèi)存询张,而是只標(biāo)記當(dāng)前進程擁有該段內(nèi)存,當(dāng)真正使用這段內(nèi)存時才會分配涎拉。這種延遲分配物理內(nèi)存的方式就通過 page fault 機制來實現(xiàn)的瑞侮。
當(dāng)我們訪問一個內(nèi)存地址時,如果該地址非法鼓拧,或者我們對其沒有訪問權(quán)限半火,或者該地址對應(yīng)的物理內(nèi)存還未分配, cpu 都會生成一個 page fault
季俩,進而執(zhí)行操作系統(tǒng)的 page fault handler
钮糖。如果是因為還未分配物理內(nèi)存,操作系統(tǒng)會立即分配物理內(nèi)存給當(dāng)前進程,然后重試產(chǎn)生這個page fault
的內(nèi)存訪問指令店归。
程序加載時阎抒,不可能一下全部加載到內(nèi)存,類似懶加載消痛。這樣就導(dǎo)致訪問虛擬內(nèi)存的某一頁時且叁,沒有和真正的物理內(nèi)存進行映射,導(dǎo)致page fault
秩伞。每次page fault
都會阻塞進程逞带,耗費5ms左右的時間。
對用戶而言纱新,使用App時第一個直接體驗就是啟動 App 時間展氓,而啟動時期會有大量的類、分類脸爱、三方等等需要加載和執(zhí)行遇汞,此時多個Page Fault
所產(chǎn)生的的耗時往往是不能小覷的,下面我們就通過二進制重排來優(yōu)化啟動耗時簿废。
三空入、二進制重排原理
程序啟動時,會加載必要的page
捏鱼,為了減少page fault
的數(shù)量执庐,加快啟動速度,可以把必要的代碼盡量合并到一個page
中导梆。iOS是ld
鏈接器轨淌,可以通過order
文件進行二進制重排達(dá)到這個目的。
假設(shè)在啟動時期我們需要調(diào)用兩個函數(shù) method1
與 method4
看尼,函數(shù)編譯在 mach-O
中的位置是根據(jù) ld
( Xcode 的鏈接器) 的編譯順序并非調(diào)用順序來的递鹉,因此很可能這兩個函數(shù)分布在不同的內(nèi)存頁上。如下圖藏斩,那么啟動時躏结,page1
與 page2
都需要從無到有加載到物理內(nèi)存中,從而觸發(fā)兩次Page Fault
狰域。二進制重排的做法就是將 method1
與method4
放到一個內(nèi)存頁中媳拴,那么啟動時則只需要加載一次 page
即可,也就是只觸發(fā)一次 Page Fault
兆览。在實際項目中屈溉,我們可以將啟動時需要調(diào)用的函數(shù)放到一起 ( 比如 前10頁中 ) 以盡可能減少 Page Fault
,進而減少啟動耗時抬探。
四子巾、調(diào)試 Page Fault
最好是卸載App,重新安裝,調(diào)試第一次啟動的效果线梗。如果多次啟動調(diào)試椰于,你會發(fā)現(xiàn)count的波動范圍很大。所以如果想獲取準(zhǔn)確的數(shù)據(jù)仪搔,最好重新安裝App或者打開多個App之后瘾婿,再來調(diào)試。這是因為內(nèi)存管理機制僻造,殺掉進程時憋他,他所占用的物理內(nèi)存空間,如果沒有被覆蓋使用髓削,那么這部分內(nèi)存有很大可能一直存在。重新打開镀娶,內(nèi)存就不需要全部初始化立膛。所以 冷熱啟動的界定不能以是否后臺殺死來簡單判斷。
首先Xcode跑對應(yīng)項目到手機上梯码,然后每次殺后臺宝泵,重啟,打開Xcode選中 Open Developer Tool
的Instruments
面板轩娶,選擇System Trace
儿奶,選擇真機設(shè)備,點擊運行按鈕鳄抒,等待首頁出現(xiàn)點擊?停止闯捎,等待分析完成。刪除過濾條件许溅,直接輸入main
瓤鼻,找到項目的target
箭頭展開,點擊Main thread
贤重,根據(jù)圖中選擇Virtual Memory
查看 File Backed Page In
次數(shù)茬祷。
- 打開
Instruments
,選擇System Trace
并蝗。
- 選擇真機祭犯,選擇工程,選擇啟動滚停,當(dāng)頁面加載出來的時候沃粗,停止。
這里面File Backed Page In
就是page fault
的次數(shù)铐刘。當(dāng)我們把APP殺死后里面再啟動陪每,結(jié)果發(fā)現(xiàn)File Backed Page In
這個值變得很小,說明APP就算殺死后,在啟動不是冷啟動檩禾,還是有一部數(shù)據(jù)在系統(tǒng)的緩存中挂签。如何才是真正的冷啟動呢,我們可以把APP殺掉后啟動多個手機里面的APP盼产,然后再啟動APP饵婆,發(fā)現(xiàn)File Backed Page In
又變得很大。二進制重排是在鏈接階段生成的戏售,重排之后生成可執(zhí)行文件侨核,所以我們只能在編譯階段來優(yōu)化,而無法對已生成的ipa進行優(yōu)化灌灾。
五搓译、order文件
前面說了這么多,那么具體該怎么操作呢锋喜?蘋果其實已經(jīng)給我們提供了這個機制些己。實際上 二進制重排就是對即將生成的可執(zhí)行文件重新排列,即它發(fā)生在鏈接階段嘿般。首先段标,Xcode 用的鏈接器叫做 ld
,ld
有一個參數(shù)叫 Order File
炉奴,我們可以通過這個參數(shù)配置一個后綴名為order
的文件路徑逼庞。在這個 order
文件中,將你需要的符號按順序?qū)懺诶锩嬲案稀.?dāng)工程 build
的時候赛糟,Xcode 會讀取這個文件,打的二進制包就會按照這個文件中的符號順序進行生成對應(yīng)的 mach-O
共耍。
我們可以在XCode配置二進制重排虑灰,首先我們要確定符號的順序,才能知道怎么重排痹兜,XCode使用的鏈接器叫做ld
穆咐,ld
有個參數(shù)叫order_file
,我們可以將文件的路徑告訴XCode字旭,在order_file
文件中把符號的順序?qū)戇M去对湃,XCode編譯的時候就會按照文件中的符號順序打包成二進制可執(zhí)行文件。
我們可以在蘋果的objc4-750源碼中找到這種文件遗淳。
可以參考一下libObjc
項目拍柒,它已經(jīng)使用了二進制重排進行優(yōu)化。這些都是ios應(yīng)用啟動加載過程中熟悉的方法屈暗。order
文件里符號寫錯了或不存在會不會有問題拆讯?ld
會忽略這些符號脂男,如果提供了link
選項-order_file_statistics
,他們會以warning
的形式把這些沒找到的符號打印在日志里种呐。會不會影響上架宰翅?不會,order
文件只是重新排列了所生成的mach-O(可執(zhí)行文件)
中函數(shù)表與符號表的順序爽室。打開后是下面這種格式:
里面全是函數(shù)符號汁讼,我們打開項目,在build setting
里面搜索order file
阔墩,發(fā)現(xiàn)這里面指定了order
的文件路徑嘿架,因為一旦在這里指定了order file
的路徑,XCode就會在編譯的時候按照文件里面寫進去的順序啸箫。
我們現(xiàn)在寫一個Demo耸彪,AppDelegate添加如下方法。
+ (void)test111 {
NSLog(@"test111");
}
+ (void)test222 {
NSLog(@"test222");
}
+ (void)test333 {
NSLog(@"test333");
}
然后編譯忘苛,如何查看整個項目的符號順序呢搜囱,我們到Build Settings
搜索Link Map
,Link Map就
是我們鏈接的符號表柑土,我們把它改成YES,這樣編譯的時候就會把鏈接的符號表給我們寫出來绊汹。
command + R
我們運行下稽屏,然后在Products
里面的.app
文件,在我們Intermediates.noindex
-->項目名.build
--->Debug-iphoneos
-->項目名.build
--->項目名-LinkMap-normal-x86_64.txt
西乖,這個文件里面就有鏈接的符號順序表狐榔。
我們在項目中用touch
創(chuàng)建test.order
文件,修改方法順序获雕。
然后在Build setting
里面搜下order file
在后面將該文件地址添加進去薄腻。
這樣Xcode在編譯時候就會按照order
文件中的符號順序鏈接代碼了,我們編譯一下届案,再看一下LinkMap-normal-x86_64.txt
文件庵楷。
我們發(fā)現(xiàn)是按照order的符號順序來的,而且如果order里面寫了項目中不存在的方法符號楣颠,XCode會自動過濾掉尽纽,不存在影響。我們二進制重排并非只是修改符號地址 , 而是利用符號順序 , 重新排列整個代碼在文件的偏移地址 , 將啟動需要加載的方法地址放到前面內(nèi)存頁中 , 以此達(dá)到減少 page fault 的次數(shù)從而實現(xiàn)時間上的優(yōu)化 , 一定要清楚這一點童漩。
項目實戰(zhàn)
1弄贿、新建一個項目,添加方法
2矫膨、修改配置差凹,編譯期奔,找到xxx.txt
文件
3、新建一個order
文件:touch binary.order
危尿,加入幾個方法
-[ViewController test3]
-[ViewController test2]
-[ViewController test1]
4呐萌、修改Order File
配置為:$(SRCROOT)/Binary/binary.order 或 ./Binary/binary.order
5、clean
脚线,編譯搁胆,再次查看xxx.txt
文件。
可以看到邮绿,我們所寫的這三個方法已經(jīng)被放到最前面了渠旁,也就是說,這三個方法被放到了距離 mach-O
中首地址偏移量最小位置船逮。假設(shè)這三個方法原本在不同的三頁顾腊,那么意味著我們已經(jīng)優(yōu)化掉了兩個 Page Fault
。到這里挖胃,離啟動優(yōu)化就只差一步了杂靶,如何獲取啟動運行的函數(shù)?我們采用clang
插樁的技術(shù)方案酱鸭,這樣完全拿到 swift吗垮、oc、c凹髓、block
全部函數(shù)烁登。
六、靜態(tài)插樁代碼覆蓋工具的機制和原理
簡單來說 SanitizerCoverage
是 Clang 內(nèi)置的一個代碼覆蓋工具蔚舀。它把一系列以 __sanitizer_cov_trace_pc_
為前綴的函數(shù)調(diào)用插入到用戶定義的函數(shù)里饵沧,借此實現(xiàn)了全局 AOP 的大殺器。其覆蓋之廣赌躺,包含 Swift/Objective-C/C/C++ 等語言狼牺,Method/Function/Block 全支持。
開啟SanitizerCoverage
的方法是:在build settings
里的 Other C Flags
中添加 -fsanitize-coverage=func,trace-pc-guard
礼患。如果含有 Swift 代碼的話是钥,還需要在 Other Swift Flags
中加入 -sanitize-coverage=func
和 -sanitize=undefined
。所有鏈接到 App 中的二進制都需要開啟 SanitizerCoverage
讶泰,這樣才能完全覆蓋到所有調(diào)用咏瑟。通過llvm
插樁的確定 order_file
的方案,需要使用源碼重新打包痪署。如果項目全是已經(jīng)編譯好的二進制模塊码泞,使用該方案效果不佳。
騰訊大神寫了個工具 AppOrderFiles
狼犯。CocoaPods 接入余寥,程序啟動完成函數(shù)一行調(diào)用领铐,生成 Order File
。全在 GitHub 里了:github.com/yulingtianx…
AppOrderFiles(^(NSString *orderFilePath) {
NSLog(@"OrderFilePath:%@", orderFilePath);
});
添加編譯設(shè)置宋舷。直接搜索 Other C Flags
來到 Apple Clang - Custom Compiler Flags
中添加配置:-fsanitize-coverage=trace-pc-guard
绪撵。通過這種方式適合純 OC 工程獲取符號。由于 swift 的編譯器前端是自己的 swift 編譯前端程序祝蝠,因此配置稍有不同音诈。搜索 Other Swift Flags
,添加兩條配置即可:-sanitize-coverage=func绎狭、 -sanitize=undefined
细溅。swift類同樣可以通過這個方式獲取。
cocoapod 工程引入的庫儡嘶,會產(chǎn)生多 targe
t喇聊,我們在主target
添加的配置是不會生效的,我們需要針對需要的target
做對應(yīng)的設(shè)置蹦狂。對于直接手動導(dǎo)入到工程里的 sdk
誓篱,不管是靜態(tài)庫 .a
還是動態(tài)庫,會默認(rèn)使用主工程的設(shè)置凯楔,也就是可以拿到符號的窜骄。
按照上面配置完成以后,在代碼任意地方實現(xiàn)如下兩個方法摆屯。這樣所有的方法調(diào)用后啊研,都會調(diào)用一次__sanitizer_cov_trace_pc_guard
方法。在每個函數(shù)調(diào)用的第一句實際代碼鸥拧,會被添加進去了一個 bl
指令, 調(diào)用到__sanitizer_cov_trace_pc_guard
這個函數(shù)中來 削解。
bl
是匯編跳轉(zhuǎn)指令富弦,即調(diào)用方法。靜態(tài)插樁實際上是在編譯期氛驮,在每一個函數(shù)內(nèi)部第一行代碼處腕柜,添加 hook
代碼 ( 即我們添加的__sanitizer_cov_trace_pc_guard
函數(shù) ) ,實現(xiàn)全局的方法 hook
矫废,即AOP
效果盏缤。
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) {
if (!*guard) return; // Duplicate the guard check.
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);
}
七、寫入order文件
寫入文件時有許多需要注意的地方蓖扑,即坑點唉铜。考慮到這個方法會來特別多次律杠,使用鎖會影響性能潭流,這里使用蘋果底層的原子隊列 ( 底層實際上是個棧結(jié)構(gòu)竞惋,利用隊列結(jié)構(gòu) + 原子性來保證順序 ) 來實現(xiàn)。上述這種clang
插樁的方式灰嫉,會在while
循環(huán)中同樣插入hook
代碼拆宛。通過匯編會查看到while
循環(huán),會被多次靜態(tài)加入 __sanitizer_cov_trace_pc_guard
調(diào)用讼撒,導(dǎo)致死循環(huán)浑厚。解決方式是將 Other C Flags
修改為如下:-fsanitize-coverage=func,trace-pc-guard
。func
表示僅hook
函數(shù)時調(diào)用根盒。
引入頭文件
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
pragma mark - 獲取order文件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (YES) {
//offsetof 就是針對某個結(jié)構(gòu)體找到某個屬性相對這個結(jié)構(gòu)體的偏移量
SymbolNode * node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));
if (node == NULL) break;
Dl_info info;
dladdr(node->pc, &info);
NSString * name = @(info.dli_sname);
// 添加 _
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
//去重
if (![symbolNames containsObject:symbolName]) {
[symbolNames addObject:symbolName];
}
}
//取反
NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
NSLog(@"%@",symbolAry);
//將結(jié)果寫入到文件
NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"binary.order"];
NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
if (result) {
NSLog(@"%@",filePath);
}else{
NSLog(@"文件寫入出錯");
}
}
//原子隊列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號結(jié)構(gòu)體
typedef struct{
void * pc;
void * next;
}SymbolNode;
pragma mark - 靜態(tài)插樁代碼
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) {
//if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
SymbolNode * node = malloc(sizeof(SymbolNode));
*node = (SymbolNode){PC,NULL};
//入隊
// offsetof 用在這里是為了入隊添加下一個節(jié)點找到 前一個節(jié)點next指針的位置
OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
}
最后運行钳幅,下載.order
文件到本地,就可以愉快的玩耍了郑象。這樣order
文件就被寫入沙盒中贡这。我們可以把這個文件導(dǎo)出,加入到我們工程中厂榛。生成的order
文件如下圖所示:
八盖矫、驗證插樁前后差距
重排前后LinkMap
文件對比,如下圖所示:
插樁前監(jiān)控數(shù)據(jù):
插樁后監(jiān)控數(shù)據(jù):
本次測試設(shè)備是iPhone xs max击奶,優(yōu)化結(jié)果:627.51-169.06=458.45ms辈双。
使用方法
每次準(zhǔn)備發(fā)版前,使用MedLinkerOptimize
這個target
柜砾,運行App湃望,點擊首頁的"重排"按鈕,然后點擊"生成order
文件”按鈕(此時比較耗時痰驱、耐心等待证芭!),用新生成的order
文件替換項目中的舊order
文件担映。
九废士、全文總結(jié)
我們可以看到圖中項目的Page Fault
數(shù)量并不多,這是因為當(dāng)前項目是一個demo蝇完,代碼和文件都極少官硝。當(dāng)代碼多起來的話,Page Fault
的 數(shù)量和加載耗時都會隨著代碼增加而增加短蜕。二進制重排 可以很好優(yōu)化這個問題氢架,其中心思想是重新排列 方法符號的順序, 使啟動的相關(guān)方法排在最前面從而減少啟動Page Falut
的數(shù)量朋魔。我們先來看看原來的符號順序岖研,這需要用到 鏈接映射文件 Link Map File
。Link Map File
里可以看到方法符號的排序警检。知道了原來的符號排序缎玫,開發(fā)者怎么去設(shè)置自己想要的順序呢硬纤?Xcode提供了排列符號的設(shè)置給開發(fā)者,設(shè)置 order_file
即可赃磨。蘋果也一直身體力行筝家,objc
源碼就采用了二進制重排優(yōu)化。雖然知道了可以通過設(shè)置 .order
文件調(diào)整符號的位置邻辉,但是并不知道怎么編寫 order_file
溪王。下載objc-750
源碼(源碼下載地址),查看其order_file
值骇。打開libobjc.order
莹菱,原來只需要填寫符號即可。全手寫一定是不可取的吱瘩,想實現(xiàn)自動化就要解決下列問題:保證不遺漏方法道伟。保證方法符號正確。保證方法符號順序正確使碾。抖音團隊使用的是 靜態(tài)掃描+運行時trace
的方案蜜徽, 能夠覆蓋到80%~90%的符號。但是上述的方法也存在性能瓶頸票摇。initialize hook
不到拘鞋。部分block hook
不到。C++通過寄存器的間接函數(shù)調(diào)用靜態(tài)掃描不出來矢门。為了解決這個瓶頸盆色,我打算嘗試一下在文末提到的編譯期插樁。顧名思義祟剔,編譯插樁就是在代碼編譯期間修改已有的代碼或者生成新代碼隔躲。編譯期時,在每一個函數(shù)內(nèi)部二進制源數(shù)據(jù)添加 hook
代碼來實現(xiàn)全局hook
效果物延。
說白了我們要跟蹤到 每個方法的執(zhí)行蹭越,從而獲取到啟動時 方法執(zhí)行的順序,然后再按照這個順序去編寫order file
教届。跟蹤的具體實現(xiàn)會用到clang 的 SanitizerCoverage
,這是什么東西驾霜?案训?LLVM 具有內(nèi)置的簡單代碼覆蓋率檢測工具(SanitizerCoverage
)它可以在函數(shù),塊粪糙、邊緣級別插入用戶定義函數(shù)并提供回調(diào)强霎。通過看守者跟蹤 (Tracing PCs with guards
)
文檔是個好東西~里面就有 example。
十蓉冈、摒棄解釋城舞,直接使用
1轩触、使用二進制重排
二進制重排原理
嘗試這樣一種場景,在應(yīng)用啟動過程中家夺,調(diào)用到的方法處在不同的內(nèi)存分頁上脱柱,那么在啟動過程中就會不停地觸發(fā)缺頁中斷,導(dǎo)致進程阻塞拉馋,從而引起啟動時間變長榨为。而如果可以嘗試將啟動過程所需要的方法盡可能集中在較少的分頁上,通過減少缺頁中斷的觸發(fā)次數(shù)煌茴,就可以將啟動時間縮短随闺。這就二進制重排優(yōu)化啟動的基本原理。
如何判斷缺頁中斷耗時
既然需要進行優(yōu)化蔓腐,那就需要有個衡量的標(biāo)準(zhǔn)矩乐,來進行優(yōu)化前后的對比來查看優(yōu)化是否達(dá)到預(yù)期效果。這里主要有兩種方式回论。
使用Instruments工具
在Xcode中可以使用Instruments
工具中的System Trace
工具來查看在應(yīng)用啟動階段中缺頁中斷的觸發(fā)次數(shù).為了能夠更加真實的反應(yīng)數(shù)據(jù) , 最好是將應(yīng)用殺掉重新安裝 , 因為冷熱啟動的界定其實由于進程的原因并不一定后臺殺掉應(yīng)用重新打開就是冷啟動散罕。
- 打開
Instruments
, 選擇System Trace
透葛。
- 選擇真機芭届,選擇工程,點擊啟動眠饮,當(dāng)首個頁面加載出來點擊停止暑劝。
- 等待分析完成,查看缺頁次數(shù)萨蚕。這就是第一次安裝時候的
page fault
次數(shù)與耗時靶草。
設(shè)置Xcode調(diào)試參數(shù)
通過Xcode啟動參數(shù)設(shè)置可以查看到啟動過程的耗時,從側(cè)面做一個驗證岳遥。打開項目在Xcode中奕翔,通過Edit Scheme
->Arguments
(或者快捷鍵組合cmd+shift+,
)設(shè)置打印加載數(shù)據(jù)分析參數(shù)來查看啟動參數(shù)。這樣就可以在啟動之后查看到啟動加載相關(guān)操作的耗時浩蓉。
如何查看自己工程的符號順序
不得不說派继,Xcode是開發(fā)神器,你需要的功能它幾乎都有捻艳。Link Map
是編譯期間產(chǎn)生的產(chǎn)物 驾窟,(ld
的讀取二進制文件順序默認(rèn)是按照Compile Sources - GUI
里的順序 ) ,它記錄了二進制文件的布局认轨,這時候就可以通過設(shè)置Write Link Map File
來查看绅络。
然后運行項目,就會在Products
的同級目錄中找到關(guān)于項目的一個.txt
文件,這里就保存了在編譯期間的二進制分布信息恩急。這個符號順序明顯是按照 Compile Sources
的文件順序來排列的杉畜。
在這個.txt
文件中可以看到符號的加載順序:
如何改變二進制符號的加載順序
在Xcode中可以通過設(shè)置Order File
來人為干預(yù)編譯期間的符號加載順序。隨便定義一個symbols.order
文件:
-[ViewController clipView]
_CGRectMake
-[ViewController pan:]
在Xcode編譯配置中設(shè)置symbols.order
的路徑:
清理項目編譯衷恭,重新編譯此叠,然后查看編譯生成的.txt文件,就會發(fā)現(xiàn)設(shè)置的order
文件確實改變了文件的編譯順序匾荆。
獲取啟動期間加載的所有符號
基于以上的實踐基礎(chǔ)拌蜘,可以使用clang
靜態(tài)插樁來獲取啟動期間調(diào)用到的函數(shù)符號。
編寫order文件
獲取到啟動期間的所有符號之后對符號進行調(diào)整順序去重之后牙丽,寫入.order
文件用以改變編譯期間的二進制布局简卧,達(dá)到減少觸發(fā)缺頁中斷,縮短啟動時間的目的烤芦。
2举娩、clang靜態(tài)插樁方式
靜態(tài)插樁作用
通過靜態(tài)插樁,可以查看項目中的代碼執(zhí)行情況构罗,進而為項目優(yōu)化提供依據(jù)铜涉。
-
重排二進制文件:可以根據(jù)啟動時調(diào)用的方法,存儲在
.order
文件中遂唧,認(rèn)為干預(yù)二進制文件的生成芙代,優(yōu)化啟動速度; - 刪除無用代碼:可以根據(jù)項目中方法的執(zhí)行情況盖彭,查看方法的覆蓋率纹烹,將沒有使用到的方法進行刪除,減少二進制文件的大姓俦摺铺呵;
- 跟蹤方法調(diào)用順序:可以將調(diào)用的符號進行保存來查看應(yīng)用中方法的調(diào)用順序,跟蹤異常隧熙。
步驟一:添加 Build Setting 設(shè)置
Target -> Build Setting -> Custom Complier Flas ->
Other C Flags 添加:
-fsanitize-coverage=func,trace-pc-guard
Other Swift Flags 添加:
-sanitize-coverage=func
-sanitize=undefined
代碼插樁是指根據(jù)一定的策略在代碼中插入樁點來統(tǒng)計代碼覆蓋的技術(shù)手段片挂,一般可以分為三個粒度:
- 函數(shù)(function):按照函數(shù)為單位進行插樁;
- 基本塊(basic block):按照代碼執(zhí)行單元進行分組的執(zhí)行單元贞盯,單元內(nèi)部的代碼執(zhí)行次數(shù)一定是相同的音念;
- 邊界(Edge):按照代碼執(zhí)行路徑進行插樁。
針對iOS來說躏敢,clang
支持以上粒度的插樁方式闷愤。這里先介紹一些函數(shù)粒度的插樁實現(xiàn)。Clang
是一個高度模塊化開發(fā)的輕量級編譯器父丰。可以通過設(shè)置Clang
的編譯參數(shù)實現(xiàn)靜態(tài)插樁。在Xcode->Build Settings
中搜索Other C Flags
然后在其中添加
// 基本塊覆蓋可以使用參數(shù): -fsanitize-coverage=bb,trace-pc-guard
// 邊緣覆蓋可以使用參數(shù): -fsanitize-coverage=edge,trace-pc-guard
-fsanitize-coverage=func,trace-pc-guard
步驟二:添加代碼
添加以下兩個函數(shù)到啟動最早的那個 ViewController
即可蛾扇。
#import "dlfcn.h"
#import <libkern/OSAtomic.h>
//初始化原子隊列
static OSQueueHead list = OS_ATOMIC_QUEUE_INIT;
//定義節(jié)點結(jié)構(gòu)體
typedef struct {
void *pc; //存下獲取到的PC
void *next; //指向下一個節(jié)點
} Node;
哨兵初始化函數(shù)攘烛,其中[*start,*end)
表示了哨兵的標(biāo)志,這里可以理解為每個哨兵guard
是一個指針镀首,保存了一個uint32_t
的整形數(shù)據(jù)來作為自己的標(biā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.
}
當(dāng)每個函數(shù)開始調(diào)用時會被插入該回調(diào),所以在方法調(diào)用開始就會執(zhí)行該回調(diào)更哄。函數(shù)中guard
就是__sanitizer_cov_trace_pc_guard_init
中[start, end)
區(qū)間中一個芋齿。
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// 在這里可以嘗試獲取到執(zhí)行函數(shù)的信息
void *PC = __builtin_return_address(0);
Node *node = malloc(sizeof(Node));
*node = (Node){PC, NULL};
// offsetof() 計算出列尾,OSAtomicEnqueue() 把 node 加入 list 尾巴
OSAtomicEnqueue(&list, node, offsetof(Node, next));
}
然后運行項目會發(fā)現(xiàn)成翩,回調(diào)正常執(zhí)行那么問題來了觅捆,如何在回調(diào)函數(shù)中獲取到當(dāng)前執(zhí)行函數(shù)的信息呢?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray *arr = [NSMutableArray array];
while(1){
//有進就有出麻敌,這個方法和 OSAtomicEnqueue() 類比使用
Node *node = OSAtomicDequeue(&list, offsetof(Node, next));
//退出機制
if (node == NULL) {
break;
}
//獲取函數(shù)信息
Dl_info info;
dladdr(node->pc, &info);
NSString *sname = [NSString stringWithCString:info.dli_sname encoding:NSUTF8StringEncoding];
printf("%s \n", info.dli_sname);
//處理c函數(shù)及block前綴
BOOL isObjc = [sname hasPrefix:@"+["] || [sname hasPrefix:@"-["];
//c函數(shù)及block需要在開頭添加下劃線
sname = isObjc ? sname: [@"_" stringByAppendingString:sname];
//去重
if (![arr containsObject:sname]) {
//因為入棧的時候是從上至下栅炒,取出的時候方向是從下至上,那么就需要倒序术羔,直接插在數(shù)組頭部即可
[arr insertObject:sname atIndex:0];
}
}
//去掉 touchesBegan 方法 啟動的時候不會用到這個
[arr removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//數(shù)組合成字符串
NSString * funcStr = [arr componentsJoinedByString:@"\n"];
//寫入文件
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"link.order"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
NSLog(@"%@", filePath);
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
步驟三:取出 order file
- 在步驟二的代碼
NSLog(@"%@", filePath);
斷點 - 如果頁面無法觸發(fā)點擊赢赊,
viewDidLoad
里面調(diào)用touchesBegan:withEvent:
也可以 - 運行代碼后記錄
link.order
的路徑 -
Finder
前往路徑取出order file
使用Xcode連接真機,啟動應(yīng)用直至第一個控制器界面加載完成级历,使用快捷鍵cmd+shift+2
進入Devices and Simulators
界面释移,選擇對應(yīng)應(yīng)用并點擊Download Containers
選擇保存路徑下載文件。在下載的.xcappd
中右鍵顯示包內(nèi)容寥殖,在AppData->Library->Caches
路徑下即可保存的.txt
文件玩讳。由于隊列的特性,這里的符號與實際調(diào)用順序是相反的扛禽。這樣就可查看到在應(yīng)用首個控制器顯示之前系統(tǒng)調(diào)用的所有符號锋边,從而為應(yīng)用啟動優(yōu)化奠定基礎(chǔ)。
步驟四:設(shè)置 order file
把link.order
的路徑放到工程根目錄
Target -> Build Setting -> Linking -> Order File
設(shè)置路徑
步驟五:編譯代碼
把步驟一 order file
的設(shè)置還原
把步驟二添加代碼刪除
clean
以后編譯代碼