希望總結(jié)項(xiàng)目中的啟動優(yōu)化,詳細(xì)學(xué)習(xí)iOS啟動流程
1. 想要對App進(jìn)行啟動優(yōu)化需要優(yōu)先了解App的啟動時(shí)間,和個(gè)個(gè)啟動過程中的時(shí)間占比(主要講解Pre-main之前的過程)
- Xcode 13.0 beta / iOS 15.0之前比較方便我們可以在 Xcode 中配置環(huán)境變量 DYLD_PRINT_STATISTICS 為 1(Edit Scheme → Run → Arguments → Environment Variables → +)。
- Xcode 13.0 beta / iOS 15.0之后這個(gè)方法失效了蘋果推薦我們使用instrument工具進(jìn)行監(jiān)測
2. 了解了方法后我們可以通過打印看到我們每個(gè)過程消耗的時(shí)間,此處有幾個(gè)關(guān)鍵指標(biāo)
dylib loading (動態(tài)庫加載)
rebase/binding (偏移修正/符號綁定)
rebase(偏移修正):任何一個(gè)app生成的二進(jìn)制文件,在二進(jìn)制文件內(nèi)部所有的方法、函數(shù)調(diào)用,都有一個(gè)地址蒜哀,這個(gè)地址是在當(dāng)前二進(jìn)制文件中的偏移地址。一旦在運(yùn)行時(shí)刻(即運(yùn)行到內(nèi)存中),每次系統(tǒng)都會隨機(jī)分配一個(gè)ASLR(Address Space Layout Randomization进泼,地址空間布局隨機(jī)化)地址值(是一個(gè)安全機(jī)制蔗衡,會分配一個(gè)隨機(jī)的數(shù)值,插入在二進(jìn)制文件的開頭)乳绕,例如绞惦,二進(jìn)制文件中有一個(gè) test方法,偏移值是0x0001洋措,而隨機(jī)分配的ASLR是0x1f00济蝉,如果想訪問test方法,其內(nèi)存地址(即真實(shí)地址)變?yōu)?ASLR+偏移值 = 運(yùn)行時(shí)確定的內(nèi)存地址(即0x1f00+0x0001 = 0x1f01)
- Objc setup (Objc相關(guān)類的注冊,selector唯一性檢查)
dyld調(diào)用的objc_init方法菠发,這個(gè)是runtime的初始化方法王滤,在這個(gè)方法里面主要的操作就是加載類(對需要的class和category進(jìn)行注冊),objc_init方法通過內(nèi)部的_dyld_objc_notify_register向dyld注冊了一個(gè)通知事件,當(dāng)有新的image(程序中對應(yīng)實(shí)例可簡稱為image雷酪,如程序可執(zhí)行文件macho淑仆,F(xiàn)ramework,bundle等)加載到內(nèi)存的時(shí)候哥力,就會觸發(fā)load_images方法蔗怠,這個(gè)方法里面就是加載對應(yīng)image里面的類,并調(diào)用load方法(在下一階段initializer),如果有繼承的類吩跋,那么會先調(diào)用父類的load方法寞射,然后調(diào)用子類的,但是在load里面不能調(diào)用[super load]锌钮。最后才是調(diào)用category的load方法桥温。總之梁丘,所有的load都會被調(diào)用到(注意:子類的initialize方法會覆蓋父類侵浸,不同于load方法)
- initializer (初始化執(zhí)行l(wèi)oad方法,創(chuàng)建靜態(tài)全局變量等)
3. 介紹幾個(gè)概念
1.Mach-O文件
Apple出品的操作系統(tǒng)的可執(zhí)行文件格式幾乎都是mach-o,iOS當(dāng)然也不例外氛谜。
mach-o可以大致的分為三部分:
- Header 頭部掏觉,包含可以執(zhí)行的CPU架構(gòu),比如x86,arm64
- Load commands 加載命令值漫,包含文件的組織架構(gòu)和在虛擬內(nèi)存中的布局方式
- Data澳腹,數(shù)據(jù),包含load commands中需要的各個(gè)段(segment)的數(shù)據(jù)杨何,每一個(gè)Segment都得大小是Page的整數(shù)倍酱塔。
2.Virtual Memory
虛擬內(nèi)存是建立在物理內(nèi)存和進(jìn)程之間的中間層。在iOS上危虱,當(dāng)內(nèi)存不足的時(shí)候羊娃,會嘗試釋放那些只讀的Page,因?yàn)橹蛔x的Page在下次被訪問的時(shí)候埃跷,可以再從磁盤讀取蕊玷。如果沒有可用內(nèi)存芦瘾,會通知在后臺的App(也就是在這個(gè)時(shí)候收到了memory warning),如果在這之后仍然沒有可用內(nèi)存集畅,則會殺死在后臺的App。
- 虛擬內(nèi)存是在物理內(nèi)存上建立的一個(gè)邏輯地址空間缅糟,它向上(應(yīng)用)提供了一個(gè)連續(xù)的邏輯地址空間挺智,向下隱藏了物理內(nèi)存的細(xì)節(jié)。
- 虛擬內(nèi)存使得邏輯地址可以沒有實(shí)際的物理地址窗宦,也可以讓多個(gè)邏輯地址對應(yīng)到一個(gè)物理地址赦颇。
- 虛擬內(nèi)存被劃分為一個(gè)個(gè)大小相同的Page(64位系統(tǒng)上是16KB),提高管理和讀寫的效率赴涵。 Page又分為只讀和讀寫的Page媒怯。
3.Page fault
在應(yīng)用執(zhí)行的時(shí)候,它被分配的邏輯地址空間都是可以訪問的髓窜,當(dāng)應(yīng)用訪問一個(gè)邏輯Page扇苞,而在對應(yīng)的物理內(nèi)存中并不存在的時(shí)候,這時(shí)候就發(fā)生了一次Page fault寄纵。當(dāng)Page fault發(fā)生的時(shí)候鳖敷,會中斷當(dāng)前的程序,在物理內(nèi)存中尋找一個(gè)可用的Page程拭,然后從磁盤中讀取數(shù)據(jù)到物理內(nèi)存定踱,接著繼續(xù)執(zhí)行當(dāng)前程序。
4.改善APP的啟動
-在 dylib loading的過程中恃鞋,會去裝載app使用的動態(tài)庫崖媚,而每一個(gè)動態(tài)庫有它自己的依賴關(guān)系,所以會消耗時(shí)間去查找和讀取,Apple官方建議盡量少的使用自定義的動態(tài)庫恤浪,或者考慮合并多個(gè)動態(tài)庫畅哑,其中一個(gè)建議是當(dāng)大于6個(gè)的時(shí)候,則需要考慮合并它們资锰。簡單的舉個(gè)例子比如使用cocoapods管理的多個(gè)自定義的UI組件可以合并成一個(gè)自己的UIKIT,同時(shí)也建議動態(tài)庫轉(zhuǎn)靜態(tài)庫
-減少+load的方法使用,+load方法中盡量少做耗時(shí)操作,+load中代碼延遲到 main 之后子線程處理或者首頁顯示之后敢课;改為 initialize 中執(zhí)行,針對 initialize 中處理需要注意的是分類 initialize 會覆蓋主類 initialize 以及有子類后 initialize 執(zhí)行多次的問題绷杜,需要使用 dispatch_once 來保證代碼只執(zhí)行一次;
-二進(jìn)制重排要實(shí)現(xiàn)符號的重排直秆,一是需要我們收集整個(gè)啟動鏈路上的方法和函數(shù)等符號,二是需要生成對應(yīng)的 order 文件來配置 ld 中的 Order File 屬性鞭盟。當(dāng)工程在編譯的時(shí)候圾结,Xcode 會讀取這個(gè) order 文件,在鏈接過程中會根據(jù)這個(gè)文件中的符號順序來生成對應(yīng)的 MachO齿诉。一般業(yè)界中收集符號的方案有兩種:
1.Hook objc_msgSend筝野,只能拿到 OC 以及 swift @objc dynamic 的符號晌姚;
2.Clang 插樁,能完美拿到 OC歇竟、C/C++挥唠、Swift、Block 的符號焕议;
第二種方法實(shí)現(xiàn)成本較高
采用第一種方法在編譯完成后通過驗(yàn)證 LinkMap 文件中 #Symbols: 部分符號順序是否和 order 文件中的符號順序一致來確定是否配置成功即可