這是一篇 WWDC 2016 Session 406 的學(xué)習(xí)筆記岗憋,從原理到實(shí)踐講述了如何優(yōu)化 App 的啟動(dòng)時(shí)間如失。
App 運(yùn)行理論
main()
執(zhí)行前發(fā)生的事
Mach-O 格式
虛擬內(nèi)存基礎(chǔ)
Mach-O 二進(jìn)制的加載
理論速成
Mach-O 術(shù)語
Mach-O 是針對(duì)不同運(yùn)行時(shí)可執(zhí)行文件的文件類型倘是。文件類型:
Executable: 應(yīng)用的主要二進(jìn)制
Dylib: 動(dòng)態(tài)鏈接庫(又稱 DSO 或 DLL)
Bundle: 不能被鏈接的 Dylib级遭,只能在運(yùn)行時(shí)使用 dlopen()
加載,可當(dāng)做 macOS 的插件嘀韧。
Image: executable秤标,dylib 或 bundleFramework: 包含 Dylib 以及資源文件和頭文件的文件夾Mach-O 鏡像文件
Mach-O 被劃分成一些 segement绝淡,每個(gè) segement 又被劃分成一些 section。segment 的名字都是大寫的苍姜,且空間大小為頁的整數(shù)牢酵。頁的大小跟硬件有關(guān),在 arm64 架構(gòu)一頁是 16KB衙猪,其余為 4KB馍乙。
section 雖然沒有整數(shù)倍頁大小的限制,但是 section 之間不會(huì)有重疊垫释。幾乎所有 Mach-O 都包含這三個(gè)段(segment): __TEXT,__DATA
和 __LINKEDIT:__TEXT
包含 Mach header丝格,被執(zhí)行的代碼和只讀常量(如C 字符串)。只讀可執(zhí)行(r-x)饶号。__DATA
包含全局變量铁追,靜態(tài)變量等∶4可讀寫(rw-)琅束。__LINKEDIT
包含了加載程序的『元數(shù)據(jù)』,比如函數(shù)的名稱和地址算谈。只讀(r–)涩禀。Mach-O Universal 文件
FAT 二進(jìn)制文件,將多種架構(gòu)的 Mach-O 文件合并而成然眼。它通過 Fat Header 來記錄不同架構(gòu)在文件中的偏移量艾船,F(xiàn)at Header 占一頁的空間。
按分頁來存儲(chǔ)這些 segement 和 header 會(huì)浪費(fèi)空間高每,但這有利于虛擬內(nèi)存的實(shí)現(xiàn)屿岂。虛擬內(nèi)存
虛擬內(nèi)存就是一層間接尋址(indirection)。軟件工程中有句格言就是任何問題都能通過添加一個(gè)間接層來解決鲸匿。虛擬內(nèi)存解決的是管理所有進(jìn)程使用物理 RAM 的問題爷怀。通過添加間接層來讓每個(gè)進(jìn)程使用邏輯地址空間,它可以映射到 RAM 上的某個(gè)物理頁上带欢。這種映射不是一對(duì)一的运授,邏輯地址可能映射不到 RAM 上,也可能有多個(gè)邏輯地址映射到同一個(gè)物理 RAM 上乔煞。針對(duì)第一種情況吁朦,當(dāng)進(jìn)程要存儲(chǔ)邏輯地址內(nèi)容時(shí)會(huì)觸發(fā) page fault;第二種情況就是多進(jìn)程共享內(nèi)存渡贾。
對(duì)于文件可以不用一次性讀入整個(gè)文件逗宜,可以使用分頁映射(mmap()
)的方式讀取。也就是把文件某個(gè)片段映射到進(jìn)程邏輯內(nèi)存的某個(gè)頁上空骚。當(dāng)某個(gè)想要讀取的頁沒有在內(nèi)存中锦溪,就會(huì)觸發(fā) page fault,內(nèi)核只會(huì)讀入那一頁府怯,實(shí)現(xiàn)文件的懶加載刻诊。也就是說 Mach-O 文件中的 __TEXT段可以映射到多個(gè)進(jìn)程,并可以懶加載牺丙,且進(jìn)程之間共享內(nèi)存则涯。__DATA段是可讀寫的。這里使用到了 Copy-On-Write 技術(shù)冲簿,簡(jiǎn)稱 COW粟判。也就是多個(gè)進(jìn)程共享一頁內(nèi)存空間時(shí),一旦有進(jìn)程要做寫操作峦剔,它會(huì)先將這頁內(nèi)存內(nèi)容復(fù)制一份出來档礁,然后重新映射邏輯地址到新的 RAM 頁上。也就是這個(gè)進(jìn)程自己擁有了那頁內(nèi)存的拷貝吝沫。這就涉及到了 clean/dirty page 的概念呻澜。dirty page 含有進(jìn)程自己的信息递礼,而 clean page 可以被內(nèi)核重新生成(重新讀磁盤)。所以 dirty page 的代價(jià)大于 clean page羹幸。Mach-O 鏡像 加載
所以在多個(gè)進(jìn)程加載 Mach-O 鏡像時(shí) __TEXT和 __LINKEDIT因?yàn)橹蛔x脊髓,都是可以共享內(nèi)存的。而 __DATA 因?yàn)榭勺x寫栅受,就會(huì)產(chǎn)生 dirty page将硝。當(dāng) dyld 執(zhí)行結(jié)束后,__LINKEDIT 就沒用了屏镊,對(duì)應(yīng)的內(nèi)存頁會(huì)被回收依疼。安全
ASLR(Address Space Layout Randomization):地址空間布局隨機(jī)化,鏡像會(huì)在隨機(jī)的地址上加載而芥。這其實(shí)是一二十年前的舊技術(shù)了律罢。
代碼簽名:可能我們認(rèn)為 Xcode 會(huì)把整個(gè)文件都做加密 hash 并用做數(shù)字簽名。其實(shí)為了在運(yùn)行時(shí)驗(yàn)證 Mach-O 文件的簽名蔚出,并不是每次重復(fù)讀入整個(gè)文件弟翘,而是把每頁內(nèi)容都生成一個(gè)單獨(dú)的加密散列值,并存儲(chǔ)在 __LINKEDIT中骄酗。這使得文件每頁的內(nèi)容都能及時(shí)被校驗(yàn)確并保不被篡改稀余。從 exec() 到 main()
exec() 是一個(gè)系統(tǒng)調(diào)用。系統(tǒng)內(nèi)核把應(yīng)用映射到新的地址空間趋翻,且每次起始位置都是隨機(jī)的(因?yàn)槭褂?ASLR)睛琳。并將起始到 0x000000
這段范圍的進(jìn)程權(quán)限都標(biāo)記為不可讀寫不可執(zhí)行。如果是 32 位進(jìn)程踏烙,這個(gè)范圍至少是 4KB师骗;對(duì)于 64 位進(jìn)程則至少是 4GB。NULL 指針引用和指針截?cái)嗾`差都是會(huì)被它捕獲讨惩。dyld
加載 dylib 文件
Unix 的前二十年很安逸辟癌,因?yàn)槟菚r(shí)還沒有發(fā)明動(dòng)態(tài)鏈接庫。有了動(dòng)態(tài)鏈接庫后荐捻,一個(gè)用于加載鏈接庫的幫助程序被創(chuàng)建黍少。在蘋果的平臺(tái)里是 dyld,其他 Unix 系統(tǒng)也有 ld.so
处面。 當(dāng)內(nèi)核完成映射進(jìn)程的工作后會(huì)將名字為 dyld的Mach-O 文件映射到進(jìn)程中的隨機(jī)地址厂置,它將 PC 寄存器設(shè)為 dyld 的地址并運(yùn)行。dyld 在應(yīng)用進(jìn)程中運(yùn)行的工作是加載應(yīng)用依賴的所有動(dòng)態(tài)鏈接庫魂角,準(zhǔn)備好運(yùn)行所需的一切昵济,它擁有的權(quán)限跟應(yīng)用一樣。下面的步驟構(gòu)成了 dyld 的時(shí)間線:
Load dylibs -> Rebase -> Bind -> ObjC -> Initializers
加載 Dylib
從主執(zhí)行文件的 header 獲取到需要加載的所依賴動(dòng)態(tài)庫列表,而 header 早就被內(nèi)核映射過访忿。然后它需要找到每個(gè) dylib瞧栗,然后打開文件讀取文件起始位置,確保它是 Mach-O 文件醉顽。接著會(huì)找到代碼簽名并將其注冊(cè)到內(nèi)核沼溜。然后在 dylib 文件的每個(gè) segment 上調(diào)用 mmap()平挑。應(yīng)用所依賴的 dylib 文件可能會(huì)再依賴其他 dylib游添,所以 dyld 所需要加載的是動(dòng)態(tài)庫列表一個(gè)遞歸依賴的集合。一般應(yīng)用會(huì)加載 100 到 400 個(gè) dylib 文件通熄,但大部分都是系統(tǒng) dylib唆涝,它們會(huì)被預(yù)先計(jì)算和緩存起來,加載速度很快唇辨。Fix-ups
在加載所有的動(dòng)態(tài)鏈接庫之后廊酣,它們只是處在相互獨(dú)立的狀態(tài),需要將它們綁定起來赏枚,這就是 Fix-ups亡驰。代碼簽名使得我們不能修改指令,那樣就不能讓一個(gè) dylib 的調(diào)用另一個(gè) dylib饿幅。這時(shí)需要加很多間接層》踩瑁現(xiàn)代 code-gen 被叫做動(dòng)態(tài) PIC(Position Independent Code),意味著代碼可以被加載到間接的地址上栗恩。當(dāng)調(diào)用發(fā)生時(shí)透乾,code-gen 實(shí)際上會(huì)在 __DATA 段中創(chuàng)建一個(gè)指向被調(diào)用者的指針,然后加載指針并跳轉(zhuǎn)過去磕秤。所以 dyld 做的事情就是修正(fix-up)指針和數(shù)據(jù)乳乌。Fix-up 有兩種類型,rebasing 和 binding市咆。
Rebasing 和 Binding
Rebasing:在鏡像內(nèi)部調(diào)整指針的指向Binding:將指針指向鏡像外部的內(nèi)容
可以通過命令行查看 rebase 和 bind 等信息:
xcrun dyldinfo -rebase -bind -lazy_bind myapp.app/myapp
通過這個(gè)命令可以查看所有的 Fix-up汉操。rebase,bind蒙兰,weak_bind磷瘤,lazy_bind 都存儲(chǔ)在 __LINKEDIT 段中,并可通過 LC_DYLD_INFO_ONLY 查看各種信息的偏移量和大小癞己。建議用 MachOView 查看更加方便直觀膀斋。從 dyld
源碼層面簡(jiǎn)要介紹下 Rebasing 和 Binding 的流程。
ImageLoader是一個(gè)用于加載可執(zhí)行文件的基類痹雅,它負(fù)責(zé)鏈接鏡像仰担,但不關(guān)心具體文件格式,因?yàn)檫@些都交給子類去實(shí)現(xiàn)。每個(gè)可執(zhí)行文件都會(huì)對(duì)應(yīng)一個(gè) ImageLoader 實(shí)例摔蓝。ImageLoaderMachO 是用于加載 Mach-O 格式文件的 ImageLoader 子類赂苗,而 ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 都繼承于 ImageLoaderMachO,分別用于加載那些 __LINKEDIT 段為傳統(tǒng)格式和壓縮格式的 Mach-O 文件贮尉。
因?yàn)?dylib 之間有依賴關(guān)系拌滋,所以 ImageLoader 中的好多操作都是沿著依賴鏈遞歸操作的,Rebasing 和 Binding 也不例外猜谚,分別對(duì)應(yīng)著 recursiveRebase() 和 recursiveBind() 這兩個(gè)方法败砂。因?yàn)槭沁f歸,所以會(huì)自底向上地分別調(diào)用 doRebase() 和 doBind() 方法魏铅,這樣被依賴的 dylib 總是先于依賴它的 dylib 執(zhí)行 Rebasing 和 Binding昌犹。傳入 doRebase() 和 doBind() 的參數(shù)包含一個(gè) LinkContext 上下文,存儲(chǔ)了可執(zhí)行文件的一堆狀態(tài)和相關(guān)的函數(shù)览芳。
在 Rebasing 和 Binding 前會(huì)判斷是否已經(jīng) Prebinding斜姥。如果已經(jīng)進(jìn)行過預(yù)綁定(Prebinding),那就不需要 Rebasing 和 Binding 這些 Fix-up 流程了沧竟,因?yàn)橐呀?jīng)在預(yù)先綁定的地址加載好了铸敏。
ImageLoaderMachO 實(shí)例不使用預(yù)綁定會(huì)有四個(gè)原因:
Mach-O Header 中 MH_PREBOUND 標(biāo)志位為 0
鏡像加載地址有偏移(這個(gè)后面會(huì)講到)
依賴的庫有變化
鏡像使用 flat-namespace,預(yù)綁定的一部分會(huì)被忽略
LinkContext 的環(huán)境變量禁止了預(yù)綁定
ImageLoaderMachO 中 doRebase() 做的事情大致如下:
如果使用預(yù)綁定悟泵,fgImagesWithUsedPrebinding
計(jì)數(shù)加一杈笔,并 return;否則進(jìn)入第二步
如果 MH_PREBOUND 標(biāo)志位為 1(也就是可以預(yù)綁定但沒使用),且鏡像在共享內(nèi)存中魁袜,重置上下文中所有的 lazy pointer桩撮。(如果鏡像在共享內(nèi)存中,稍后會(huì)在 Binding 過程中綁定峰弹,所以無需重置)
如果鏡像加載地址偏移量為0店量,則無需 Rebasing,直接 return鞠呈;否則進(jìn)入第四步調(diào)用 rebase() 方法融师,這才是真正做 Rebasing 工作的方法。如果開啟 TEXT_RELOC_SUPPORT 宏蚁吝,會(huì)允許 rebase() 方法對(duì) __TEXT 段做寫操作來對(duì)其進(jìn)行 Fix-up旱爆。所以其實(shí) __TEXT 只讀屬性并不是絕對(duì)的。
ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 分別實(shí)現(xiàn)了自己的 doRebase() 方法窘茁。實(shí)現(xiàn)邏輯大同小異怀伦,同樣會(huì)判斷是否使用預(yù)綁定,并在真正的 Binding 工作時(shí)判斷 TEXT_RELOC_SUPPORT 宏來決定是否對(duì) __TEXT 段做寫操作山林。最后都會(huì)調(diào)用 setupLazyPointerHandler 在鏡像中設(shè)置 dyld 的 entry point房待,放在最后調(diào)用是為了讓主可執(zhí)行文件設(shè)置好 __dyld 或 __program_vars。
- Rebasing
在過去,會(huì)把 dylib 加載到指定地址桑孩,所有指針和數(shù)據(jù)對(duì)于代碼來說都是對(duì)的拜鹤,dyld 就無需做任何 fix-up 了。如今用了 ASLR 后悔將 dylib 加載到新的隨機(jī)地址(actual_address)流椒,這個(gè)隨機(jī)的地址跟代碼和數(shù)據(jù)指向的舊地址(preferred_address)會(huì)有偏差敏簿,dyld
需要修正這個(gè)偏差(slide),做法就是將 dylib 內(nèi)部的指針地址都加上這個(gè)偏移量宣虾,偏移量的計(jì)算方法如下:
Slide = actual_address - preferred_address
然后就是重復(fù)不斷地對(duì) __DATA 段中需要 rebase 的指針加上這個(gè)偏移量惯裕。這就又涉及到 page fault 和 COW。這可能會(huì)產(chǎn)生 I/O 瓶頸安岂,但因?yàn)?rebase 的順序是按地址排列的轻猖,所以從內(nèi)核的角度來看這是個(gè)有次序的任務(wù)帆吻,它會(huì)預(yù)先讀入數(shù)據(jù)域那,減少 I/O 消耗。
Binding
Binding 是處理那些指向 dylib 外部的指針猜煮,它們實(shí)際上被符號(hào)(symbol)名稱綁定次员,也就是個(gè)字符串。之前提到 __LINKEDIT
段中也存儲(chǔ)了需要 bind 的指針王带,以及指針需要指向的符號(hào)淑蔚。dyld
需要找到 symbol 對(duì)應(yīng)的實(shí)現(xiàn),這需要很多計(jì)算愕撰,去符號(hào)表里查找刹衫。找到后會(huì)將內(nèi)容存儲(chǔ)到 __DATA 段中的那個(gè)指針中。Binding 看起來計(jì)算量比 Rebasing 更大搞挣,但其實(shí)需要的 I/O 操作很少带迟,因?yàn)橹?Rebasing 已經(jīng)替 Binding 做過了。ObjC Runtime
Objective-C 中有很多數(shù)據(jù)結(jié)構(gòu)都是靠 Rebasing 和 Binding 來(fix-up)的博肋,比如 Class 中指向超類的指針和指向方法的指針伴找。
ObjC 是個(gè)動(dòng)態(tài)語言脐恩,可以用類的名字來實(shí)例化一個(gè)類的對(duì)象。這意味著 ObjC Runtime 需要維護(hù)一張映射類名與類的全局表搀继。當(dāng)加載一個(gè) dylib 時(shí),其定義的所有的類都需要被注冊(cè)到這個(gè)全局表中翠语。
C++ 中有個(gè)問題叫做易碎的基類(fragile base class)叽躯。ObjC 就沒有這個(gè)問題,因?yàn)闀?huì)在加載時(shí)通過 fix-up 動(dòng)態(tài)類中改變實(shí)例變量的偏移量肌括。
在 ObjC 中可以通過定義類別(Category)的方式改變一個(gè)類的方法点骑。有時(shí)你想要添加方法的類在另一個(gè) dylib 中,而不在你的鏡像中(也就是對(duì)系統(tǒng)或別人的類動(dòng)刀),這時(shí)也需要做些 fix-up畔况。
ObjC 中的 selector 必須是唯一的鲸鹦。Initializers
C++ 會(huì)為靜態(tài)創(chuàng)建的對(duì)象生成初始化器。而在 ObjC 中有個(gè)叫 +load
的方法跷跪,然而它被廢棄了馋嗜,現(xiàn)在建議使用 +initialize。對(duì)比詳見:http://stackoverflow.com/questions/13326435/nsobject-load-and-initialize-what-do-they-do
現(xiàn)在有了主執(zhí)行文件吵瞻,一堆 dylib葛菇,其依賴關(guān)系構(gòu)成了一張巨大的有向圖,那么執(zhí)行初始化器的順序是什么橡羞?自頂向上眯停!按照依賴關(guān)系,先加載葉子節(jié)點(diǎn)卿泽,然后逐步向上加載中間節(jié)點(diǎn)莺债,直至最后加載根節(jié)點(diǎn)。這種加載順序確保了安全性签夭,加載某個(gè) dylib 前齐邦,其所依賴的其余 dylib 文件肯定已經(jīng)被預(yù)先加載。最后 dyld 會(huì)調(diào)用 main() 函數(shù)第租。main() 會(huì)調(diào)用 UIApplicationMain()措拇。
改善啟動(dòng)時(shí)間
從點(diǎn)擊 App 圖標(biāo)到加載 App 閃屏之間會(huì)有個(gè)動(dòng)畫,我們希望 App 啟動(dòng)速度比這個(gè)動(dòng)畫更快慎宾。雖然不同設(shè)備上 App 啟動(dòng)速度不一樣丐吓,但啟動(dòng)時(shí)間最好控制在 400ms。需要注意的是啟動(dòng)時(shí)間一旦超過 20s趟据,系統(tǒng)會(huì)認(rèn)為發(fā)生了死循環(huán)并殺掉 App 進(jìn)程券犁。當(dāng)然啟動(dòng)時(shí)間最好以 App 所支持的最低配置設(shè)備為準(zhǔn)。直到 applicationWillFinishLaunching
被調(diào)動(dòng)之宿,App 才啟動(dòng)結(jié)束族操。
測(cè)量啟動(dòng)時(shí)間
Warm launch: App 和數(shù)據(jù)已經(jīng)在內(nèi)存中Cold launch: App 不在內(nèi)核緩沖存儲(chǔ)器中冷啟動(dòng)(Cold launch)耗時(shí)才是我們需要測(cè)量的重要數(shù)據(jù),為了準(zhǔn)確測(cè)量冷啟動(dòng)耗時(shí)比被,測(cè)量前需要重啟設(shè)備色难。在 main()
方法執(zhí)行前測(cè)量是很難的,好在 dyld
提供了內(nèi)建的測(cè)量方法:
在 Xcode 中 Edit scheme -> Run -> Auguments 將環(huán)境變量 DYLD_PRINT_STATISTICS 設(shè)為 1
等缀〖侠颍控制臺(tái)輸出的內(nèi)容如下:
Total pre-main time: 228.41 milliseconds (100.0%) dylib loading time: 82.35 milliseconds (36.0%) rebase/binding time: 6.12 milliseconds (2.6%) ObjC setup time: 7.82 milliseconds (3.4%) initializer time: 132.02 milliseconds (57.8%) slowest intializers : libSystem.B.dylib : 122.07 milliseconds (53.4%) CoreFoundation : 5.59 milliseconds (2.4%)
優(yōu)化啟動(dòng)時(shí)間
可以針對(duì) App 啟動(dòng)前的每個(gè)步驟進(jìn)行相應(yīng)的優(yōu)化工作。
加載 Dylib之前提到過加載系統(tǒng)的 dylib 很快尺迂,因?yàn)橛袃?yōu)化笤妙。但加載內(nèi)嵌(embedded)的 dylib 文件很占時(shí)間冒掌,所以盡可能把多個(gè)內(nèi)嵌 dylib 合并成一個(gè)來加載,或者使用 static archive蹲盘。使用 dlopen() 來在運(yùn)行時(shí)懶加載是不建議的股毫,這么做可能會(huì)帶來一些問題,并且總的開銷更大召衔。
Rebase/Binding
之前提過 Rebaing 消耗了大量時(shí)間在 I/O 上铃诬,而在之后的 Binding 就不怎么需要 I/O 了,而是將時(shí)間耗費(fèi)在計(jì)算上苍凛。所以這兩個(gè)步驟的耗時(shí)是混在一起的趣席。之前說過可以從查看 __DATA 段中需要修正(fix-up)的指針,所以減少指針數(shù)量才會(huì)減少這部分工作的耗時(shí)醇蝴。對(duì)于 ObjC 來說就是減少 Class,selector 和 category 這些元數(shù)據(jù)的數(shù)量宣肚。從編碼原則和設(shè)計(jì)模式之類的理論都會(huì)鼓勵(lì)大家多寫精致短小的類和方法,并將每部分方法獨(dú)立出一個(gè)類別悠栓,其實(shí)這會(huì)增加啟動(dòng)時(shí)間霉涨。對(duì)于 C++ 來說需要減少虛方法,因?yàn)樘摲椒〞?huì)創(chuàng)建 vtable闸迷,這也會(huì)在 __DATA 段中創(chuàng)建結(jié)構(gòu)嵌纲。雖然 C++ 虛方法對(duì)啟動(dòng)耗時(shí)的增加要比 ObjC 元數(shù)據(jù)要少,但依然不可忽視腥沽。最后推薦使用 Swift 結(jié)構(gòu)體,它需要 fix-up 的內(nèi)容較少鸠蚪。
ObjC Setup
針對(duì)這步所能事情很少今阳,幾乎都靠 Rebasing 和 Binding 步驟中減少所需 fix-up 內(nèi)容。因?yàn)榍懊娴墓ぷ饕矔?huì)使得這步耗時(shí)減少茅信。
Initializer
顯式初始化
使用 +initialize 來替代 +load
不要使用 atribute((constructor)) 將方法顯式標(biāo)記為初始化器盾舌,而是讓初始化方法調(diào)用時(shí)才執(zhí)行。比如使用 dispatch_once(),pthread_once() 或 std::once()蘸鲸。也就是在第一次使用時(shí)才初始化妖谴,推遲了一部分工作耗時(shí)。
隱式初始化
對(duì)于帶有復(fù)雜(non-trivial)構(gòu)造器的 C++ 靜態(tài)變量:
在調(diào)用的地方使用初始化器酌摇。
只用簡(jiǎn)單值類型賦值(POD:Plain Old Data)膝舅,這樣靜態(tài)鏈接器會(huì)預(yù)先計(jì)算 __DATA 中的數(shù)據(jù),無需再進(jìn)行 fix-up 工作窑多。
使用編譯器 warning 標(biāo)志 -Wglobal-constructors 來發(fā)現(xiàn)隱式初始化代碼仍稀。
使用 Swift 重寫代碼,因?yàn)?Swift 已經(jīng)預(yù)先處理好了埂息,強(qiáng)力推薦技潘。
不要在初始化方法中調(diào)用 dlopen()遥巴,對(duì)性能有影響。因?yàn)?dyld 在 App 開始前運(yùn)行享幽,由于此時(shí)是單線程運(yùn)行所以系統(tǒng)會(huì)取消加鎖铲掐,但 dlopen() 開啟了多線程,系統(tǒng)不得不加鎖值桩,這就嚴(yán)重影響了性能迹炼,還可能會(huì)造成死鎖以及產(chǎn)生未知的后果。所以也不要在初始化器中創(chuàng)建線程颠毙。
Reference
http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time
/https://developer.apple.com/videos/play/wwdc2016/406/