啟動優(yōu)化乾巧,在不影響業(yè)務(wù)的前提條件下句喜,怎么提高啟動的速度预愤,這是我們要考慮的事情。
在這咳胃,根據(jù)系統(tǒng)打印提示信息這條主線植康,看下啟動過程中,每個階段都做的什么展懈,在這些階段我們能做哪些優(yōu)化的事情销睁。
添加Xcode的打印環(huán)境DYLD_PRINT_STATISTICS
Total pre-main time: 390.79 milliseconds (100.0%)
dylib loading time: 228.22 milliseconds (58.4%)
rebase/binding time: 5.04 milliseconds (1.2%)
ObjC setup time: 21.47 milliseconds (5.4%)
initializer time: 135.91 milliseconds (34.7%)
slowest intializers :
libSystem.B.dylib : 3.75 milliseconds (0.9%)
libMainThreadChecker.dylib : 41.48 milliseconds (10.6%)
xxxx : 155.96 milliseconds (39.9%)
這是main函數(shù)之前系統(tǒng)啟動過程中,也就是pre-main存崖,經(jīng)過的一些階段冻记,咱們針對每個階段來詳細說。
dylib loading
這是庫加載的時間来惧,包括共享緩存庫冗栗、我們自己的動態(tài)庫,如果進行了越獄開發(fā)供搀,還包含了我們插入的動態(tài)庫贞瞒。
系統(tǒng)的共享緩存庫(UIKit、Foundation)這些趁曼,系統(tǒng)是做了優(yōu)化的,加載時間一般都是固定的棕洋,浪費時間的點一般在我們自己的動態(tài)庫挡闰,我們自己的動態(tài)庫怎么優(yōu)化呢,超過6個最好合并一下掰盘。使用靜態(tài)庫等摄悯。
rebase/binding
rebase :這個是因為我們可執(zhí)行文件加載到虛擬內(nèi)存的時候,都會使用ASLR的技術(shù)愧捕,也就是在內(nèi)存的最前邊加一個隨機的偏移值奢驯,來保證我們應(yīng)用的安全。所以次绘,我們使用的系統(tǒng)的函數(shù)瘪阁、我們自定義的類、變量這些指針邮偎,都需要加上這個偏移值管跺,才是真正在內(nèi)存中的位置。
換句話說禾进,類越少豁跑、方法越少,需要修正的就越少泻云。
那么就可以使用一些工具檢測你APP中的無用類艇拍,無用的方法等狐蜕。
binding:這個是指針對外綁定,上邊提到了共享緩存卸夕,也就是系統(tǒng)的庫层释,是不加載到我們虛擬內(nèi)存中的,他是在公共內(nèi)存中娇哆,供大家都使用的湃累,那么我們寫了系統(tǒng)的函數(shù),怎么最終找到這個真是的函數(shù)地址碍讨,就需要binding這個操作治力,當(dāng)然,這個分為普通綁定和lazy綁定勃黍,這一部分其實沒什么需要優(yōu)化的空間宵统。
ObjC setup
這一部分是加載類信心,初始化類覆获,加載類信息马澈,是從Mach-O的_DATA段中加載一系列類相關(guān)的信息,然后初始化弄息,這部分先加載所有的類痊班,插入到一個大表中,加載方法摹量、protocol涤伐、property等,然后就是初始化類缨称,設(shè)置類的superClass指針(這一步是遞歸創(chuàng)建的)凝果,設(shè)置類的ro/ rw相關(guān)的字段,然后合并Category相關(guān)的信息睦尽,將Category的方法器净、協(xié)議、property按照規(guī)則当凡,插入到生成的類中山害,方法協(xié)議這些都是二維數(shù)組的形式存放,后編譯的Category沿量,插入的位置越靠前粗恢。這個想了解細節(jié)的可以看下源碼。
那這部分的優(yōu)化欧瘪,還是在類的數(shù)量眷射,方法的數(shù)量,協(xié)議的數(shù)量這些。
initializer time
這部分主要做的事情妖碉,加載load方法涌庭,C++靜態(tài)初始化函數(shù)(attribute((constructor))),attribute((constructor)) 的調(diào)用是在load的后邊欧宜,這個可以了解一下坐榆。
這部分的優(yōu)化:減少load方法,盡量將一些操作進行懶加載冗茸,也就是放到+(void)initialize 函數(shù)中荷鼠,這個函數(shù)是在類在第一次被調(diào)用的時候進行調(diào)用的祭犯,注意的是洋丐,這個方法可能會被多次調(diào)用吊圾,最好加dispatch_once做一下防護。
attribute((constructor))這種方法最好就是不用挂绰。
這是系統(tǒng)打印的階段流程屎篱,pre-main我們能做的事情和系統(tǒng)能做的事情就是這些。
在這里拓展一下系統(tǒng)詳細的啟動流程葵蒂,可以比對上述階段做下了解交播。
應(yīng)用的啟動流程
- 內(nèi)核加載我們的Mach-O的可執(zhí)行文件
- 從Mach-O文件中,找到dyld的路徑并加載
從最開始的dyld start開始uintptr_t start() - slide 首先生成一個ASLR的隨機值 (這就是那個偏移值)
- rebaseDyld 開始符號的綁定過程 (將我們的指針開始rebase操作)
- // allow dyld to use mach messaging
mach_init(); - dyld main (這是dyld的main函數(shù))
- 配置環(huán)境變量 (Xcode相關(guān)的一些環(huán)境變量的讀燃丁)
- load shared cache(加載共享緩存秦士,系統(tǒng)動態(tài)庫UIKit等)
- 這里邊有幾個判斷
- 緩存庫是不是只用在這個程序
- 是否存在reuseExistingCache,加載過就什么都不做
- 不存在緩存中永高,加載mapCacheSystemWide
- 這里邊有幾個判斷
- 加載主程序Mach-O(實例化主程序)
- 這是第一個被加載的image
- load inserted libraries 插入動態(tài)庫(越獄開發(fā)修改這個字段伍宦,插入自己的動態(tài)庫),等于問你插入哪些動態(tài)庫,并且加載
- link 鏈接程序乏梁,這是一個遞歸的過程,鏈接我們依賴系統(tǒng)的動態(tài)庫关贵,這里邊也有rebase操作(指針修復(fù)遇骑,前邊加一個偏移)
- binding 符號的綁定
- link 我們inset的dylib
- binding 過程,weak binding等
- initializeMainExecutable()初始化主程序開始了
- 最終dyld調(diào)用到了notifySingle()//后續(xù)就到了runtime去初始化了
- 初始化以后揖曾,會將一個函數(shù)指針load_images落萎,傳給dyld,讓dyld調(diào)用炭剪,load_images這個函數(shù)里邊調(diào)用了load方法练链。do while循環(huán)調(diào)用所有l(wèi)oad
-
attribute((constructor)) void func(){
}
也就是執(zhí)行C++的構(gòu)造函數(shù) - 調(diào)用真正的main入口
runtime中objc所作的預(yù)處理,因為在runtime中放了兩個回調(diào)函數(shù)供dyld去調(diào)用
- objc - setup
- 從Mach-O中讀取類信息奴拦,注冊到一個全局的表中
- 讀取Mach-O方法信息(sel)媒鼓,注冊到全局的一個表中。
- 創(chuàng)建類,信息的組裝realizeClassWithoutSwift
- ISA指針
- superclass指針
- cache
- bits相關(guān)
- 生成rw
- 添加方法绿鸣、協(xié)議疚沐、property等
- 處理category,方法潮模、協(xié)議拷貝進bits中亮蛔。(注意這個二維數(shù)組的形式)
- 生成rw
以上過程包含兩部分,dyld的程序初始化過程擎厢,有一部分需要objc runtime的支持究流。
整個pre-main就這些,也可以自己進行了解后动遭,看些是否還有更多需要注意的地方芬探。
有了pre-mian,就有 main之后相關(guān)沽损。
我這里認定灯节,從main到第一個頁面的viewDidAppear是main之后。
這里邊都是具體業(yè)務(wù)相關(guān)绵估,真正可以節(jié)省時間的點炎疆,都在這里邊;那么啟動前的代碼應(yīng)該怎么寫呢国裳?
個人理解形入,原則上到viewDidAppear所有的函數(shù),都應(yīng)該只是為了服務(wù)第一個頁面的創(chuàng)建及渲染缝左,等第一個頁面出來后亿遂,在初始化其他相關(guān)的內(nèi)容;當(dāng)然這也只是美好的愿望渺杉,有些庫的初始化和一些其他操作蛇数,可能不僅是為了第一個頁面服務(wù),并且還必須要放到這里去做是越。
這其實可以叫做分階段啟動耳舅。
必須要做的:
- 埋點功能、
- Crash 采集
- 網(wǎng)絡(luò)配置
等
可以延后的:各種SDK的初始化倚评、配置文件的請求等浦徊。其實地理位置的請求也可以延后,之前看過美團還是哪個公司天梧,他們會先用上次緩存的地理位置請求盔性,等到首頁加載完以后,如果地理位置不一樣呢岗,再重新刷新冕香。
第二種思路
可以考慮多線程啟動蛹尝,目前手機都是多核,可以開2-3個線程暂筝,是可以加快初始化速度的箩言。
使用這個方案有幾個注意點:
- 如果主線程忙完,子線程還沒忙完焕襟,這時候會黑屏陨收,所以啟動的時候,就要加一個假的圖片鸵赖,等每個線程結(jié)束务漩,都去問一下其他線程狀態(tài)怎么樣了,都結(jié)束了它褪,假的圖片消失饵骨,顯示首頁。
- 不是所有的SDK初始化都適合放到子線程茫打,需要測試居触,有些依賴主線程的執(zhí)行,有些內(nèi)部使用了UIKit相關(guān)的老赤。
其實目前大部分APP都有廣告圖轮洋,我們可以在顯示廣告的時候,去初始化其他的東西抬旺。
main之后也就這些弊予。
去年的時候,抖音發(fā)布了一個二進制重排的技術(shù)進行優(yōu)化开财,這里也研究了一下汉柒。
我們APP加載進內(nèi)存,其實是一個虛擬內(nèi)存责鳍,不是真正的物理內(nèi)存碾褂,代碼的執(zhí)行是要在真正的物理內(nèi)存進行的,這時候历葛,又有一個中間的映射表出現(xiàn)正塌,我們虛擬內(nèi)存的數(shù)據(jù),通過映射表啃洋,映射到真正的物理內(nèi)存,然后進行代碼的執(zhí)行屎鳍,為什么要有中間的映射表呢宏娄?因為他可以使用頁的形式分塊加載進物理內(nèi)存,不必將整個APP全部加載進內(nèi)存逮壁,每一頁的大小是16KB孵坚,也就是代碼執(zhí)行到哪,就通過映射表將這16KB的數(shù)據(jù)映射到物理內(nèi)存去執(zhí)行,這樣可以充分利用物理內(nèi)存卖宠,保證整個物理內(nèi)存基本都是高效率運轉(zhuǎn)巍杈,并且安全性也有保證。
原理就是這樣扛伍,如果每次啟動時候的代碼筷畦,分散在虛擬內(nèi)存中,可能會引發(fā)多次的映射過程(缺頁異常)刺洒,如果能把啟動是的代碼鳖宾,放到一個16KB頁中,是不是映射一次就可以了逆航,當(dāng)然這是理想的情況鼎文,整個啟動的過程大約4000次左右的映射過程大約0.2ms左右,如果進行優(yōu)化因俐,能減少百分之10左右拇惋。
我們可以使用Instruments 中的system trace 進行查看page fault的次數(shù)。
具體怎么將啟動的函數(shù)聚合到一起抹剩,兩種方案:
- 抖音使用的是HOOK objc_msgSend撑帖,找到調(diào)用的所有函數(shù),有幾個缺點吧兔,靜態(tài)方法hook不到磷仰,swift的結(jié)構(gòu)體和枚舉都是值類型、我們自己寫的C函數(shù)境蔼、block這些都hook不到灶平。(具體hook方法可以使用fishkook,一個fb開源的工具箍土,但是還要用到匯編相關(guān)逢享,不過肯定很多開源的)
- 可以考慮使用Clang編譯器帶的函數(shù),一般叫做Clang插樁吴藻,Clang編譯器會在每個函數(shù)執(zhí)行過程中插入代碼瞒爬,打印當(dāng)前函數(shù),這樣找到的方法比較全面沟堡。
__sanitizer_cov_trace_pc_guard
__sanitizer_cov_trace_pc_guard_init配置這兩個函數(shù)侧但,然后將啟動方法輸出到一個文件中,配置到在Xcode中order-file航罗。這樣禀横,系統(tǒng)在編譯的時候,會按照我們制定的順序加載粥血。默認的編譯順序是按照Build Phases中的Compile source里邊加載的柏锄。
搞完酿箭,我們可以通過輸出link map查看symbols的編譯順序。
這里涉及到一點趾娃,冷啟動和熱啟動缭嫡;我們通過page fault的次數(shù)可以看出,如果啟動過的APP抬闷,被殺掉以后妇蛀,下一次啟動page fault的次數(shù)減少了很多,說明啟動過饶氏,內(nèi)存中這些數(shù)據(jù)就已經(jīng)存在了(內(nèi)存中的數(shù)據(jù)銷毀讥耗,其實就是被覆蓋,如果這個時候疹启,我們啟動很多其他的應(yīng)用古程,再次啟動這個應(yīng)用,還是會減緩啟動時間)喊崖。
冷啟動就是內(nèi)存中挣磨,完全不存在這個APP,熱啟動就是被殺掉荤懂,然后接著啟動茁裙,這時候,內(nèi)存中很多數(shù)據(jù)已經(jīng)有了节仿,不需要操作了晤锥。
工具拓展:
怎么監(jiān)控啟動時間呢?
前滴滴出行技術(shù)專家戴銘廊宪,他實現(xiàn)了hook objc_msgSend矾瘾,然后在這個里邊添加開始調(diào)用函數(shù)和函數(shù)調(diào)用結(jié)束的標(biāo)識,從而了解每一個函數(shù)的執(zhí)行時間箭启。
粗顆粒壕翩,可以使用一些定時工具,計算從啟動傅寡,到啟動完放妈,或者某個階段的時間(BLStopwatch可以看下)
Instruments 也是很好的工具 Timer 、system trace都可以顯示每個函數(shù)的執(zhí)行時間荐操,他的實現(xiàn)原理好像是固定時間抓取函數(shù)的調(diào)用棧芜抒,粗略的算下執(zhí)行時間,就和微信開源的性能檢測工具Matrix托启,定時抓取調(diào)用棧信息宅倒,比對棧的頭部,看這個調(diào)用棧出現(xiàn)了幾次驾中,次數(shù)多了唉堪,就認為是卡頓,優(yōu)點類似肩民。
當(dāng)然唠亚,整個流程還有一些細節(jié)點,如果要考慮特別完善持痰,要考慮網(wǎng)絡(luò)灶搜、首頁渲染、數(shù)據(jù)讀取等一些細節(jié)工窍,看有沒有優(yōu)化的空間割卖。