前言:本文簡單描述APP啟動過程和監控,一些深入原理性的東西可能需要繞路了,站在大神的肩膀上,簡單總結跟APP啟動性能有關,如果差錯請不吝賜教
冷啟動
相對而言冷啟動就是App被kill掉以后一切從頭開始啟動的過程。
App 點擊啟動前,它的進程不在系統里,需要系統新創建一個進程分配給它啟動的情況。這是一次完整的啟動過程。
用戶感知到的啟動慢,其實都發生在主線程上。而主線程慢的原因有很多,比如在主線程上執行了大文件讀寫操作、在渲染周期中執行了大量計算等。
熱啟動
當用戶按下home鍵的時候,iOS的App并不會馬上被kill掉,還會繼續存活若干時間。用戶點擊App的圖標再次回來的時候,App幾乎不需要做什么,就可以還原到退出前的狀態,繼續為用戶服務。App 在冷啟動后用戶將 App 退后臺,在 App 的進程還在系統里的情況下,用戶重新啟動進入 App 的過程。這種持續存活的情況下啟動App,稱為熱啟動。
查看APP啟動耗時
根據APP啟動時間,繼續了解APP啟動時候都做了哪些
Xcode:(快捷鍵:command + shift + <
)
Project
→ Scheme
→ Edit Scheme
→ Run
→ Environment Variables
添加 DYLD_PRINT_STATISTICS
環境變量,value為1
APP 啟動時間:
Total pre-main time: 802.16 milliseconds (100.0%)
dylib loading time: 294.37 milliseconds (36.6%)
rebase/binding time: 377.42 milliseconds (47.0%)
ObjC setup time: 86.68 milliseconds (10.8%)
initializer time: 43.51 milliseconds (5.4%)
slowest intializers :
libSystem.B.dylib : 4.20 milliseconds (0.5%)
libMainThreadChecker.dylib : 21.22 milliseconds (2.6%)
時間消耗解讀
- main()函數之前總共使用了802.16ms
- 加載動態庫占用36.6%
- 指針重定位占用47.6%
- ObjC類初始化占用10.8%
- 各種初始化使用了5.4%。
- initializer time中最耗時的是libSystem.B.dylib、libBacktraceRecording.dylib。
換言之:
App開始啟動后,系統首先加載可執行文件(自身App的所有.o文件的集合),然后加載動態鏈接器dyld,dyld是一個專門用來加載動態鏈接庫的庫。 執行從dyld開始,dyld從可執行文件的依賴開始, 遞歸加載所有的依賴動態鏈接庫。
動態鏈接庫包括:iOS 中用到的所有系統 framework,加載OC runtime方法的libobjc,系統級別的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。
APP啟動階段
啟動時間:用戶點擊APP → APP首頁面加載完成
- 階段1:main() 函數執行前
- 階段2:main() 函數執行后
- 階段3:首屏渲染完成后
main()函數執行前
在 main() 函數執行前,系統主要會做下面幾件事情:
- 【解析Info.plist】:加載信息,例如閃屏;沙盒建立、權限檢查
- 【Mach-O加載】:加載所有依賴的Mach-O文件(遞歸調用Mach-O加載的方法);加載可執行文件(App 的.o 文件的集合)
- 【加載動態鏈接庫】:進行 rebase 指針調整和 bind 符號綁定;定位內部、外部指針引用,例如字符串、函數等
- 【Objc 運行時的初始處理】:包括 Objc 相關類的注冊、category 注冊、selector 唯一性檢查等
- 【初始化】:執行 +load() 方法,執行聲明為attribute((constructor))的C函數,C++靜態對象加載
程序執行
- 調用
main()
- 調用
UIApplicationMain()
- 調用
applicationWillFinishLaunching
可優化的功能點
- 【減少動態庫加載】:每個庫本身都有依賴關系,使用更少的動態庫,并且建議在使用動態庫的數量較多時,盡量將多個動態庫進行合并。最多可以支持 6 個非系統動態庫合并為一個。
- 【減少+load方法】:方法里的內容可以放到首屏渲染完成后再執行,或使用 +initialize() 方法替換掉。在一個 +load() 方法里,進行運行時方法替換操作會帶來 4 毫秒的消耗。
- 【減少使用】:減少寫attribute((constructor))的C函數,控制 C++ 全局變量的數量;
main()函數之后
從main()
函數開始至 appDelegate
的
didFinishLaunchingWithOptions
結束,稱為main()函數之后的部分。
主要執行內容
- 首屏初始化所需配置文件的讀寫操作;
- 首屏列表大數據的讀??;
- 首屏渲染的大量計算等。
main()函數之后耗時的影響因素
- 執行
main()
函數的耗時 - 執行
applicationWillFinishLaunching
的耗時 -
rootViewController
及其childViewController
的加載、view
及其subviews
的加載
首屏渲染完成之后
[首屏渲染完成之后]指的是非首屏其他業務服務模塊的初始化、監聽的注冊、配置文件的讀取等。
該階段指的就是截止到 didFinishLaunchingWithOptions
方法作用域內執行首屏渲染之后的所有方法執行完成。從渲染完成時開始,到 didFinishLaunchingWithOptions
方法作用域結束時結束。
優化思路一:功能啟動優化
main() 函數開始執行后到首屏渲染完成前只處理首屏相關的業務,其他非首屏業務的初始化、監聽注冊、配置文件讀取等都放到首屏渲染完成后去做。
根據剛需分置階段進行
根據啟動流程把剛需功能放置在啟動階段,其他業務功能放在合適的階段
- 首屏渲染必要的初始化功能
- App 啟動必要的初始化功能
- 只需要在對應功能開始使用時才需要初始化的功能
- 例如:主視圖第一時間加載,里面的數據和界面延后加載
優化思路二:方法啟動優化
檢查首屏渲染完成前主線程上的耗時方法,將非剛需的耗時方法滯后或異步執行。耗時較長的方法主要發生在計算大量數據的情況下,例如加載、編輯、存儲圖片和文件等資源。
+load() 方法,一個耗時 4 毫秒,100 個就是 400 毫秒,不可小視
優化三:移除不必要的動態庫
移除項目中非必要的動態庫
優化四:移除不必要用到的類
代碼工程的維護非常重要
優化五:合并功能相似的類和擴展(Category)
由于Category的實現原理,和ObjC的動態綁定有很強的關系,實際上類的擴展是比較占用啟動時間的。盡量可能合并一些擴展,并不是讓你不使用擴展
優化六:壓縮資源圖片
圖片小了,IO操作量小了,啟動就快了。推薦 TinyPNG
優化七:優化applicationWillFinishLaunching
需要在applicationWillFinishLaunching
里處理的業務較多時,可以管理起這些任務
將不需要馬上在applicationWillFinishLaunching執行的代碼延后執行
優化八:優化rootViewController
rootViewController的加載,適當將某一級的childViewController或subviews延后加載
如果你的App可能會被后臺拉起并冷啟動,可考慮不加載rootViewController
優化九:小優化
- 不使用xib,直接視用代碼加載首頁視圖
- NSUserDefaults實際上是在Library文件夾下會生產一個plist文件,如果文件太大的話一次能讀取到內存中可能很耗時,如果耗時很大的話需要拆分(需考慮老版本覆蓋安裝兼容問題)
- 每次用NSLog方式打印會隱式的創建一個Calendar,因此需要刪減啟動時各業務方打的log,或者僅僅針對內測版輸出log
- 梳理應用啟動時發送的所有網絡請求,是否可以統一在異步線程請求
Debug 打印代碼塊
//MARK: - DEBUG print
func printLog<T>(msg: T,
file: String = #file,
method: String = #function,
line: Int = #line){
if !DEBUG_ALPHA{//線上環境不print
return
}
print("\((file as NSString).lastPathComponent) \(method),[\(line)]: \(msg)")
}
APP監控方法一:計算主線程方法耗時
定時抓取主線程上的方法調用對戰,計算在一段時間內各個方法的耗時。Xcode 工具套件里自帶的 Time Profiler,開發類似工具成本不高,能夠快速開發后集成到你的 App 中,以便在真實環境中進行檢查。
對于定時時間間隔的控制
- 間隔長:會漏掉某些方法,從而導致檢查出來的耗時不精確
- 間隔短:抓取堆棧這個方法本身調用過多會影響整體耗時,導致結果不準確
- 定時抓取主線程調用棧的方式精準度不夠高,做參考足以
- 大神得出最合適時間為 0.01 秒,雖然導致許多運行速度快的方法監控誤差,對整體耗時影響小
APP監控方法二:objc_msgSend 方法 hook 所有方法耗時
Hook:在原方法開始執行時換成執行其他指定的方法,或者在原有方法執行前后執行你指定的方法,來達到掌握和改變指定方法的目的。
- 優點:非常精確
- 缺點:只能對Objective-C方法,對c方法和block需要借助第三方框架處理,編寫維護成本高
Objective-C 里每個對象都會指向一個類,每個類都會有一個方法列表,方法列表里的每個方法都是由 selector、函數指針和 metadata 組成的。
objc_msgSend
就是在運行時根據對象和方法的 selector
去找到對應的函數指針,然后執行。objc_msgSend
是 Objective-C
里方法執行的必經之路,能夠控制所有的 Objective-C
的方法,可自行閱讀objc_msgSend
源碼獲得更深層次的理解。
objc_msgSend
用匯編語言寫的:
-
objc_msgSend
的調用頻次最高,在它上面進行的性能優化能夠提升整個 App 生命周期的性能。匯編語言在性能優化上屬于原子級優化,能夠把優化做到極致。 - 其他語言難以實現未知參數跳轉到任意函數指針的功能。
objc_msgSend
方法執行邏輯
先獲取對象對應類的信息,再獲取方法的緩存,根據方法的 selector 查找函數指針,經過異常錯誤處理后,最后跳到對應函數的實現
Tips
Swift AppDelegate 沒有main函數入口了
錯,swift有main函數,被精簡成了一個@NSApplicationMain
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
檢查方法耗時的工具
推薦大神神作 SMCallTrace
友情提示:需要在SMCallTrace.m中打開第54行的注釋。
+load為什么會增加4毫秒,Swift呢
aop 的耗時,swift沒有
iOS中用llvm的IR中插樁來統計函數耗時這個方法也可行
有待學習,嚶嚶嚶
objc的hook是用method swizzle來實現,對于swift
使用Time Profiler 或者 使用Clang打樁統計耗時
oc的代碼在編譯時會轉成c++,再轉成c,那swift如何轉換?
swift 和 c 編譯方式類似。Swift 會先編成 SIL( Swift Intermediate Language)然后再編成機器碼。
未完待續…
【干貨推薦】
iOS啟動時間優化
iOS App 啟動性能優化
今日頭條iOS客戶端啟動速度優化
優化 App 的啟動時間
《How we cut our iOS app’s launch time in half (with this one cool trick)》
匯編相關
https://blog.nelhage.com/2010/10/amd64-and-va_arg/
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf
https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html
博客、課程、開源 推薦 戴神
小結:一枚突然頓悟的小白兔,學無止境