如何優化 App 的啓動耗時?

原文:iOS面試題大全c++

iOS 的 App 啓動主要分爲如下步驟:面試

  • 打開 App,系統內核進行初始化跳轉到 dyld 執行。這個過程包括這些步驟:1)分配虛擬內存空間;2)fork 進程;3)加載 MachO (自身全部的可執行 MachO 文件的集合)到進程空間;4)加載動態連接器 dyld 並將控制權交給 dyld 處理。在這個過程當中內核會產生 ASLR(Address space layout randomization) 隨機數值,這個值用於加載的 MachO 起始地址在內存中的偏移,隨機的地址可防止 MachO 代碼掃描並被 hack,提高安全性。經過 ASLR 雖然可隨機化各內存區基地址,但沒法將程序內的代碼段和數據段隨機化,若是繞過(bypass) ASLR 依然可進行篡改,就須要結合 PIE(Position Independent Executable) 共同使用。與之類似的還有 PIC(Position Independent Code),位置無關代碼,做用於共享庫代碼。PIE/PIC 技術須要在編譯階段開啓。顧名思義,PIC 可將程序代碼裝載到任意地址,這樣就內部的指針不能靠固定的絕對地址訪問,而經過相對地址指令如 adrp 來獲取代碼和數據。
  • 進入 dyld 動態連接器,它負責將一個 App 處理爲一個可運行的狀態,包含:
  • 加載 MachO 的依賴庫(這些依賴庫也是 MachO 格式的文件)。dyld 從可執行 MachO 文件的依賴開始, 遞歸加載全部依賴的動態庫。 動態庫包括:iOS 中用到的全部系統動態庫:加載 OC runtime 方法的 libobjc,系統級別的 libSystem(例如 libdispatch(GCD) 和 libsystem_blocks(Block));其餘 App 本身的動態庫。根據 Apple 的描述,大部分 App 所加載的庫在 100~400 個。不過 iOS 系統庫已經被特殊優化過,如提早加入共享緩存,提早作好地址修正等。
  • Fix-ups(地址修正),包括 rebasing 和 binding 等。ASLR + PIE 技術加強了程序的安全性,使得依賴固定地址進行攻擊的方法失效,但也增長了程序自身的複雜度,MachO 文件的 rebase 和 bind info 等部分以及啓動時的 fix-ups 地址修正階段就是配合它而產生的。
  • ObjC 環境配置。通過了 MachO 程序和依賴庫的加載以及地址修正以後,dyld 所作的大部分事情已經完成了。在這一階段,dyld 開始對主程序的依賴庫進行初始化工做,而初始化的執行部分會回調到依賴庫內部執行,如 ObjC 的運行時環境所在的 libobjc.A.dylib 以及 libdispatch.dylib 等。ObjC Setup 的過程,主要是對 ObjC 數據進行關聯註冊:1)dyld 將主程序 MachO 基址指針和包含的 ObjC 相關類信息傳遞到 libobjc;2)ObjC Runtime 從 __DATA 段中獲取 ObjC 類信息,因爲 ObjC 是動態語言,能夠經過類名獲取其實例,因此 Runtime 維護了一個映射全部類的全局類名錶。當加載的數據包含了類的定義,類的名字就須要註冊到全局表中;3)獲取 protocol、category 等類相關屬性並與對應類進行關聯;4)ObjC 的調用都是基於 selector 的,因此須要對 selector 全局惟一性進行處理。以上步驟由 dyld 啓動 libSystem.dylib 統一對基礎庫進行調用執行,這裏面就包含了 libobjc 的 Runtime,同時 Runtime 會在 dyld 綁定回調,當 dyld 處理完相關數據後就會調用 ObjC Runtime 執行 Setup 工做。
  • 執行各模塊初始化器。從這一步就開始接近上(業務)層:1)經過 ObjC Runtime 在 dyld 註冊的通知,當 MachO 鏡像準備完畢後,dyld 會回調到 ObjC 中執行 +load() 方法,包括如下步驟:a)獲取全部 non-lazy class 列表;b)按繼承以及 category 的順序將類排入待加載列表;c)對待加載列表中的類進行方法判斷並調用 +load() 方法。2)執行 C/C++ 初始化構造器,如經過 attribute((constructor)) 註解的函數。3)若是包含 C++,則 dyld 一樣會回調到 libc++ 庫中對全局靜態變量、隱式初始化等進行調用。
  • 查找並跳轉到 main() 函數入口。到了最後,dyld 回到 Load command,找到 LC_MAIN,拿到 entryoff 再加上 MachO 在內存的加載首地址(首地址就是內核傳來的 slide 偏移)就獲得了 main() 的入口地址,從而進入咱們顯式的程序邏輯。

