極客時(shí)間戴銘學(xué)習(xí)筆記
App啟動(dòng)干了什么
一般分為冷啟動(dòng)和熱啟動(dòng)兩種
冷啟動(dòng)指, App點(diǎn)擊啟動(dòng)前, 進(jìn)程不在系統(tǒng)里, 需要系統(tǒng)新創(chuàng)建一個(gè)進(jìn)程分配給該App, 這是一個(gè)完整的啟動(dòng)過(guò)程
熱啟動(dòng)指, App在冷啟動(dòng)后用戶(hù)將app退到后臺(tái), 在App的進(jìn)程還在系統(tǒng)里用戶(hù)重新進(jìn)入?的過(guò)程
冷啟動(dòng)階段
三個(gè)階段: main函數(shù)執(zhí)行前、main之后、首屏渲染完成后
main()執(zhí)行前
加載可執(zhí)行文件(App的.o文件的集合)
加載動(dòng)態(tài)鏈接庫(kù), 進(jìn)行rebase指針調(diào)整和bind符號(hào)綁定
Objc運(yùn)行時(shí)的初始化處理, 包括Objc相關(guān)類(lèi)的注冊(cè)、category注冊(cè)、selector唯一性檢查等
初始化, 包括了執(zhí)行+load()方法、attribute((constructor))修飾的函數(shù)調(diào)用、創(chuàng)建C++靜態(tài)全局變量
相應(yīng)優(yōu)化方法
減少動(dòng)態(tài)庫(kù)加載. 每個(gè)庫(kù)本身都有依賴(lài)關(guān)系, 蘋(píng)果公司建議使用更少的動(dòng)態(tài)庫(kù), 并且建議在使用動(dòng)態(tài)庫(kù)的數(shù)量較多時(shí), 盡量將多個(gè)動(dòng)態(tài)庫(kù)進(jìn)行合并. 數(shù)量上, 最多可以支持6個(gè)非系統(tǒng)動(dòng)態(tài)庫(kù)合并為一個(gè)
減少加載啟動(dòng)后不會(huì)去使用的類(lèi)或方法
+load()方法里的內(nèi)容可以放到首屏渲染完成后再執(zhí)行, 或者用+initialize()方法替換掉, 因?yàn)? 在一個(gè)+load()方法里,進(jìn)行運(yùn)行時(shí)方法替換操作會(huì)帶來(lái)4毫秒的消耗
控制C++全局變量的數(shù)量
main()函數(shù)執(zhí)行后
main()函數(shù)執(zhí)行后的階段, 指從main()函數(shù)執(zhí)行開(kāi)始, 到appDelegate的didFinishLaunchingWithOptions方法里首屏渲染執(zhí)行完成
首頁(yè)的業(yè)務(wù)代碼都是在這個(gè)階段, 也就是首屏渲染前執(zhí)行的, 主要包括了:
首屏初始化所需配置文件的讀寫(xiě)操作;
首屏列表大數(shù)據(jù)的讀取;
首屏渲染的大量計(jì)算等.
相應(yīng)優(yōu)化方法
從功能上保留首屏渲染必要的初始化功能, 啟動(dòng)必要的初始化功能,
其他使用時(shí)才需要初始化的可以?xún)?yōu)化到之后的時(shí)間里執(zhí)行
首屏渲染完成后
非首屏其他業(yè)務(wù)服務(wù)模塊的初始化、監(jiān)聽(tīng)的注冊(cè)、配置文件的讀取.
從函數(shù)上看, 這個(gè)階段指的是截止到didFinishLaunchingWithOptions方法作用域內(nèi)執(zhí)行首屏渲染之后的所有方法執(zhí)行完成.
這個(gè)優(yōu)化的優(yōu)先級(jí)排在最后
功能級(jí)別的啟動(dòng)優(yōu)化
跟前面有點(diǎn)重復(fù), main()函數(shù)開(kāi)始執(zhí)行后到首屏渲染完成前只處理首屏相關(guān)的業(yè)務(wù), 其他非首屏業(yè)務(wù)的初始化、監(jiān)聽(tīng)注冊(cè)、配置文件讀取等放到首屏渲染完成后去做
方法級(jí)別的啟動(dòng)優(yōu)化
某些方法大量調(diào)用會(huì)增加耗時(shí).
監(jiān)控啟動(dòng)速度兩種方案
- 定時(shí)抓取主線程上的方法調(diào)用堆棧, 計(jì)算一段時(shí)間里各個(gè)方法的耗時(shí),比如Time Profiler
- 對(duì)objc_msgSend方法進(jìn)行hook來(lái)掌握所有方法的執(zhí)行耗時(shí), 通過(guò)監(jiān)聽(tīng)所有的Objective-C里方法執(zhí)行來(lái)實(shí)現(xiàn)
objc_msgSend方法執(zhí)行的邏輯是: 先獲取對(duì)象對(duì)應(yīng)類(lèi)的信息, 再獲取方法的緩存, 根據(jù)方法的selector查找函數(shù)指針, 經(jīng)過(guò)異常錯(cuò)誤處理后, 最后跳到對(duì)應(yīng)函數(shù)的實(shí)現(xiàn)
使用fishhook的方法來(lái)實(shí)現(xiàn)
基于靜態(tài)庫(kù)插樁的二進(jìn)制重排啟動(dòng)優(yōu)化 (手淘的最新實(shí)踐)
- App 啟動(dòng)時(shí)PageFault的性能分析
- 靜態(tài)庫(kù)插樁重排方案的技術(shù)原理
App啟動(dòng)和PageFault
當(dāng)我們向操作系統(tǒng)申請(qǐng)內(nèi)存時(shí), 操作系統(tǒng)并不是直接分配給我們物理內(nèi)存, 而是只標(biāo)記當(dāng)前進(jìn)程擁有該段內(nèi)存, 當(dāng)真正使用這段內(nèi)存時(shí)才會(huì)分配. 這種延遲分配物理內(nèi)存的方式就通過(guò)page fault機(jī)制來(lái)實(shí)現(xiàn)的. 當(dāng)我們?cè)L問(wèn)一個(gè)內(nèi)存地址時(shí), 如果該地址非法, 或者我們對(duì)其沒(méi)有訪問(wèn)權(quán)限, 或者該地址對(duì)應(yīng)的物理內(nèi)存還未分配, cpu都會(huì)生成一個(gè)page fault, 進(jìn)而執(zhí)行操作系統(tǒng)的page fault handler. 如果是因?yàn)檫€未分配物理內(nèi)存, 操作系統(tǒng)會(huì)立即分配物理內(nèi)存給當(dāng)前進(jìn)程, 然后重試產(chǎn)生這個(gè)page fault的內(nèi)存訪問(wèn)指令
App在啟動(dòng)時(shí), 需要執(zhí)行各種函數(shù), 我們需要讀取TEXT段代碼到物理內(nèi)存中, 這個(gè)過(guò)程會(huì)發(fā)生缺頁(yè)中斷, 由于啟動(dòng)時(shí)所需要執(zhí)行的代碼分布在TEXT段的各個(gè)部分, 會(huì)讀取很多頁(yè)面, 導(dǎo)致啟動(dòng)時(shí)Page Fault數(shù)量非常多. 與直接訪問(wèn)物理內(nèi)存不同, page fault過(guò)程大部分由軟件完成, 消耗時(shí)間比較久, 所以是影響啟動(dòng)性能的一個(gè)關(guān)鍵指標(biāo).
手淘啟動(dòng)時(shí)首先調(diào)用的幾個(gè)方法會(huì)分布在虛擬內(nèi)存的各個(gè)頁(yè)面中, 執(zhí)行這些方法時(shí), 需要從讀取到物理內(nèi)存中, 就會(huì)產(chǎn)生多次page fault.
如果能將啟動(dòng)階段需要的讀取代碼集中排布, 將這些方法全都放到相鄰的區(qū)域中, 我們讀取這些方法可能就只需要極少的page fault次數(shù). 可以減少不必要的page fault時(shí)間.達(dá)到優(yōu)化啟動(dòng)時(shí)間的效果.
重排前后的函數(shù)在頁(yè)面的布局對(duì)比

