iOS客戶端啓動速度優化實踐

應用啓動時間,直接影響用戶對一款應用的判斷和使用體驗。頭條主app自己就包含很是多而且複雜度高的業務模塊(如新聞、視頻等),也接入了不少第三方的插件,這勢必會拖慢應用的啓動時間,本着精益求精的態度和對用戶體驗的追求,咱們但願在業務擴張的同時最大程度的優化啓動時間。html

技術調研ios

先說結論:面試

t(App總啓動時間) = t1(main()以前的加載時間) + t2(main()以後的加載時間)。

t1 = 系統dylib(動態連接庫)和自身App可執行文件的加載;swift

t2 = main方法執行以後到AppDelegate類中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法執行結束前這段時間,主要是構建第一個界面,並完成渲染展現。xcode

main()調用以前的加載過程緩存

App開始啓動後, 系統首先加載可執行文件(自身App的全部.o文件的集合),而後加載動態連接庫dyld,dyld是一個專門用來加載動態連接庫的庫。 執行從dyld開始,dyld從可執行文件的依賴開始, 遞歸加載全部的依賴動態連接庫。網絡

動態連接庫包括:iOS 中用到的全部系統 framework,加載OC runtime方法的libobjc,系統級別的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。app

其實不管對於系統的動態連接庫仍是對於App自己的可執行文件而言,他們都算是image(鏡像),而每一個App都是以image(鏡像)爲單位進行加載的,那麼image究竟包括哪些呢?dom

什麼是image異步

  1. executable可執行文件 好比.o文件。
  2. dylib 動態連接庫 framework就是動態連接庫和相應資源包含在一塊兒的一個文件夾結構。
  3. bundle 資源文件 只能用dlopen加載,不推薦使用這種方式加載。

除了咱們App自己的可行性文件,系統中全部的framework好比UIKit、Foundation等都是以動態連接庫的方式集成進App中的。

iOS開發交流技術羣:563513413,無論你是大牛仍是小白都歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 你們一塊兒交流學習成長!

系統使用動態連接有幾點好處

代碼共用:不少程序都動態連接了這些 lib,但它們在內存和磁盤中中只有一份。

易於維護:因爲被依賴的 lib 是程序執行時才連接的,因此這些 lib 很容易作更新,好比libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升級直接換成libSystem.C.dylib 而後再替換替身就好了。

減小可執行文件體積:相比靜態連接,動態連接在編譯時不須要打進去,因此可執行文件的體積要小不少。

如上圖所示,不一樣進程之間共用系統dylib的_TEXT區,可是各自維護對應的_DATA區。

全部動態連接庫和咱們App中的靜態庫.a和全部類文件編譯後的.o文件最終都是由dyld(the dynamic link editor),Apple的動態連接器來加載到內存中。每一個image都是由一個叫作ImageLoader的類來負責加載(一一對應),那麼ImageLoader又是什麼呢?

什麼是ImageLoader

image 表示一個二進制文件(可執行文件或 so 文件),裏面是被編譯過的符號、代碼等,因此 ImageLoader 做用是將這些文件加載進內存,且每個文件對應一個ImageLoader實例來負責加載。

兩步走:

  • 在程序運行時它先將動態連接的 image 遞歸加載 (也就是上面測試棧中一串的遞歸調用的時刻)。
  • 再從可執行文件 image 遞歸加載全部符號。

固然全部這些都發生在咱們真正的main函數執行前。

動態連接庫加載的具體流程

動態連接庫的加載步驟具體分爲5步:

  1. load dylibs image 讀取庫鏡像文件
  2. Rebase image
  3. Bind image
  4. Objc setup
  5. initializers

下面對每一步進行分析。

load dylibs image

在每一個動態庫的加載過程當中, dyld須要:

  • 分析所依賴的動態庫
  • 找到動態庫的mach-o文件
  • 打開文件
  • 驗證文件
  • 在系統核心註冊文件簽名
  • 對動態庫的每個segment調用mmap()

一般的,一個App須要加載100到400個dylibs, 可是其中的系統庫被優化,能夠很快的加載。針對這一步驟的優化有:

  1. 減小非系統庫的依賴
  2. 合併不是系統庫
  3. 使用靜態資源,好比把代碼加入主程序

rebase/bind

因爲ASLR(address space layout randomization)的存在,可執行文件和動態連接庫在虛擬內存中的加載地址每次啓動都不固定,因此須要這2步來修復鏡像中的資源指針,來指向正確的地址。

rebase修復的是指向當前鏡像內部的資源指針; 而bind指向的是鏡像外部的資源指針。

