iOS學習
- APP的啟動流程
- 啟動
- 流程
- 缺頁錯誤
- 主要階段
- pre-main階段
- main階段
- 啟動優化
- pre-main
- main階段
- 啟動優化總結
- 流程總結
APP的啟動流程
啟動
首先我們來了解啟動的概念:
- 廣義上的啟動是點擊圖標到首頁數據加載完畢
- 狹義上的啟動是點擊圖標到啟動圖完全消失的第一幀
啟動的最佳時間是400ms以內,因為從點擊圖標到顯示Launch Screen,到Launch Screen消失這段時間是400ms。啟動時間不可以大于20s,否則會被系統殺掉
啟動的計算方式如下:
- 起點:進程創建的時間
- 終點:第一個
CA::Transaction::commit()
啟動分為冷啟動和熱啟動:
冷啟動:系統里沒有任何進程的緩存信息,典型的是重啟手機后直接啟動 App
熱啟動:從后臺再次進入App時,這次啟動就是熱啟動,因為進程緩存還在
那么我們使用app時,哪個啟動更多呢?
這實際上要看產品的形態,打開的頻次越高,熱啟動比例越高,很好理解。
流程
我們先看圖:
以下是APP啟動的底層細節鏈
點擊APP啟動 -> 加載libSystem -> Runtime注冊回調函數 -> 加載image(鏡像文件) -> 執行map_images和load_images方法 -> 調用main函數。
而其中打開app直到調用main函數之前,就是上圖的dyld工作階段,即pre-main階段的一部分。
-
進程創建與環境準備(內核 + SpringBoard)
-
創建進程、啟用 ASLR、加載簽名與沙箱、校驗權限與 Entitlements
-
解析 Info.plist 的關鍵鍵(Bundle 標識、支持架構/平臺、啟動場景配置等)
-
系統根據 LaunchScreen.storyboard/舊版 LaunchImage 在“你的代碼尚未執行”時顯示啟動畫面(閃屏由系統渲染,非你主動繪制)
-
-
dyld 加載與 Mach-O 修復
-
加載可執行文件及其依賴的動態庫(如 libSystem.B.dylib、libobjc.A.dylib、Swift runtime 等)
-
Rebase:把鏡像內的指針按 ASLR 偏移修正到“真實地址”
-
Bind:為外部符號(如 NSLog)在運行時綁定到實際實現地址
-
(dyld3/共享緩存會顯著縮短 dylib 加載與綁定時間)
-
運行時初始化與預執行階段(均在 main 之前)
-
ObjC Runtime 初始化:注冊類、元類、選擇子、協議,處理 Category
-
調度并執行 Objective?C +load(父類→子類,類→分類;各類/分類各執行一次)
-
執行 C/C++ 全局構造與帶 __ attribute__((constructor)) 的函數(均為“進程初始化時機”,具體與 +load 的跨語言相對順序不應依賴)
-
-
進入程序主入口與應用生命周期
-
調用 main() → UIApplicationMain(…)
-
創建 UIApplication 實例,建立主 RunLoop
-
讀取 Info.plist 中的主界面/場景配置:iOS 13+ 走 UIScene,舊版讀取 UIApplicationMainStoryboardFile
-
AppDelegate 回調:
-
application:willFinishLaunchingWithOptions:(可選)
-
application:didFinishLaunchingWithOptions:(常用,創建 UIWindow、設 rootVC、makeKeyAndVisible())
-
-
首個 UIViewController 生命周期:loadView → viewDidLoad → viewWillAppear → 動畫 → viewDidAppear
-
系統移除啟動畫面,展示首屏
-
rebase(偏移修正):任何一個app生成的二進制文件,在二進制文件內部所有的方法、函數調用,都有一個地址,這個地址是在當前二進制文件中的偏移地址。一旦在運行時刻(即運行到內存中),每次系統都會隨機分配一個ASLR(Address Space Layout Randomization,地址空間布局隨機化)地址值(是一個安全機制,會分配一個隨機的數值,插入在二進制文件的開頭),例如,二進制文件中有一個 test方法,偏移值是0x0001,而隨機分配的ASLR是0x1f00,如果想訪問test方法,其內存地址(即真實地址)變為 ASLR+偏移值 = 運行時確定的內存地址(即0x1f00+0x0001 = 0x1f01)。程序每次啟動后地址都會隨機變化,這樣程序里所有的代碼地址都需要需要重新對進行計算修復才能正常訪問。rebasing這一步主要就是調整鏡像內部指針的指向。
binding(綁定):,例如NSLog方法,在編譯時期生成的mach-o文件中,會創建一個符號!NSLog(目前指向一個隨機的地址),然后在運行時(從磁盤加載到內存中,是一個鏡像文件),會將真正的地址給符號(即在內存中將地址與符號進行綁定,是dyld做的,也稱為動態庫符號綁定),一句話概括:綁定就是給符號賦值的過程
缺頁錯誤
程序運能運行時因為存在物理內存,也就是說程序加入到物理內存中才得以運行,這一步就是虛擬內存映射到物理內存。這個過程是個使用懶加載方式完成系統到CPU的交互(翻譯)的過程。
而這個過程因為懶加載映射方式的緣故,它是“有多少拿多少”,所以我們會通過一頁一頁的方式也就是page的方式去加載的,iOS的頁的大小是16kb,而macOS是4kb。
也是因為是懶加載的方式,所以如果需要用到的時候發現物理內存中沒有,就會報出“page fault”的缺頁錯誤,然后缺的頁會再加載放入物理內存。這個過程很短,可能30ms,也可能是10ms。
主要階段
主要分為兩個階段:pre-main階段和main階段
pre-main階段:程序啟動到main函數執行前
main階段:在執行main函數后,調用AppDelegate中的-application:didFinishLaunchingWithOptions:
方法完成初始化,并展示首頁
pre-main階段
pre-main階段做的事情與dyld的版本有關,此處以dyld2為例。
- 加載應用的可執行文件。
- 加載動態鏈接庫加載器dyld(dynamic loader)。
- dyld遞歸加載應用所有依賴的dylib(dynamic library 動態鏈接庫)。
- 進行**
rebase
指針調整和bind
**符號綁定。 ObjC
的runtime
初始化(ObjC setup):ObjC
相關Class
的注冊、category
注冊、selector
唯一性檢查等。- 初始化(Initializers):執行
+load()
方法、用attribute((constructor))
修飾的函數的調用、創建C++
靜態全局變量等。
dyld流程概述:
我們看這個流程是為了看APP啟動到main函數前,也就是dyld是如何將images(鏡像文件:如動靜態庫等)鏈接到內存中去的。而在objc_init的時候是做了什么操作去調起dyld,以及dyld又如何回調至objc中。
加載鏈接庫
從主執行文件的 header
獲取到需要加載的所依賴動態庫列表,而 header
早就被內核映射過。然后它需要找到每個 dylib
,然后打開文件讀取文件起始位置,確保它是 Mach-O
文件。接著會找到代碼簽名并將其注冊到內核。然后在 dylib
文件的每個 segment
上調用 mmap()
。應用所依賴的 dylib
文件可能會再依賴其他 dylib
,所以 dyld
所需要加載的是動態庫列表一個遞歸依賴的集合。一般應用會加載 100
到 400
個 dylib
文件,但大部分都是系統 dylib
,它們會被預先計算和緩存起來,加載速度很快。
修正
在加載所有的動態鏈接庫之后,它們只是處在相互獨立的狀態,需要將它們綁定起來,這就是 Fix-ups
。代碼簽名使得我們不能修改指令,那樣就不能讓一個 dylib
的調用另一個 dylib
。這時需要加很多間接層。 現代 code-gen
被叫做動態 PIC(Position Independent Code),意味著代碼可以被加載到間接的地址上。當調用發生時,code-gen
實際上會在 __DATA
段中創建一個指向被調用者的指針,然后加載指針并跳轉過去。所以 dyld
做的事情就是修正(fix-up
)指針和數據。Fix-up
有兩種類型,rebasing
和 binding
在執行main函數之前,需要把類的信息注冊到一個全局的Table中。同時,Objective C支持Category,在初始化的時候,也會把Category中的方法注冊到對應的類中,同時會唯一Selector,這也是為什么當你的Cagegory實現了類中同名的方法后,類中的方法會被覆蓋。
另外,由于iOS開發時基于Cocoa Touch的,所以絕大多數的類起始都是系統類,所以大多數的Runtime初始化起始在Rebase和Bind中已經完成。
初始化
調用+load方法和C/C++靜態初始化對象和標記為 __ attribute__(constructor)的方法
到此結束dyld2的流程
我們再來看一下dyld2與dyld3的區別
dyld2是純粹的in-process,也就是在程序進程內執行的,也就意味著只有當應用程序被啟動的時候,dyld2才能開始執行任務。
dyld3則是部分out-of-process,部分in-process。圖中,虛線之上的部分是out-of-process的,在App下載安裝和版本更新的時候會去執行,out-of-process會做如下事情:
- 分析Mach-o Headers
- 分析依賴的動態庫
- 查找需要Rebase & Bind之類的符號
- 把上述結果寫入緩存
此時,在進程內就只需要讀取這個closure(閉包)直接從緩存中讀取數據,大大減少了加載時間。
dyld是在是在dyld::_main
函數中調用的
main階段
- 執行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions 初始化Window
- 初始化基礎的ViewController結構(一般是UINavigationController+UITabViewController) 獲取數據(Local DB/Network),展示給用戶。
啟動優化
了解app啟動流程和主要階段,我們就可以來進行啟動的優化了。
pre-main
在pre-main階段,這個階段最主要的就是dyld的加載。
而dylibs啟動的第一步就是加載動態庫,加載系統的動態庫使很快的,因為可以緩存,而加載內嵌的動態庫速度較慢。所以,提高這一步的效率的關鍵是:減少動態庫的數量。
我們還可以考慮合并動態庫,但是個人開發就不建議了
Rebase & Bind & Objective C Runtime
Rebase和Bind都是為了解決指針引用的問題。對于Objective C開發來說,主要的時間消耗在Class/Method的符號加載上,所以常見的優化方案是:
- 減少__DATA段中的指針數量。
- 合并Category和功能類似的類。比如:UIView+Frame,UIView+AutoLayout…合并為一個
- 刪除無用的方法和類。
- 用initialize替代load
- 減少__atribute__((constructor))的使用,而是在第一次訪問的時候才用dispatch_once等方式初始化。
- 不要創建線程
main階段
延遲初始化那些不必要的UIViewController
。
能延遲執行的就延遲執行。比如SDK的初始化,界面的創建。 不能延遲執行的,盡量放到后臺執行。比如數據讀取,原始JSON數據轉對象,日志發送。
啟動優化總結
-
pre?main(dyld/Runtime)
-
減少/合并第三方與自家動態庫數量;能靜態鏈就靜態鏈(SPM/靜態 XCFramework)。
-
避免在 +load / 構造器做耗時:移至懶加載(首次使用)或 didFinishLaunching 后的異步。
-
精簡 ObjC 元數據:刪除無用類/方法/Category,合并零碎 Category(權衡維護性)。
-
減少符號/指針膨脹:避免過度 @objc 暴露與反射;Swift 盡量 final/struct,降低動態性。
-
盡量避免啟動期方法交換(swizzle);必須 swizzle 的延后到需要的子系統啟用時。
-
-
main 之后(App/Scene)
-
輕量化首屏:小根 VC,延遲創建次級控制器與大型視圖樹。
-
避免主線程同步 I/O/網絡/大 JSON 解析;放后臺隊列,首幀后再做。
- SDK 延遲初始化;按需加載功能模塊。
-
使用 Auto Layout 時約束閉環、減少首幀布局抖動;首屏資源(字體/圖片)盡量小并就近。
-
流程總結
從用戶點擊 App 圖標開始,系統先創建進程,按 Info.plist 做基礎配置校驗,建立沙箱,并用 LaunchScreen.storyboard 顯示啟動畫面。接著進入 dyld 階段:把可執行文件和依賴的動態庫從共享緩存加載進來,做 ASLR 的 rebase 和符號 binding。隨后運行時初始化:Objective?C/Swift Runtime 注冊類與分類,執行 C/C++ 全局構造器和 ObjC 的 +load(這些都在 main 之前完成)。
然后進入 main,調用 UIApplicationMain 創建 UIApplication,啟動主 RunLoop。iOS 13 及以后通常走 UIScene:在 scene:willConnectTo: 里配置 window 和 rootViewController;老版本是在 application:didFinishLaunching 里創建 window 并 makeKeyAndVisible。接著首個控制器會依次觸發 loadView、viewDidLoad、viewWillAppear,動畫結束后 viewDidAppear,系統移除啟動圖,首幀呈現給用戶。