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符號綁定。
  • ObjCruntime初始化(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方法)。
  • 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中出現的順序(先編譯,則先調用,列表中在下方的為“先”)

(5)+load方法是函數指針調用,即遍歷類中的方法列表,直接根據函數地址調用。如果子類沒有實現+load方法,子類也不會自動調用父類的+load方法。


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)相同點

  1. load和initialize會被自動調用,不能手動調用它們。
  2. 子類實現了load和initialize的話,會隱式調用父類的load和initialize方法。
  3. load和initialize方法內部使用了鎖,因此它們是線程安全的。

(2)不同點

  1. 調用順序不同,以main函數為分界,+load方法在main函數之前執行,+initialize在main函數之后執行。
  2. 子類中沒有實現+load方法的話,子類不會調用父類的+load方法;而子類如果沒有實現+initialize方法的話,也會自動調用父類的+initialize方法。
  3. +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 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM