這是一篇 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ù)語(yǔ)
Mach-O 是針對(duì)不同運(yùn)行時(shí)可執(zhí)行文件的文件類型灼狰。
文件類型:
Executable: 應(yīng)用的主要二進(jìn)制
Dylib: 動(dòng)態(tài)鏈接庫(kù)(又稱 DSO 或 DLL)
Bundle: 不能被鏈接的 Dylib,只能在運(yùn)行時(shí)使用dlopen()加載浮禾,可當(dāng)做 macOS 的插件交胚。
Image: executable,dylib 或 bundle
Framework: 包含 Dylib 以及資源文件和頭文件的文件夾
Mach-O 鏡像文件
Mach-O 被劃分成一些 segement伐厌,每個(gè) segement 又被劃分成一些 section承绸。
segment 的名字都是大寫的,且空間大小為頁(yè)的整數(shù)挣轨。頁(yè)的大小跟硬件有關(guān)军熏,在 arm64 架構(gòu)一頁(yè)是 16KB,其余為 4KB卷扮。
section 雖然沒(méi)有整數(shù)倍頁(yè)大小的限制荡澎,但是 section 之間不會(huì)有重疊。
幾乎所有 Mach-O 都包含這三個(gè)段(segment):__TEXT,__DATA和__LINKEDIT:
__TEXT包含 Mach header晤锹,被執(zhí)行的代碼和只讀常量(如C 字符串)摩幔。只讀可執(zhí)行(r-x)。
__DATA包含全局變量鞭铆,靜態(tài)變量等或衡。可讀寫(rw-)车遂。
__LINKEDIT包含了加載程序的『元數(shù)據(jù)』封断,比如函數(shù)的名稱和地址。只讀(r–)舶担。
Mach-O Universal 文件
FAT 二進(jìn)制文件坡疼,將多種架構(gòu)的 Mach-O 文件合并而成。它通過(guò) Fat Header 來(lái)記錄不同架構(gòu)在文件中的偏移量衣陶,F(xiàn)at Header 占一頁(yè)的空間柄瑰。
按分頁(yè)來(lái)存儲(chǔ)這些 segement 和 header 會(huì)浪費(fèi)空間,但這有利于虛擬內(nèi)存的實(shí)現(xiàn)剪况。
虛擬內(nèi)存
虛擬內(nèi)存就是一層間接尋址(indirection)教沾。軟件工程中有句格言就是任何問(wèn)題都能通過(guò)添加一個(gè)間接層來(lái)解決。虛擬內(nèi)存解決的是管理所有進(jìn)程使用物理 RAM 的問(wèn)題拯欧。通過(guò)添加間接層來(lái)讓每個(gè)進(jìn)程使用邏輯地址空間详囤,它可以映射到 RAM 上的某個(gè)物理頁(yè)上。這種映射不是一對(duì)一的,邏輯地址可能映射不到 RAM 上藏姐,也可能有多個(gè)邏輯地址映射到同一個(gè)物理 RAM 上隆箩。針對(duì)第一種情況,當(dāng)進(jìn)程要存儲(chǔ)邏輯地址內(nèi)容時(shí)會(huì)觸發(fā) page fault羔杨;第二種情況就是多進(jìn)程共享內(nèi)存捌臊。
對(duì)于文件可以不用一次性讀入整個(gè)文件,可以使用分頁(yè)映射(mmap())的方式讀取兜材。也就是把文件某個(gè)片段映射到進(jìn)程邏輯內(nèi)存的某個(gè)頁(yè)上理澎。當(dāng)某個(gè)想要讀取的頁(yè)沒(méi)有在內(nèi)存中,就會(huì)觸發(fā) page fault曙寡,內(nèi)核只會(huì)讀入那一頁(yè)糠爬,實(shí)現(xiàn)文件的懶加載。
也就是說(shuō) Mach-O 文件中的__TEXT段可以映射到多個(gè)進(jìn)程举庶,并可以懶加載执隧,且進(jìn)程之間共享內(nèi)存。__DATA段是可讀寫的户侥。這里使用到了 Copy-On-Write 技術(shù)镀琉,簡(jiǎn)稱 COW。也就是多個(gè)進(jìn)程共享一頁(yè)內(nèi)存空間時(shí)蕊唐,一旦有進(jìn)程要做寫操作屋摔,它會(huì)先將這頁(yè)內(nèi)存內(nèi)容復(fù)制一份出來(lái),然后重新映射邏輯地址到新的 RAM 頁(yè)上替梨。也就是這個(gè)進(jìn)程自己擁有了那頁(yè)內(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就沒(méi)用了串稀,對(duì)應(yīng)的內(nèi)存頁(yè)會(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è)文件喘漏,而是把每頁(yè)內(nèi)容都生成一個(gè)單獨(dú)的加密散列值护蝶,并存儲(chǔ)在__LINKEDIT中。這使得文件每頁(yè)的內(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í)還沒(méi)有發(fā)明動(dòng)態(tài)鏈接庫(kù)盲链。有了動(dòng)態(tài)鏈接庫(kù)后,一個(gè)用于加載鏈接庫(kù)的幫助程序被創(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)鏈接庫(kù),準(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)庫(kù)列表,而 header 早就被內(nèi)核映射過(guò)忙菠。然后它需要找到每個(gè) dylib何鸡,然后打開(kāi)文件讀取文件起始位置,確保它是 Mach-O 文件牛欢。接著會(huì)找到代碼簽名并將其注冊(cè)到內(nèi)核骡男。然后在 dylib 文件的每個(gè) segment 上調(diào)用mmap()。應(yīng)用所依賴的 dylib 文件可能會(huì)再依賴其他 dylib傍睹,所以dyld所需要加載的是動(dòng)態(tài)庫(kù)列表一個(gè)遞歸依賴的集合隔盛。一般應(yīng)用會(huì)加載 100 到 400 個(gè) dylib 文件,但大部分都是系統(tǒng) dylib拾稳,它們會(huì)被預(yù)先計(jì)算和緩存起來(lái)吮炕,加載速度很快。
Fix-ups
在加載所有的動(dòng)態(tài)鏈接庫(kù)之后访得,它們只是處在相互獨(dú)立的狀態(tài)龙亲,需要將它們綁定起來(lá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)過(guò)去珊豹。
所以dyld做的事情就是修正(fix-up)指針和數(shù)據(jù)。Fix-up 有兩種類型榕订,rebasing 和 binding店茶。
Rebasing 和 Binding
Rebasing:在鏡像內(nèi)部調(diào)整指針的指向
Binding:將指針指向鏡像外部的內(nèi)容
可以通過(guò)命令行查看 rebase 和 bind 等信息:
xcrundyldinfo -rebase -bind -lazy_bind myapp.app/myapp
通過(guò)這個(gè)命令可以查看所有的 Fix-up。rebase劫恒,bind贩幻,weak_bind,lazy_bind 都存儲(chǔ)在__LINKEDIT段中两嘴,并可通過(guò)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)著recursiveBind()和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)行過(guò)預(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ì)講到)
依賴的庫(kù)有變化
鏡像使用 flat-namespace犬钢,預(yù)綁定的一部分會(huì)被忽略
LinkContext的環(huán)境變量禁止了預(yù)綁定
ImageLoaderMachO中doRebase()做的事情大致如下:
如果使用預(yù)綁定,fgImagesWithUsedPrebinding計(jì)數(shù)加一思灰,并return;否則進(jìn)入第二步
如果MH_PREBOUND標(biāo)志位為1(也就是可以預(yù)綁定但沒(méi)使用)玷犹,且鏡像在共享內(nèi)存中,重置上下文中所有的 lazy pointer洒疚。(如果鏡像在共享內(nèi)存中歹颓,稍后會(huì)在 Binding 過(guò)程中綁定,所以無(wú)需重置)
如果鏡像加載地址偏移量為0油湖,則無(wú)需 Rebasing巍扛,直接return;否則進(jìn)入第四步
調(diào)用rebase()方法乏德,這才是真正做 Rebasing 工作的方法撤奸。如果開(kāi)啟TEXT_RELOC_SUPPORT宏,會(huì)允許rebase()方法對(duì)__TEXT段做寫操作來(lái)對(duì)其進(jìn)行 Fix-up喊括。所以其實(shí)__TEXT只讀屬性并不是絕對(duì)的胧瓜。
ImageLoaderMachOClassic和ImageLoaderMachOCompressed分別實(shí)現(xiàn)了自己的doRebase()方法疚漆。實(shí)現(xiàn)邏輯大同小異帐要,同樣會(huì)判斷是否使用預(yù)綁定,并在真正的 Binding 工作時(shí)判斷TEXT_RELOC_SUPPORT宏來(lái)決定是否對(duì)__TEXT段做寫操作帖鸦。最后都會(huì)調(diào)用setupLazyPointerHandler在鏡像中設(shè)置dyld的 entry point蹦误,放在最后調(diào)用是為了讓主可執(zhí)行文件設(shè)置好__dyld或__program_vars劫拢。
Rebasing
在過(guò)去,會(huì)把 dylib 加載到指定地址强胰,所有指針和數(shù)據(jù)對(duì)于代碼來(lái)說(shuō)都是對(duì)的舱沧,dyld就無(wú)需做任何 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)核的角度來(lá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 看起來(lái)計(jì)算量比 Rebasing 更大祸泪,但其實(shí)需要的 I/O 操作很少,因?yàn)橹?Rebasing 已經(jīng)替 Binding 做過(guò)了藤树。
ObjC Runtime
Objective-C 中有很多數(shù)據(jù)結(jié)構(gòu)都是靠 Rebasing 和 Binding 來(lái)修正(fix-up)的浴滴,比如Class中指向超類的指針和指向方法的指針。
ObjC 是個(gè)動(dòng)態(tài)語(yǔ)言岁钓,可以用類的名字來(lái)實(shí)例化一個(gè)類的對(duì)象升略。這意味著 ObjC Runtime 需要維護(hù)一張映射類名與類的全局表。當(dāng)加載一個(gè) dylib 時(shí)屡限,其定義的所有的類都需要被注冊(cè)到這個(gè)全局表中品嚣。
C++ 中有個(gè)問(wèn)題叫做易碎的基類(fragile base class)。ObjC 就沒(méi)有這個(gè)問(wèn)題钧大,因?yàn)闀?huì)在加載時(shí)通過(guò) fix-up 動(dòng)態(tài)類中改變實(shí)例變量的偏移量翰撑。
在 ObjC 中可以通過(guò)定義類別(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ì)比詳見(jiàn):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í)間一旦超過(guò) 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-maintime:228.41milliseconds (100.0%)
dylib loadingtime:82.35milliseconds (36.0%)
rebase/bindingtime:6.12milliseconds (2.6%)
ObjC setuptime:7.82milliseconds (3.4%)
initializertime:132.02milliseconds (57.8%)
slowestintializers :libSystem.B.dylib:122.07milliseconds (53.4%)
CoreFoundation:5.59milliseconds (2.4%)
優(yōu)化啟動(dòng)時(shí)間
可以針對(duì) App 啟動(dòng)前的每個(gè)步驟進(jìn)行相應(yīng)的優(yōu)化工作舷胜。
加載 Dylib
之前提到過(guò)加載系統(tǒng)的 dylib 很快,因?yàn)橛袃?yōu)化活翩。但加載內(nèi)嵌(embedded)的 dylib 文件很占時(shí)間烹骨,所以盡可能把多個(gè)內(nèi)嵌 dylib 合并成一個(gè)來(lái)加載,或者使用 static archive材泄。使用dlopen()來(lái)在運(yùn)行時(shí)懶加載是不建議的沮焕,這么做可能會(huì)帶來(lái)一些問(wèn)題,并且總的開(kāi)銷更大脸爱。
Rebase/Binding
之前提過(guò) Rebaing 消耗了大量時(shí)間在 I/O 上遇汞,而在之后的 Binding 就不怎么需要 I/O 了,而是將時(shí)間耗費(fèi)在計(jì)算上簿废。所以這兩個(gè)步驟的耗時(shí)是混在一起的空入。
之前說(shuō)過(guò)可以從查看__DATA段中需要修正(fix-up)的指針,所以減少指針數(shù)量才會(huì)減少這部分工作的耗時(shí)族檬。對(duì)于 ObjC 來(lái)說(shuō)就是減少Class,selector和category這些元數(shù)據(jù)的數(shù)量歪赢。從編碼原則和設(shè)計(jì)模式之類的理論都會(huì)鼓勵(lì)大家多寫精致短小的類和方法,并將每部分方法獨(dú)立出一個(gè)類別单料,其實(shí)這會(huì)增加啟動(dòng)時(shí)間埋凯。對(duì)于 C++ 來(lái)說(shuō)需要減少虛方法点楼,因?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來(lái)替代+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ù)线梗,無(wú)需再進(jìn)行 fix-up 工作椰于。
使用編譯器 warning 標(biāo)志-Wglobal-constructors來(lái)發(fā)現(xiàn)隱式初始化代碼。
使用 Swift 重寫代碼仪搔,因?yàn)?Swift 已經(jīng)預(yù)先處理好了瘾婿,強(qiáng)力推薦。
不要在初始化方法中調(diào)用dlopen()烤咧,對(duì)性能有影響偏陪。因?yàn)閐yld在 App 開(kāi)始前運(yùn)行,由于此時(shí)是單線程運(yùn)行所以系統(tǒng)會(huì)取消加鎖煮嫌,但dlopen()開(kāi)啟了多線程笛谦,系統(tǒng)不得不加鎖,這就嚴(yán)重影響了性能昌阿,還可能會(huì)造成死鎖以及產(chǎn)生未知的后果饥脑。所以也不要在初始化器中創(chuàng)建線程。