rebase步驟先進行,須要把鏡像讀入內存,並以page爲單位進行加密驗證,保證不會被篡改,因此這一步的瓶頸在IO。bind在其後進行,因爲要查詢符號表,來指向跨鏡像的資源,加上在rebase階段,鏡像已被讀入和加密驗證,因此這一步的瓶頸在於CPU計算。

經過命令行能夠查看相關的資源指針:

xcrun dyldinfo -rebase -bind -lazy_bind myApp.App/myApp

優化該階段的關鍵在於減小__DATA segment中的指針數量。咱們能夠優化的點有:

  1. 減小Objc類數量, 減小selector數量
  2. 減小C++虛函數數量
  3. 轉而使用swift stuct(其實本質上就是爲了減小符號的數量)

Objc setup

這一步主要工做是:

  1. 註冊Objc類 (class registration)
  2. 把category的定義插入方法列表 (category registration)
  3. 保證每個selector惟一 (selctor uniquing)

因爲以前2步驟的優化,這一步實際上沒有什麼可作的。

initializers

以上三步屬於靜態調整(fix-up),都是在修改__DATA segment中的內容,而這裏則開始動態調整,開始在堆和堆棧中寫入內容。

在這裏的工做有:

  1. Objc的+load()函數
  2. C++的構造函數屬性函數 形如attribute((constructor)) void DoSomeInitializationWork()
  3. 非基本類型的C++靜態全局變量的建立(一般是類或結構體)(non-trivial initializer) 好比一個全局靜態結構體的構建,若是在構造函數中有繁重的工做,那麼會拖慢啓動速度

Objc的load函數和C++的靜態構造函數採用由底向上的方式執行,來保證每一個執行的方法,均可以找到所依賴的動態庫。

上圖是在自定義的類XXViewController的+load方法斷點的調用堆棧,清楚的看到整個調用棧和順序:

  1. dyld 開始將程序二進制文件初始化
  2. 交由 ImageLoader 讀取 image,其中包含了咱們的類、方法等各類符號
  3. 因爲 runtime 向 dyld 綁定了回調,當 image 加載到內存後,dyld 會通知 runtime 進行處理
  4. runtime 接手後調用 map_images 作解析和處理,接下來 load_images 中調用 call_load_methods 方法,遍歷全部加載進來的 Class,按繼承層級依次調用 Class 的 +load 方法和其 Category 的 +load 方法

至此,可執行文件中和動態庫全部的符號(Class,Protocol,Selector,IMP,…)都已經按格式成功加載到內存中,被 runtime 所管理,再這以後,runtime 的那些方法(動態添加 Class、swizzle 等等才能生效)。

整個事件由 dyld 主導,完成運行環境的初始化後,配合 ImageLoader 將二進制文件按格式加載到內存,動態連接依賴庫,並由 runtime 負責加載成 objc 定義的結構,全部初始化工做結束後,dyld 調用真正的 main 函數。

若是程序剛剛被運行過,那麼程序的代碼會被dyld緩存,所以即便殺掉進程再次重啓加載時間也會相對快一點,若是長時間沒有啓動或者當前dyld的緩存已經被其餘應用佔據,那麼此次啓動所花費的時間就要長一點,這就分別是熱啓動和冷啓動的概念,以下圖所示:

main()以前的加載時間如何衡量

那麼問題就來了,那怎麼衡量main()以前也就是time1的耗時呢,蘋果官方提供了一種方法,那就是在真機調試的時候勾選dyld_PRINT_STATISTICS選項。

會獲得以下形式的輸出:

因而可知對於系統級別的動態連接庫,由於蘋果作了優化,因此耗時並很少,在這個awesome的例子中,自身App中的代碼佔用了總體時間的94.2%

咱們應用中一次典型的Log以下:

因而可知,最多的用時仍是在image加載和OC類的初始化,共佔用總時長的79.3%,精簡framework的引入和OC類有優化的空間。

總結一下:對於main()調用以前的耗時咱們能夠優化的點有:

  • 減小沒必要要的framework,由於動態連接比較耗時
  • check framework應當設爲optional和required,若是該framework在當前App支持的全部iOS系統版本都存在,那麼就設爲required,不然就設爲optional,由於optional會有些額外的檢查
  • 合併或者刪減一些OC類,關於清理項目中沒用到的類,使用工具AppCode代碼檢查功能,查到當前項目中沒有用到的類以下:

  • 刪減一些無用的靜態變量
  • 刪減沒有被調用到或者已經廢棄的方法。方法見:

http://stackoverflow.com/ques...

https://developer.Apple.com/l...

  • 將沒必要須在+load方法中作的事情延遲到+initialize中
  • 儘可能不要用C++虛函數(建立虛函數表有開銷)

main()調用以後的加載時間

在main()被調用以後,App的主要工做就是初始化必要的服務,顯示首頁內容等。而咱們的優化也是圍繞如何可以快速展示首頁來開展。

App一般在AppDelegate類中的- (BOOL)Application:(UIApplication )Application didFinishLaunchingWithOptions:(NSDictionary )launchOptions方法中建立首頁須要展現的view,而後在當前runloop的末尾,主動調用CA::Transaction::commit完成視圖的渲染。而視圖的渲染主要涉及三個階段:

  1. 準備階段 這裏主要是圖片的解碼
  2. 佈局階段 首頁全部UIView的- (void)layoutSubViews()運行
  3. 繪製階段 首頁全部UIView的- (void)drawRect:(CGRect)rect運行

再加上啓動以後必要服務的啓動、必要數據的建立和讀取,這些就是咱們能夠嘗試優化的地方

所以,對於main()函數調用以前咱們能夠優化的點有:

  • 不使用xib,直接視用代碼加載首頁視圖
  • NSUserDefaults其實是在Library文件夾下會生產一個plist文件,若是文件太大的話一次能讀取到內存中可能很耗時,這個影響須要評估,若是耗時很大的話須要拆分(需考慮老版本覆蓋安裝兼容問題)
  • 每次用NSLog方式打印會隱式的建立一個Calendar,所以須要刪減啓動時各業務方打的log,或者僅僅針對內測版輸出log
  • 梳理應用啓動時發送的全部網絡請求,是否能夠統一在異步線程請求

實測數據

創建了一個空的HelloWorld工程,只加入了pods中的代碼,不包含主端的業務邏輯代碼,一次典型的冷啓動基本接近2s iPhone6 iOS9.3.5系統測試主要時間在加載動態庫,類/方法的初始化還有符號地址綁定階段。

一次典型的熱啓動數據以下:能夠看到由於系統作了緩存方面的優化,比冷啓動快了500ms加上頭條主端業務邏輯代碼以後一次典型的熱啓動耗時2.1s。

以上用時均爲main()以前的加載耗時。

 main函數以後加載時間優化記錄

NSUserDefaults是不是瓶頸

蘋果官方文檔提到NSUserDefaults加載的時候是整個plist配置文件所有load到內存中,目前頭條主端當中NSUserDefaults存儲了200多項緩存數據,所以懷疑可能拖慢啓動速度,可是測試結果顯示並不會。

經過符號斷點+[NSUserDefaults standardUserDefaults]肯定最先一次的+load()從執行到結束耗時1.8ms,可見NSUserDefaults的初始化僅耗時1.8ms,並非啓動耗時的瓶頸。

如何找到拖慢啓動應用時長的瓶頸

爲了找到瓶頸,咱們在啓動以後的didFinishLauhcning方法開始執行到首頁列表頁的NewsListViewController的viewDidAppear方法,幾乎每一個可能比較耗時的流程進行拆分和統計,獲得統計數據以後發現:

主要耗時在首頁UI構造和渲染(storyboard加載,tabBar/topBar渲染,開屏廣告加載/cell註冊/日誌模塊初始化這幾個步驟)。

具體優化點

所以,針對於今日頭條這個App咱們能夠優化的點以下:

  1. 純代碼方式而不是storyboard加載首頁UI。
  2. didFinishLaunching裏的函數考慮可否挖掘能夠延遲加載或者懶加載,須要與各個業務方pm和rd共同check 對於一些已經下線的業務,刪減冗餘代碼。
  3. 對於一些與UI展現無關的業務,如微博認證過時檢查、圖片最大緩存空間設置等作延遲加載。
  4. 對實現了+load()方法的類進行分析,儘可能將load裏的代碼延後調用。
  5. 上面統計數據顯示展現feed的導航控制器頁面(NewsListViewController)比較耗時,對於viewDidLoad以及viewWillAppear方法中儘可能去嘗試少作,晚作,不作。

優化結果

以前曾經有一位同事已經作了必定的優化,好比啓動以後展現閃屏廣告圖的同時初始化首頁的列表頁,當廣告展現完成以後列表頁也就渲染完成了。通過這一次優化以後的main()以後的啓動總時長經過上線以後收集數據的驗證達到了預期的效果。

相關文章
相關標籤/搜索