重排方案
如何獲取方法的執(zhí)行順序
為了生存order_file, 我們需要確定應(yīng)用啟動(dòng)時(shí)方法的執(zhí)行順序.
抖音通過(guò)靜態(tài)掃描和運(yùn)行時(shí)Trace等方法確定order_file, 該方案無(wú)法覆蓋initialize、block和C++通過(guò)寄存器的間接函數(shù)調(diào)用
Facebook分享過(guò)通過(guò)llvm插樁的確定order_file的方案, 需要使用源碼重新打包.
靜態(tài)庫(kù)插樁
我們編譯過(guò)的靜態(tài)庫(kù)由.o文件組成, 我們可以對(duì).o中的函數(shù)代碼進(jìn)行修改, 在每個(gè)函數(shù)的開(kāi)頭插入調(diào)用我們指定記錄函數(shù)的指令
生成order file
linkmap記錄了連接過(guò)程中的相關(guān)信息. 其中包含鏈接用到的symbol相關(guān)信息.通過(guò)pc address減去slide得到的地址, 我們可以在linkmap中找到對(duì)應(yīng)的symbol
address = pc - slide. // 因?yàn)锳SLR, APP 可執(zhí)行文件隨機(jī)載入的原因,需要處理一下偏移
量。
我們需要將之前記錄的地址轉(zhuǎn)換成對(duì)應(yīng)的符號(hào), 為了真實(shí)還原線上的執(zhí)行環(huán)境, 在app中簡(jiǎn)單的記錄了pc地址和image的偏移量. 通過(guò)解析linkmap, 獲取函數(shù)的地址區(qū)間, 得到距離address最近的symbol, 生成order_file
更改符號(hào)的排列順序
默認(rèn)情況下, Id鏈接器會(huì)按照鏈接的順序?qū)⒏鱾€(gè).o文件的數(shù)據(jù)重新布局生成可執(zhí)行文件.Id鏈接器提供-order-file選項(xiàng)操控?cái)?shù)據(jù)排列的順序.在Xcode中可以通過(guò)Order File選項(xiàng)指定符號(hào)排序文件.