如何优化 App 的启动耗时?
生活随笔
收集整理的這篇文章主要介紹了
如何优化 App 的启动耗时?
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
原文:iOS面試題大全
iOS 的 App 啟動主要分為以下步驟:
- 打開 App,系統(tǒng)內(nèi)核進行初始化跳轉(zhuǎn)到 dyld 執(zhí)行。這個過程包括這些步驟:1)分配虛擬內(nèi)存空間;2)fork 進程;3)加載 MachO (自身所有的可執(zhí)行 MachO 文件的集合)到進程空間;4)加載動態(tài)鏈接器 dyld 并將控制權(quán)交給 dyld 處理。在這個過程中內(nèi)核會產(chǎn)生 ASLR(Address space layout randomization) 隨機數(shù)值,這個值用于加載的 MachO 起始地址在內(nèi)存中的偏移,隨機的地址可防止 MachO 代碼掃描并被 hack,提升安全性。通過 ASLR 雖然可隨機化各內(nèi)存區(qū)基地址,但無法將程序內(nèi)的代碼段和數(shù)據(jù)段隨機化,如果繞過(bypass) ASLR 依然可進行篡改,就需要結(jié)合 PIE(Position Independent Executable) 共同使用。與之相似的還有 PIC(Position Independent Code),位置無關(guān)代碼,作用于共享庫代碼。PIE/PIC 技術(shù)需要在編譯階段開啟。顧名思義,PIC 可將程序代碼裝載到任意地址,這樣就內(nèi)部的指針不能靠固定的絕對地址訪問,而通過相對地址指令如 adrp 來獲取代碼和數(shù)據(jù)。
- 進入 dyld 動態(tài)鏈接器,它負責將一個 App 處理為一個可運行的狀態(tài),包含:
- 加載 MachO 的依賴庫(這些依賴庫也是 MachO 格式的文件)。dyld 從可執(zhí)行 MachO 文件的依賴開始, 遞歸加載所有依賴的動態(tài)庫。 動態(tài)庫包括:iOS 中用到的所有系統(tǒng)動態(tài)庫:加載 OC runtime 方法的 libobjc,系統(tǒng)級別的 libSystem(例如 libdispatch(GCD) 和 libsystem_blocks(Block));其他 App 自己的動態(tài)庫。根據(jù) Apple 的描述,大部分 App 所加載的庫在 100~400 個。不過 iOS 系統(tǒng)庫已經(jīng)被特殊優(yōu)化過,如提前加入共享緩存,提前做好地址修正等。
- Fix-ups(地址修正),包括 rebasing 和 binding 等。ASLR + PIE 技術(shù)增強了程序的安全性,使得依賴固定地址進行攻擊的方法失效,但也增加了程序自身的復(fù)雜度,MachO 文件的 rebase 和 bind info 等部分以及啟動時的 fix-ups 地址修正階段就是配合它而產(chǎn)生的。
- ObjC 環(huán)境配置。經(jīng)過了 MachO 程序和依賴庫的加載以及地址修正之后,dyld 所做的大部分事情已經(jīng)完成了。在這一階段,dyld 開始對主程序的依賴庫進行初始化工作,而初始化的執(zhí)行部分會回調(diào)到依賴庫內(nèi)部執(zhí)行,如 ObjC 的運行時環(huán)境所在的 libobjc.A.dylib 以及 libdispatch.dylib 等。ObjC Setup 的過程,主要是對 ObjC 數(shù)據(jù)進行關(guān)聯(lián)注冊:1)dyld 將主程序 MachO 基址指針和包含的 ObjC 相關(guān)類信息傳遞到 libobjc;2)ObjC Runtime 從 __DATA 段中獲取 ObjC 類信息,由于 ObjC 是動態(tài)語言,可以通過類名獲取其實例,所以 Runtime 維護了一個映射所有類的全局類名表。當加載的數(shù)據(jù)包含了類的定義,類的名字就需要注冊到全局表中;3)獲取 protocol、category 等類相關(guān)屬性并與對應(yīng)類進行關(guān)聯(lián);4)ObjC 的調(diào)用都是基于 selector 的,所以需要對 selector 全局唯一性進行處理。以上步驟由 dyld 啟動 libSystem.dylib 統(tǒng)一對基礎(chǔ)庫進行調(diào)用執(zhí)行,這里面就包含了 libobjc 的 Runtime,同時 Runtime 會在 dyld 綁定回調(diào),當 dyld 處理完相關(guān)數(shù)據(jù)后就會調(diào)用 ObjC Runtime 執(zhí)行 Setup 工作。
- 執(zhí)行各模塊初始化器。從這一步就開始接近上(業(yè)務(wù))層:1)通過 ObjC Runtime 在 dyld 注冊的通知,當 MachO 鏡像準備完畢后,dyld 會回調(diào)到 ObjC 中執(zhí)行 +load() 方法,包括以下步驟:a)獲取所有 non-lazy class 列表;b)按繼承以及 category 的順序?qū)㈩惻湃氪虞d列表;c)對待加載列表中的類進行方法判斷并調(diào)用 +load() 方法。2)執(zhí)行 C/C++ 初始化構(gòu)造器,如通過 attribute((constructor)) 注解的函數(shù)。3)如果包含 C++,則 dyld 同樣會回調(diào)到 libc++ 庫中對全局靜態(tài)變量、隱式初始化等進行調(diào)用。
- 查找并跳轉(zhuǎn)到 main() 函數(shù)入口。到了最后,dyld 回到 Load command,找到 LC_MAIN,拿到 entryoff 再加上 MachO 在內(nèi)存的加載首地址(首地址就是內(nèi)核傳來的 slide 偏移)就得到了 main() 的入口地址,從而進入我們顯式的程序邏輯。
進入 main() -> UIApplicationMain -> 初始化回調(diào) -> 顯示UI。
iOS 的 App 啟動時長大概可以這樣計算:
t(App 總啟動時間) = t1(main 調(diào)用之前的加載時間) + t2(main 調(diào)用之后的加載時間)。
t1 = 系統(tǒng) dylib(動態(tài)鏈接庫)和自身 App 可執(zhí)行文件的加載。
t2 = main 方法執(zhí)行之后到 AppDelegate 類中的 application:didFinishLaunchingWithOptions:方法執(zhí)行結(jié)束前這段時間,主要是構(gòu)建第一個界面,并完成渲染展示。
在 t1 階段加快 App 啟動的建議:
- 盡量使用靜態(tài)庫,減少動態(tài)庫的使用,動態(tài)鏈接比較耗時。
- 如果要用動態(tài)庫,盡量將多個 dylib 動態(tài)庫合并成一個。
- 盡量避免對系統(tǒng)庫使用 optional linking,如果 App 用到的系統(tǒng)庫在你所有支持的系統(tǒng)版本上都有,就設(shè)置為 required,因為 optional 會有些額外的檢查。
- 減少 Objective-C Class、Selector、Category 的數(shù)量。可以合并或者刪減一些 OC 類。
- 刪減一些無用的靜態(tài)變量,刪減沒有被調(diào)用到或者已經(jīng)廢棄的方法。
- 將不必須在 +load 中做的事情盡量挪到 +initialize 中,+initialize 是在第一次初始化這個類之前被調(diào)用,+load 在加載類的時候就被調(diào)用。盡量將 +load 里的代碼延后調(diào)用。
- 盡量不要用 C++ 虛函數(shù),創(chuàng)建虛函數(shù)表有開銷。
- 不要使用 __atribute__((constructor)) 將方法顯式標記為初始化器,而是讓初始化方法調(diào)用時才執(zhí)行。比如使用 dispatch_once(),pthread_once() 或 std::once()。
- 在初始化方法中不調(diào)用 dlopen(),dlopen() 有性能和死鎖的可能性。
- 在初始化方法中不創(chuàng)建線程。
在 t2 階段加快 App 啟動的建議:
- 盡量不要使用 xib/storyboard,而是用純代碼作為首頁 UI。
- 如果要用 xib/storyboard,不要在 xib/storyboard 中存放太多的視圖。
- 對 application:didFinishLaunchingWithOptions: 里的任務(wù)盡量延遲加載或懶加載。
- 不要在 NSUserDefaults 中存放太多的數(shù)據(jù),NSUserDefaults 是一個 plist 文件,plist 文件被反序列化一次。
- 避免在啟動時打印過多的 log。
- 少用 NSLog,因為每一次 NSLog 的調(diào)用都會創(chuàng)建一個新的 NSCalendar 實例。
- 每一段 SQLite 語句都是一個段被編譯的程序,調(diào)用 sqlite3_prepare 將編譯 SQLite 查詢到字節(jié)碼,使用 sqlite_bind_int 綁定參數(shù)到 SQLite 語句。
- 為了防止使用 GCD 創(chuàng)建過多的線程,解決方法是創(chuàng)建串行隊列, 或者使用帶有最大并發(fā)數(shù)限制的 NSOperationQueue。
- 線程安全:UIKit只能在主線程執(zhí)行,除了 UIGraphics、UIBezierPath 之外,UIImage、CG、CA、Foundation 都不能從兩個線程同時訪問。
- 不要在主線程執(zhí)行磁盤、網(wǎng)絡(luò)、Lock 或者 dispatch_sync、發(fā)送消息給其他線程等操作。
總結(jié)
以上是生活随笔為你收集整理的如何优化 App 的启动耗时?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中缀表达式转换成后缀表达式(只适用于加减
- 下一篇: 使用class-validator替换J