一、應用啟動流程
1、整體過程
(1)解析Info.plist
- 加載相關信息,例如如閃屏
- 沙箱建立、權限檢查
(2)Mach-O(可執行文件)加載
- 如果是胖二進制文件(為了保持向下兼容,且支持舊有設備及舊有指令集),尋找合適當前CPU類別的部分
- 加載所有依賴的Mach-O文件(遞歸調用Mach-O加載的方法)
- 定位內部、外部指針引用,例如字符串、函數等
- 加載類擴展(Category)中的方法
- C++靜態對象加載、調用ObjC的 +load 函數
- 執行聲明為__attribute__((constructor))的C函數
(3)程序執行
- 調用main()
- 調用UIApplicationMain()
- 調用applicationWillFinishLaunching
2、主要階段:
分為兩個階段,pre-main階段和main()階段。程序啟動到main函數執行前是pre-main階段;在執行main函數后,調用AppDelegate中的-application:didFinishLaunchingWithOptions:
方法完成初始化,並展示首頁,這是main()階段,或者叫做main()之后階段。
(1)pre-main階段:
- 加載應用的可執行文件。
- 加載動態鏈接庫加載器dyld(dynamic loader)。
- dyld遞歸加載應用所有依賴的dylib(dynamic library 動態鏈接庫)。
- 進行
rebase
指針調整和bind
符號綁定。 ObjC
的runtime
初始化(ObjC setup):ObjC
相關Class
的注冊、category
注冊、selector
唯一性檢查等。- 初始化(Initializers):執行
+load()
方法、用attribute((constructor))
修飾的函數的調用、創建C++
靜態全局變量等。
(2)main()階段:
- dyld調用main()
- 調用UIApplicationMain()
- 調用applicationWillFinishLaunching
- 調用didFinishLaunchingWithOptions
二、獲取啟動流程的時間消耗
1、pre-main階段
對於pre-main階段,Apple提供了一種測量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 將環境變量DYLD_PRINT_STATISTICS 設為1 。之后控制台會輸出類似內容,我們可以清晰的看到每個耗時:
從上面可以看出時間區域主要分為下面幾個部分:
- dylib loading time
- 動態庫載入過程,會去裝載app使用的動態庫,而每一個動態庫有它自己的依賴關系,所以會消耗時間去查找和讀取。
- dyld (the dynamic link editor)動態鏈接器,是一個專門用來加載動態鏈接庫的庫,它是開源的。在 xnu 內核為程序啟動做好准備后,執行由內核態切換到用戶態,由dyld完成后面的加載工作,dyld的主要是初始化運行環境,開啟緩存策略,加載程序依賴的動態庫(其中也包含我們的可執行文件),並對這些庫進行鏈接(主要是rebaseing和binding),最后調用每個依賴庫的初始化方法,在這一步,runtime被初始化。
- rebase/binding time
ASLR(Address Space Layout Randomization),地址空間布局隨機化。在ASLR技術出現之前,程序都是在固定的地址加載的,這樣hacker可以知道程序里面某個函數的具體地址,植入某些惡意代碼,修改函數的地址等,帶來了很多的危險性。ASLR就是為了解決這個的,程序每次啟動后地址都會隨機變化,這樣程序里所有的代碼地址都需要需要重新對進行計算修復才能正常訪問。rebasing這一步主要就是調整鏡像內部指針的指向。
Binding:將指針指向鏡像外部的內容。
- ObjC setup time
- dyld調用的
objc_init
方法,這個是runtime的初始化方法,在這個方法里面主要的操作就是加載類(對需要的class和category進行注冊); - objc_init方法通過內部的_dyld_objc_notify_register向dyld注冊了一個通知事件,當有新的image(程序中對應實例可簡稱為image,如程序可執行文件macho,Framework,bundle等)加載到內存的時候,就會觸發
load_images
方法,這個方法里面就是加載對應image里面的類,並調用load
方法(在下一階段initializer)。 - 如果有繼承的類,那么會先調用父類的
load
方法,然后調用子類的,但是在load
里面不能調用[super load]
。最后才是調用category的load
方法。總之,所有的load
都會被調用到(注意:子類的initialize方法會覆蓋父類,不同於load方法)。
- dyld調用的
- initializer time
承接上一過程進行初始化(load)。如果我們代碼里面使用了clang的__attribute__((constructor))
構造方法,這里會調用到。
2、main()階段
測量main()函數開始執行到didFinishLaunchingWithOptions執行結束的時間,簡單的方法:直接插入代碼。(也可以使用其他工具)
- main函數里
- 到主UI框架的.m文件用extern聲明全局變量StartTime
- 在viewDidAppear函數里,再獲取一下當前時間,與StartTime的差值即是main()階段運行耗時。
三、改善APP的啟動
建議應用的啟動時間控制在400ms之下,並且在20s內啟動,否則系統會kill app。優化APP的啟動時間,需要就是分別優化pre-main和main的時間。
1、改善啟動時pre-main階段
(1)加載 Dylib
(2)Rebase/Binding
大部分ObjC初始化工作已經在Rebase/Bind階段做完了,這一步dyld會注冊所有聲明過的ObjC類,將分類插入到類的方法列表里,再檢查每個selector的唯一性。
在這一步倒沒什么優化可做的,Rebase/Bind階段優化好了,這一步的耗時也會減少。
(4)Initializers
到了這一階段,dyld開始運行程序的初始化函數,調用每個Objc類和分類的+load方法,調用C/C++ 中的構造器函數(用attribute((constructor))修飾的函數),和創建非基本類型的C++靜態全局變量。Initializers階段執行完后,dyld開始調用main()函數。
在這一步,我們可以做的優化有:
-
- 少在類的+load方法里做事情,盡量把這些事情推遲到+initiailize
- 減少構造器函數個數,在構造器函數里少做些事情
- 減少C++靜態全局變量的個數
2、main()階段的優化
(1)核心點:didFinishLaunchingWithOptions方法
這一階段的優化主要是減少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里我們經常會進行:
- 創建應用的window,指定其rootViewController,調用window的makeKeyAndVisible方法讓其可見;
- 由於業務需要,我們會初始化各個三方庫;
- 設置系統UI風格;
- 檢查是否需要顯示引導頁、是否需要登錄、是否有新版本等;
由於歷史原因,這里的代碼容易變得比較龐大,啟動耗時難以控制。
(2)優化點:
滿足業務需要的前提下,didFinishLaunchingWithOptions在主線程里做的事情越少越好。在這一步,我們可以做的優化有:
- 梳理各個二方/三方庫,把可以延遲加載的庫做延遲加載處理,比如放到首頁控制器的viewDidAppear方法里。
- 梳理業務邏輯,把可以延遲執行的邏輯做延遲執行處理。比如檢查新版本、注冊推送通知等邏輯。
- 避免復雜/多余的計算。
- 避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情,這2個方法執行完,首頁控制器才能顯示,部分可以延遲創建的視圖應做延遲創建/懶加載處理。
- 首頁控制器用純代碼方式來構建。
四、+load與+initialize
1、+load
(1)+load
方法是一定會在runtime中被調用的。只要類被添加到runtime中了,就會調用+load
方法,即只要是在Compile Sources
中出現的文件總是會被裝載,與這個類是否被用到無關,因此+load
方法總是在main函數之前調用。
(2)+load
方法不會覆蓋。也就是說,如果子類實現了+load
方法,那么會先調用父類的+load
方法(無需手動調用super),然后又去執行子類的+load
方法。
(3)+load方法只會調用一次。
(4)+load方法執行順序是:類 -> 子類 ->分類。而不同分類之間的執行順序不一定,依據在Compile Sources
中出現的順序(先編譯,則先調用,列表中在下方的為“先”)。
2、+initialize
(1)+initialize
方法是在類或它的子類收到第一條消息之前被調用的,這里所指的消息包括實例方法和類方法的調用。因此+initialize
方法總是在main函數之后調用。
(2)+initialize
方法只會調用一次。
(3)+initialize
方法實際上是一種惰性調用,如果一個類一直沒被用到,那它的+initialize
方法也不會被調用,這一點有利於節約資源。
(4)+initialize
方法會覆蓋。如果子類實現了+initialize
方法,就不會執行父類的了,直接執行子類本身的。如果分類實現了+initialize
方法,也不會再執行主類的。
(5)+initialize
方法的執行覆蓋順序是:分類 -> 子類 ->類。且只會有一個+initialize
方法被執行。
(6)+initialize
方法是發送消息(objc_msgSend()),如果子類沒有實現+initialize
方法,也會自動調用其父類的+initialize
方法。
3、兩者的異同
(1)相同點
- load和initialize會被自動調用,不能手動調用它們。
- 子類實現了load和initialize的話,會隱式調用父類的load和initialize方法。
- load和initialize方法內部使用了鎖,因此它們是線程安全的。
(2)不同點
- 調用順序不同,以main函數為分界,
+load
方法在main函數之前執行,+initialize
在main函數之后執行。 - 子類中沒有實現
+load
方法的話,子類不會調用父類的+load
方法;而子類如果沒有實現+initialize
方法的話,也會自動調用父類的+initialize
方法。 +load
方法是在類被裝在進來的時候就會調用,+initialize
在第一次給某個類發送消息時調用(比如實例化一個對象),並且只會調用一次,是懶加載模式,如果這個類一直沒有使用,就不回調用到+initialize
方法。
4、使用場景
(1)+load
一般是用來交換方法Method Swizzle
,由於它是線程安全的,而且一定會調用且只會調用一次,通常在使用UrlRouter的時候注冊類的時候也在+load
方法中注冊。(2)+initialize
方法主要用來對一些不方便在編譯期初始化的對象進行賦值,或者說對一些靜態常量進行初始化操作。
參考鏈接: