前言
作為程式猿來說,“性能優化”是我們都很熟悉的詞,也是我們需要不斷努⼒以及持續進⾏的事情;其實優化是⼀個很⼤的課題,因為細分來說的話有⼤⼤⼩⼩⼗⼏種優化⽅向 ,但是切忌在實際開發過程中不能盲⽬的 為了優化⽽優化,這樣有時可能會造成适得其反的負效果,需要我們根據實際場景以及業務需求進⾏合理優 化。接下來進⼊正題,本⽂将會以iOS App的啟動優化為展開點進⾏探讨。
啟動流程:
iOS App 的啟動我們都知道分為 為pre-main 和 main() 兩個階段,并且在這兩個階段中,系統會進 ⾏⼀系列的加載操作,過程如下:
1、pre-main階段
1. 加載應⽤的可執⾏⽂件
2. 加載
dyld動态連接配接器
3. dyld遞歸加載應⽤所有依賴的動态連結庫dylib
2、main()階段
1. dyld調⽤ main()
2. 調⽤UIApplicationMain()
3. 調⽤applicationWillFinishLaunching
4. 調⽤didFinishLaunchingWithOptions
階段優化項
針對 pre-main 階段做優化時,我們需要先詳細了解其加載過程,這個可以在2016年
WWDC的 Optimizing App Startup Time 中詳細了解到,
相關材料1.1 Load dylibs