進入 main() -> UIApplicationMain -> 初始化回調 -> 顯示UI。sql

iOS 的 App 啓動時長大概能夠這樣計算:緩存

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

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

t2 = main 方法執行以後到 AppDelegate 類中的 application:didFinishLaunchingWithOptions:方法執行結束前這段時間,主要是構建第一個界面,並完成渲染展現。併發

在 t1 階段加快 App 啓動的建議:app

  • 儘可能使用靜態庫,減小動態庫的使用,動態連接比較耗時。
  • 若是要用動態庫,儘可能將多個 dylib 動態庫合併成一個。
  • 儘可能避免對系統庫使用 optional linking,若是 App 用到的系統庫在你全部支持的系統版本上都有,就設置爲 required,由於 optional 會有些額外的檢查。
  • 減小 Objective-C Class、Selector、Category 的數量。能夠合併或者刪減一些 OC 類。
  • 刪減一些無用的靜態變量,刪減沒有被調用到或者已經廢棄的方法。
  • 將沒必要須在 +load 中作的事情儘可能挪到 +initialize 中,+initialize 是在第一次初始化這個類以前被調用,+load 在加載類的時候就被調用。儘可能將 +load 裏的代碼延後調用。
  • 儘可能不要用 C++ 虛函數,建立虛函數表有開銷。
  • 不要使用 __atribute__((constructor)) 將方法顯式標記爲初始化器,而是讓初始化方法調用時才執行。好比使用 dispatch_once()pthread_once() std::once()
  • 在初始化方法中不調用 dlopen()dlopen() 有性能和死鎖的可能性。
  • 在初始化方法中不建立線程。

在 t2 階段加快 App 啓動的建議:dom

  • 儘可能不要使用 xib/storyboard,而是用純代碼做爲首頁 UI。
  • 若是要用 xib/storyboard,不要在 xib/storyboard 中存放太多的視圖。
  • application:didFinishLaunchingWithOptions: 裏的任務儘可能延遲加載或懶加載。
  • 不要在 NSUserDefaults 中存放太多的數據,NSUserDefaults 是一個 plist 文件,plist 文件被反序列化一次。
  • 避免在啓動時打印過多的 log。
  • 少用 NSLog,由於每一次 NSLog 的調用都會建立一個新的 NSCalendar 實例。
  • 每一段 SQLite 語句都是一個段被編譯的程序,調用 sqlite3_prepare 將編譯 SQLite 查詢到字節碼,使用 sqlite_bind_int 綁定參數到 SQLite 語句。
  • 爲了防止使用 GCD 建立過多的線程,解決方法是建立串行隊列, 或者使用帶有最大併發數限制的 NSOperationQueue。
  • 線程安全:UIKit只能在主線程執行,除了 UIGraphics、UIBezierPath 以外,UIImage、CG、CA、Foundation 都不能從兩個線程同時訪問。
  • 不要在主線程執行磁盤、網絡、Lock 或者 dispatch_sync、發送消息給其餘線程等操做。
相關文章
相關標籤/搜索