最近在作一些App品質提高,啓動時間優化是其中很重要的一項,本文圍繞啓動時間作一個深刻了解。xcode
啓動時間能夠理解爲從用戶點擊App的Icon到用戶看到App真正畫面而且能夠進行交互的時間。這段時間還能夠爲兩部分:iOS系統啓動App的時間 和 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
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時從新建立。
開發階段,能夠在環境變量中設置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;
最實用的方式就是打點統計:
+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