iOS應用的啟動流程和優化詳解
一、應用啟動流程
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
載入動態庫,這個過程中,會去裝載app使用的動態庫,而每一個動態庫有它自己的依賴關係,所以會消耗時間去查詢和讀取。對於Apple提供的的系統動態庫,做了高度的優化。而對於開發者定義匯入的動態庫,則需要在花費更多的時間。Apple官方建議儘量少的使用自定義的動態庫,或者考慮合併多個動態庫,其中一個建議是當大於6個的時候,則需要考慮合併它們。(2)Rebase/Binding 減少App的Objective-C類,分類和Selector的個數。這樣做主要是為了加快程式的整個動態連結, 在進行動態庫的重定位和繫結(Rebase/binding)過程中減少指標修正的使用,加快程式機器碼的生成; (3)Objc setup
大部分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
方法主要用來對一些不方便在編譯期初始化的物件進行賦值,或者說對一些靜態常量進行初始化操作。
參考連結: https://www.jianshu.com/p/f41bf869809f https://www.jianshu.com/p/c9406eff7b89 https://www.jianshu.com/p/7a5610d5802f https://www.cnblogs.com/cleven/p/12796608.html ------------------越是喧囂的世界,越需要寧靜的思考------------------ 合抱之木,生於毫末;九層之臺,起於壘土;千里之行,始於足下。 積土成山,風雨興焉;積水成淵,蛟龍生焉;積善成德,而神明自得,聖心備焉。故不積跬步,無以至千里;不積小流,無以成江海。騏驥一躍,不能十步;駑馬十駕,功在不捨。鍥而舍之,朽木不折;鍥而不捨,金石可鏤。蚓無爪牙之利,筋骨之強,上食埃土,下飲黃泉,用心一也。蟹六跪而二螯,非蛇鱔之穴無可寄託者,用心躁也。