這⼀階段dyld會分析應⽤依賴的 dylib (xcode7以後.dylib已改為名.tbd),找到其 mach-o ⽂件,打開和讀取這些⽂件并驗證其有效性,接着會找到代碼簽名注冊到核心,最後對 dylib 的每⼀個 segment 調⽤ mmap()。不過這⾥的 dylib ⼤部分都是系統庫,不需要我們去做額外的優化。
優化結論:
1、盡量不使⽤内嵌的dylib,從⽽避免增加 `Load dylibs`開銷 2、合并已有的dylib和使⽤靜态庫(static archives),減少dylib的使⽤個數 3、懶加載dylib,但是要注意dlopen()可能造成⼀些問題,且實際上懶加載做的⼯作會更多 |
1.2 Rebase/Bind
在dylib的加載過程中,系統為了安全考慮,引⼊了ASLR (Address Space Layout Randomization)技術和 代碼簽名。由于ASLR的存在,鏡像(Image,包括可執⾏⽂件、 dylib和bundle)會在随機的位址上加載,和 之前指針指向的位址(preferred_address)會有⼀個偏差(slide), dyld需要修正這個偏差,來指向正确的 位址。 Rebase在前, Bind在後, Rebase做的是将鏡像讀⼊記憶體,修正鏡像内部的指針,性能消耗主要在 IO。 Bind做的是查詢符号表,設定指向鏡像外部的指針,性能消耗主要在CPU計算。
在此過程中,我們需要注意的是盡量減少指針數量,⽐如: 1. 減少ObjC類(class)、⽅法(selector)、分類(category)的數量 2. 減少C++虛函數的的數量(建立虛函數表有開銷) 3. 使⽤ Swift struct (内部做了優化,符号數量更少) |
1.3 Objc setup
⼤部分ObjC初始化⼯作已經在Rebase/Bind階段做完了,這⼀步dyld會注冊所有聲明過的ObjC類,将分類插 ⼊到類的⽅法清單⾥,再檢查每個selector的唯⼀性。
在這⼀步倒沒什麼優化可做的, Rebase/Bind階段優化好了,這⼀步的耗時也會減少。
1.4 Initializers
在這⼀階段, dyld開始運⾏程式的初始化函數,調⽤每個Objc類和分類的+load⽅法,調⽤C/C++ 中的構造器 函數(⽤attribute((constructor))修飾的函數),和建立⾮基本類型的C++靜态全局變量。 Initializers階段執⾏ 完後, dyld開始調⽤main()函數。
1. 少在類的+load⽅法⾥做事情,盡量把這些事情推遲到+initiailize 2. 減少構造器函數個數,在構造器函數⾥少做些事情 3. 減少構造器函數個數,在構造器函數⾥少做些事情 |
在這⼀階段⾥,主要優化重點放在 SDK初始化、業務⼯具注冊、整體
didFinishLaunchingWithOptions ⽅法中,因為我們的⼀些第三⽅ app ⻛格配置、啟動引導⻚顯示狀态邏輯、版本更新邏輯等等基本⽅都會在這⾥進⾏,如果這部分邏輯沒有做好優化梳理,随着業務不斷拓展,臃腫的業務邏輯會直接導緻啟動時 間加⻓。
在滿⾜業務需求的前提下,盡量減少 didFinishLaunchingWithOptions ⽅法在主線程中的事件處理邏輯, ⽐如: 1. 根據實際業務狀況,梳理各個⼆⽅/三⽅庫,找到可以延遲加載的庫,做延遲加載處理,⽐如放到⾸⻚控制器 的viewDidAppear⽅法⾥。 2. 梳理業務邏輯,把可以延遲執⾏的邏輯,做延遲執⾏處理。⽐如檢查新版本、注冊推送通知等邏輯 3. 避免進⾏⼀些複雜/多餘的計算邏輯,這類邏輯盡量進⾏異步延遲處理 4. 避免在⾸⻚控制器的viewDidLoad和viewWillAppear做太多容易阻塞主線程的事情,這2個⽅法執⾏完, ⾸⻚控制器才能顯示 |
場景補充:
另外,在我們實際開發過程中,很多項⽬的⾸⻚控制器都會有⼀些背景可配、較為豐富的結構或者推薦資料 進⾏展示,⽽且我們的⾸⻚展示速度通常也會被納⼊啟動優化的⼀部分,其實對于這種類型的優化,如果我 們還隻是⽤傳統的 api -> data -> UI ⽅式進⾏的話,就很難有明顯的改善空間,因為⽤戶的⽹絡狀态 并不是可控項,如果不做其他處理的話,那在很多場景下對⽤戶來說,即使我們放上⼀些占位圖,展示的樣式也是很不友好的,畢竟⾸⻚控制器對⽤戶的第⼀視覺沖擊影響還是⽐較⼤的。
對于這種場景下的優化來說,⼀般我們可以采取 Local + Network + Update 的⽅式在⼀定程度上優化 ⾸⻚加載速度: 即:
1、 app更新過程中,⾸先進⾏本地内嵌處理邏輯,内嵌⾸⻚資料結構( localDataBase)、内嵌⾸⻚樣式所需 資源( localStorage) 2、在安裝啟動之後,對本地與線上資料更新記錄進⾏對⽐,檢測是否需要更新本地内嵌資料結構 3、檢測到有需要更新的資料時,才會對指定結構進⾏靜默更新,并且同步更新本地資料結構 |
這樣做的好處是:
1、⾸⻚資料直接從本地加載,減少⽹絡資料等待時間 2、僅檢測資料key值變化,⼩資料量對⽐定向更新結構,減少api資料互動頻次及資料包體積 3、能夠保證⾸⻚對于⽤戶來說會⼀直處于⼀個友好的展示狀态 |
當然這種也并不是唯⼀的應對⽅式,⽽且也并⾮對所有場景都适⽤,隻是提供⼀種思路⽽已,還是需要根據 項⽬的實際場景選擇适合的優化⽅案。
統計時⻓
另外如果在開發過程中,我們想直覺的檢視 app 啟動期間,各階段的耗時情況,也可以在Xcode,的 edit scheme 設定添加 DYLD_PRINT_STATISTICS 為1 ,列印啟動時⻓,例如
優化前啟動時⻓:
優化後啟動時⻓:
當然,這些log我們僅僅隻能在開發調試階段檢視列印,那麼在實際項⽬中,我們需要對線上項⽬的啟動資料 進⾏監控,以便及時的定位和優化那些影響 app 啟動時⻓的環節,這時我們應該怎樣更好的處理呢?
當然我們可以通過伺服器埋點上報的⽅式⾃⾏統計分析,不過這樣⼀來會發現我們的統計成本就會⼤⼤增 加,⽽且結果分析也會變得不那麼靈活。是以這⾥推薦⼀種簡單的監控⽅式,那就是
友盟的 U-APM 應能性 能監控SDK,隻需要我們進⾏簡單的pod內建之後,便可根據我們的實際需要進⾏⼿動或者⾃動監控啟動數 據,詳情可以參考
U-APM, 并且為了⽅便我們對資料進⾏分析,友盟背景已經根據這些資料幫我們繪制出 了對應的分布圖,我們可以⼀⽬了然的得出啟動耗時分布、啟動類型占⽐等等,如圖:
除此之外,我們還可以通過SDK進⾏崩潰分析、 ANR分析、監控告警、卡頓分析、記憶體分析等等諸多功能, 有了
U-APM 這個監控平台,其實在實際開發過程中很⼤程度的提升了我們對線上 app 的優化分析效率。
當然本⽂的介紹也隻是⽐較淺顯的優化項,僅供參考以及思路引導,優化之路任重⽽道遠,還需要我們不斷 的去探索、發現、提⾼。不過最後還是要提醒⼀句:在實際項⽬開發過程中,不要為了優化⽽優化,要根據 項⽬情況有針對性的進⾏優化。
參考:
探秘 Mach-O ⽂件 iOS底層 - 從頭梳理 dyld 加載流程 iOSapp啟動 - dyld加載App流程wwdc2016optimizingappstartuptime.pdf
作者:武玉寶