iOS 的 App 啟動主要分為以下步驟:
打開 App腐碱,系統(tǒng)內(nèi)核進行初始化跳轉(zhuǎn)到 dyld 執(zhí)行。這個過程包括這些步驟:1)分配虛擬內(nèi)存空間掉弛;2)fork 進程症见;3)加載 MachO (自身所有的可執(zhí)行 MachO 文件的集合)到進程空間;4)加載動態(tài)鏈接器 dyld 并將控制權交給 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),位置無關代碼寂殉,作用于共享庫代碼囚巴。PIE/PIC 技術需要在編譯階段開啟。顧名思義友扰,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 技術增強了程序的安全性循衰,使得依賴固定地址進行攻擊的方法失效铲敛,但也增加了程序自身的復雜度,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ù)進行關聯(lián)注冊:1)dyld 將主程序 MachO 基址指針和包含的 ObjC 相關類信息傳遞到 libobjc串远;2)ObjC Runtime 從 __DATA 段中獲取 ObjC 類信息宏多,由于 ObjC 是動態(tài)語言儿惫,可以通過類名獲取其實例,所以 Runtime 維護了一個映射所有類的全局類名表伸但。當加載的數(shù)據(jù)包含了類的定義肾请,類的名字就需要注冊到全局表中;3)獲取 protocol更胖、category 等類相關屬性并與對應類進行關聯(lián)铛铁;4)ObjC 的調(diào)用都是基于 selector 的,所以需要對 selector 全局唯一性進行處理函喉。以上步驟由 dyld 啟動 libSystem.dylib 統(tǒng)一對基礎庫進行調(diào)用執(zhí)行避归,這里面就包含了 libobjc 的 Runtime荣月,同時 Runtime 會在 dyld 綁定回調(diào)管呵,當 dyld 處理完相關數(shù)據(jù)后就會調(diào)用 ObjC Runtime 執(zhí)行 Setup 工作。
執(zhí)行各模塊初始化器哺窄。從這一步就開始接近上(業(yè)務)層: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++ 初始化構造器婴程,如通過 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é)束前這段時間咙冗,主要是構建第一個界面沾歪,并完成渲染展示。
在 t1 階段加快 App 啟動的建議:
- 盡量使用靜態(tài)庫乞娄,減少動態(tài)庫的使用瞬逊,動態(tài)鏈接比較耗時显歧。
- 如果要用動態(tài)庫,盡量將多個 dylib 動態(tài)庫合并成一個确镊。
- 盡量避免對系統(tǒng)庫使用 optional linking士骤,如果 App 用到的系統(tǒng)庫在你所有支持的系統(tǒng)版本上都有,就設置為 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:
里的任務盡量延遲加載或懶加載。 - 不要在 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)絡绞幌、Lock 或者 dispatch_sync蕾哟、發(fā)送消息給其他線程等操作。
更多:iOS面試題合集