iOS啓動時間的一些分析

前言

最近在作一些App品質提高,啓動時間優化是其中很重要的一項,本文圍繞啓動時間作一個深刻了解。xcode

正文

什麼是啓動時間?

啓動時間能夠理解爲從用戶點擊App的Icon到用戶看到App真正畫面而且能夠進行交互的時間。這段時間還能夠爲兩部分:iOS系統啓動App的時間 和 App初始化應用內部邏輯和界面的時間。緩存

1、App產生

在探究iOS系統如何處理App啓動以前,咱們須要先了解下一個App是如何產生的:
一、編譯:咱們打開一個xcode工程,會看到若干個.h/.m組成;當咱們進行編譯時,編譯器會分別對每一個.m文件進行編譯,獲得對應的.o文件;
sass

二、連接:將編譯產生的多個.o文件結合靜態庫、動態庫進行連接,獲得一個可執行文件,也叫Mach-O文件;​ Mach-O裏的部分信息會被行裁剪(strip),好比說調試符號、行號等信息;爲了方便調試,會把這些信息放到一個dsym文件; ​markdown

三、簽名&打包&上傳:將裁剪後的Mach-O與資源文件(storyboard、asset)等一塊兒打包成.app文件,再進行簽名,最後上傳到AppStore後臺;app

2、iOS如何啓動App

WWDC視頻中對啓動過程作了一些介紹,先看iOS 13之前用dyld2是如何啓動App:ide

一、解析Mach-O文件的頭部,找到​LC_LOAD_DYLINKER,定位到dyld的路徑,將dyld加載到內存中;函數

二、解析動態庫的依賴,好比說咱們工程中這部分依賴;工具

三、分別將動態庫mmap到內存中,一個App運行過程當中會依賴不少動態庫;​oop

四、符號查找定位,下圖是咱們工程依賴的GLKit.framework,可是點開framework的所在文件夾,會發現只有頭文件和一個tbd文件;tbd是text-based stub library的簡稱,爲xcode連接過程提供符號;App真正運行的時候,還須要加載動態庫,進行真正的連接;(動態連接的瞭解能夠看前文)佈局

五、符號綁定和重定向,動態連接與靜態連接同樣,符號最終都須要轉換爲運行時的內存地址;動態庫的符號須要運行時,才能肯定全部符號的具體位置;還有另一個影響的因素是iOS的ASLR(進程地址空間佈局隨機化)也須要在運行時加上偏移;

六、靜態初始化,包括咱們經常使用​+load方法,以及其餘靜態初始化的方法;

dyld3如何進行優化? iOS 13以後,系統提供的dyld3將啓動過程的解析Mach-O文件的頭部解析動態庫的依賴符號查找定位的結果作了一個緩存,寫到是disk中。在啓動時候,就直接讀取緩存並校驗是否有效,再進行後續的動態庫加載符號綁定和重定向以及靜態初始化

這個緩存存儲在沙盒的tmp/com.apple.dyld目錄(tmp目錄不能再整個清除),緩存會在手機系統升級或者更新App時從新建立。

3、開發時如何對這些時間進行分析

開發階段,能夠在環境變量中設置DYLD_PRINT_STATISTICS值爲1;

啓動的時候,就能夠看到控制檯打出了具體的時間。

Total pre-main time: 622.64 milliseconds (100.0%)
         dylib loading time:  33.89 milliseconds (5.4%)
        rebase/binding time: 279.52 milliseconds (44.8%)
            ObjC setup time: 270.59 milliseconds (43.4%)
           initializer time:  38.63 milliseconds (6.2%)
           slowest intializers :
             libSystem.B.dylib :   7.08 milliseconds (1.1%)
    libMainThreadChecker.dylib :  19.92 milliseconds (3.2%)
複製代碼

一樣,還能夠設置DYLD_PRINT_LIBRARIES值爲1,就會打印出來裝載了哪些動態庫。

dyld: loaded: /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 12.2.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libMobileGestaltExtensions.dylib
dyld: loaded: /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 12.2.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libobjc-trampolines.dylib
dyld: loaded: /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 12.2.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/FontServices.framework/libTrueTypeScaler.dylib
dyld: loaded: /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 12.2.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Accelerate.framework/Frameworks/vImage.framework/Libraries/libCGInterfaces.dylib
...
複製代碼

Instrucment也有工具進行這些時間的分析,好比說你們最經常使用的Time Profiler,以及更復雜的System Trace。
Time Profiler​基於採樣的方式進行運行時間統計,大概每毫秒會採樣一次,能夠經過​勾選Recording Options的​High Frequency來提升採樣頻率;Time Profiler​的使用比較簡單,能直接反饋出來瓶頸的問題。
System Trace能夠更細緻的分析鎖、線程狀態、內存變化、系統調用等,好比說下圖的​Zero Fill、File Backed Page In、Page Cache Hit、Copy On Write的分佈。
​ File Backed Page In 就是PageFault,內存缺頁中斷,訪問一個虛擬內存地址而內存中還不存在時觸發,操做系統會分配物理內存並拷貝內容到對應物理內存;
Page Cache Hit 若是操做系統的PageCache裏有對應緩存,則會觸發一個Page Cache Hit;(參考資料
Copy On Write 操做系統中的內存頁存在共享的狀況,若是某些頁是隻讀,則一直是能夠共享的;可是若是對一個可寫的共享內存頁進行寫操做時,須要先複製一份再嘗試寫入,這個過程就是Copy On Write;
Zero Fill 部份內存頁的值都是0,在讀入後須要出發一次填充0的操做,這個過程就是Zero Fill;

4、如何對線上用戶進行啓動時間統計

最實用的方式就是打點統計:
+load方法開始打點:+load方法的調用順序是按照連接順序執行,若是使用CocoaPod來管理集成庫,能夠新建一個A開頭的Pod庫(CocoaPod是按照字母升序),讓該Pod庫的+load方法第一個被執行;
main函數開始打點:__attribute__能夠設置函數、變量和類型屬性,能夠設置一個constructor屬性,讓函數在main()函數執行以前被自動的執行。

static void __attribute__((constructor)) _mainConstructor() {
    NSLog(@"main constructor");
}
複製代碼

didFinishLaunchingWithOptions開始打點:直接在APPDelegate的didFinishLaunchingWithOptions方法開始時打點;
didFinishLaunchingWithOptions結束打點:直接在APPDelegate的didFinishLaunchingWithOptions方法結束時打點;
RootViewControllerDidAppear打點:在viewDidAppear:方法開始時打點;

總結

瞭解更多關於啓動相關的知識,才能更好去分析問題,設計良好的解決方案。
最後介紹了兩個工具:MachOView 和 Hopper Disassembler。 前者開源免費(直接搜索下載),後者收費軟件(也能夠30分鐘試用)。

附錄

dyld開源代碼
iOS的文件內存映射——mmap
WWDC2017-App Startup Time: Past, Present, and Future

相關文章
相關標籤/搜索