這篇文章首發(fā)在公司微信技術(shù)公眾號(hào):京東金融技術(shù)說
「摘要」每個(gè)iOS app從點(diǎn)擊啟動(dòng)到首頁面加載渲染完成惰赋,雖然時(shí)間比較短暫番川,但系統(tǒng)進(jìn)行了不少重要的操作船庇,比如會(huì)加載100-400個(gè)支撐app后續(xù)運(yùn)行的dylib鞭盟。本文從Dylib、Mach-O等基本運(yùn)行時(shí)文件說起,介紹了iOS 的Virtual Memory劈彪,以及從exec()到main()之間經(jīng)歷的主要階段:Dylibs Loading竣蹦、Rebasing、Binding沧奴、ObjC Runtime痘括、Initializer。然后從理論升級(jí)到實(shí)踐滔吠,針對(duì)啟動(dòng)中的各個(gè)環(huán)節(jié)分析如何更進(jìn)一步提升app的啟動(dòng)速度纲菌。
一、Mach-O
相關(guān)術(shù)語說明(文件類型)
Executable—iOS 應(yīng)用程序的主要二進(jìn)制文件
Dylib—Dynamic library疮绷,動(dòng)態(tài)庫 (其他平臺(tái)中叫DSO 或者 DLL)
Bundle—可以看做是不能鏈接的Dylib,只能使用dlopen()來加載,就像插件一樣
Image— 泛指 Executable,Dylib或者Bundle
Framework—包含Dylib翰舌、圖片等資源、頭文件(.h)的特定結(jié)構(gòu)的目錄文件
1冬骚、Mach-O Image
Image 被分割成多個(gè) segment灶芝。segment一般用大寫字母來命名,其大小一般是 page 大小的整數(shù)倍(arm64環(huán)境下1page是16KB唉韭,其它環(huán)境下是4KB)。如下圖犯犁,一個(gè)Image通常由三個(gè)segment組成属愤,分別是__TEXT(3page)、__DATA(1page)酸役、__LINKEDIT(1page)住诸。
Section 是Segment所包含的一部分,一般用小寫字母命名涣澡。Section的大小沒有是page整數(shù)倍的要求贱呐,但它是不可被覆蓋的,同時(shí)它也是被編譯器忽略的入桂。如下圖奄薇,__TEXT是文件的開始,包含了Machheader抗愁,主要有對(duì)應(yīng)硬件環(huán)境信息的說明馁蒂,只讀的常量(如C字符串)。__DATA是可讀亦可寫的蜘腌,主要包含一些全局變量沫屡,靜態(tài)變量。__LINKEDIT并不包含全局變量的函數(shù)/方法(function)撮珠,而是包含函數(shù)/方法(function)的一些信息沮脖,比如其名字,地址等等。
2勺届、Mach-O universal file(通常稱為胖二進(jìn)制文件)
由于硬件在快速的升級(jí)換代驶俊,之前是32位的機(jī)器,現(xiàn)在已經(jīng)有好多的64位機(jī)器涮因。并且废睦,每一代的iPhone,其CPU架構(gòu)都不完全相同养泡,有i386嗜湃,armv7,armv7s澜掩,arm64购披。為了讓同一份代碼可以部署到不同的環(huán)境下,就需要一種通用的可執(zhí)行文件了—Mach-O universal file肩榕。如下圖刚陡,將armv7s和arm64下的Mac-O文件合并,形成一個(gè)新的Mach-O universal file株汉,其包含了一個(gè)Fat Header筐乳。Fat Header中包含了所支持的架構(gòu)列表以及其在文件中的偏移量(offsets)。
二乔妈、Virtual Memory
Virtual Memory 是一個(gè)中間層蝙云,用于將每個(gè)進(jìn)程(process)的邏輯地址空間映射到物理RAM上(以page的粒度)。VM有如下的特點(diǎn):
某些邏輯地址沒有映射到具體物理RAM上路召,內(nèi)核訪問該地址時(shí)勃刨,會(huì)出現(xiàn)Pagefault
多個(gè)process可以映射到相同的一個(gè)page 上
可以實(shí)現(xiàn)對(duì)文件的延遲讀取
對(duì)于共享的page可以進(jìn)行 Copy-On-Write
Copy-On-Write 會(huì)導(dǎo)致Dirtypage,所以需要pageclean
RAM的權(quán)限(rwx)與映射權(quán)限的聯(lián)系
1股淡、Mach-O ImageLoading
結(jié)合上面的Dylib和VM的基本知識(shí)身隐,Mach-O Image 的加載過程可通過下圖來展示出來:
兩個(gè)重要的安全因素對(duì)Image Loading的影響:
一個(gè)是 ASLR:(address space layout randomization),為了防止Image 每次都被加載到同一個(gè)物理RAM上而被惡意利用,Image每次分配到的地址偏移量是隨機(jī)的唯灵。
另一個(gè)是 code sign:這個(gè)使用XCode編譯打包過的人應(yīng)該非常熟悉了贾铝,和編譯期不同的是,每一個(gè)page的Mach-O都有自己的簽名埠帕,這些簽名信息保存在__LINKEDIT中忌傻。
三、從exex() 到 main()
exec()是一個(gè)系統(tǒng)級(jí)別的調(diào)用搞监,當(dāng)點(diǎn)擊appicon或者不同app間切換時(shí)水孩,會(huì)觸發(fā)它。對(duì)于該app琐驴,exec()主要進(jìn)行如下的操作:
將app映射到一個(gè)隨機(jī)的地址空間
app的起始地址是隨機(jī)的
將起始地址和0x000000之間的地址空間俘种,對(duì)該app標(biāo)記為不可操作(不可讀秤标,不可寫,不可執(zhí)行)
捕獲空指針的使用
捕獲指針缺失的錯(cuò)誤
1宙刘、加載Dylibs
接下來苍姜,Dyld(dynamicloader)會(huì)開始加載app中的dylib,主要步驟如下:
從胖二進(jìn)制文件中的header中解析app所依賴的dylibs列表
找到該環(huán)境下所需的mach-O 文件
打開每一個(gè)dylib,讀取其頭部悬包,進(jìn)行驗(yàn)證衙猪,看是否是mach-O格式,然后找到代碼簽名布近,并將代碼簽名注冊(cè)到內(nèi)核
調(diào)用mmap()垫释,對(duì)所有的segment進(jìn)行映射
現(xiàn)在,所有的一級(jí)dylib都加載了撑瞧,但是有的dylib依賴于其他dylib棵譬,甚至同一dylib被多個(gè)dylib依賴。所以需要進(jìn)行遞歸的加載预伺,一直到所有的都加載完成
通常一個(gè)app會(huì)加載100-400個(gè)dylib订咸,這其中大部分是操作系統(tǒng)的,系統(tǒng)本身已經(jīng)對(duì)這些dylib的加載進(jìn)行了優(yōu)化
上述過程如下圖:
2、Rebasing 和 Binding
每個(gè)dylib分配到的地址空間也是隨機(jī)的酬诀,也就是其起始地址會(huì)不斷的變化脏嚷,或者說是滑動(dòng)(slide)。
Rebasing就是在發(fā)生變化時(shí)瞒御,對(duì)內(nèi)部的指針按照新的偏移量進(jìn)行校正然眼。而binding是對(duì)指向外部的指針進(jìn)行校正。校正后的地址信息都是保存在__LINKEDIT中葵腹。
可以使用 dyldinfo 命令來查看任意 dylib 的Rebasing和Binding信息:
2、通知 ObjC Runtime
大多數(shù)的ObjC設(shè)置都通過Rebasing和Binding完成了屿岂,比如注冊(cè)O(shè)bjC class 聲明践宴,將Category中聲明的方法插入到方法列表中。之后爷怀,Runtime啟動(dòng)并開始初始化操作
3阻肩、Initializer
如果是C++,靜態(tài)對(duì)象的initializer開始執(zhí)行运授。對(duì)于ObjC烤惊,+load 方法被調(diào)用。其她的+方法也開始被調(diào)用吁朦。由于+方法之間會(huì)存在繼承調(diào)用關(guān)系柒室,所以,整個(gè)+方法的被調(diào)用順序是由下向上的逗宜。
接下來雄右,app中的main()方法會(huì)被調(diào)用空骚,main()被調(diào)用前經(jīng)歷的TimeLine:
四、從理論到實(shí)踐-如何提升啟動(dòng)時(shí)間擂仍?
1囤屹、怎樣的速度算快
啟動(dòng)速度應(yīng)當(dāng)比閃屏動(dòng)畫速度快,不同的硬件設(shè)備逢渔、系統(tǒng)環(huán)境下肋坚,啟動(dòng)速度都不相同,400ms是一個(gè)比較合理的目標(biāo)肃廓,啟動(dòng)時(shí)間不能超過20 秒智厌,超過了會(huì)被系統(tǒng)Kill掉
需要在低性能設(shè)備上做測試。
2亿昏、回顧一下整個(gè)啟動(dòng)堆棧
Parse ?images
Map ?images
Rebase ?images
Bind ?images
Run ?imageinitializers
Call ?main()
Call ?UIApplicationMain()
CallapplicationWillFinishLaunching
Call applicationdidFinishLaunchingWithOptions(iOSer 寫應(yīng)用程序的入口)
3峦剔、冷啟動(dòng)、熱啟動(dòng)角钩,檢測啟動(dòng)時(shí)間
熱啟動(dòng)是指app啟動(dòng)之前吝沫,app執(zhí)行文件和相關(guān)的數(shù)據(jù)已經(jīng)在緩存中。冷啟動(dòng)則相反递礼,內(nèi)核緩存中沒有任何數(shù)據(jù)惨险,所以對(duì)于冷啟動(dòng)來說,時(shí)間更為重要脊髓。冷啟動(dòng)的使用場景是辫愉,更新系統(tǒng)后第一次打開app,或者很長時(shí)間沒有使用過app将硝,再次打開恭朗。
之前,檢測main()之前的時(shí)間消耗是非常困難的依疼,在最新的iOS10系統(tǒng)痰腮,嵌入了新功能,可以方便的測試啟動(dòng)時(shí)間:
4律罢、dylib 加載階段
盡量減少dylib 的數(shù)量膀值,可以合并多個(gè)dylib,或者將代碼直接放在executable中误辑。
使用dlopen來延遲加載沧踏,雖然使用dlopen會(huì)導(dǎo)致額外的開銷,甚至大于在啟動(dòng)時(shí)加載的巾钉,但它是延遲的翘狱。(注:近一年內(nèi)Hot patch非常火熱砰苍,由于Hot patch本身的機(jī)制和安全問題盒蟆,導(dǎo)致蘋果開始封殺踏烙,dlopen就被加入了禁用名單了)
從架構(gòu)的角度,把a(bǔ)pp中的一些共用功能以dylib的形式模塊化历等,可以降低耦合讨惩,提升開發(fā)效率。但是過多的話寒屯,會(huì)影響啟動(dòng)時(shí)間荐捻。
5、Rebasing和Binding階段
Rebasing和Binding階段寡夹,會(huì)產(chǎn)生IO和一些計(jì)算消耗处面,可以通過如下方案優(yōu)化:
減少__DATA 中的指針數(shù)量可以減少處理時(shí)間
減少Objective-C的元數(shù)據(jù),如Classes菩掏,selectors魂角,categores
減少C++中的虛方法
使用swift 的 struct
6、Initializer階段
ObjC +load方法替換成+initialize 形式的:C/C++中的__attribute__((constructor)) 換成 dispatch_once()智绸,pthread_once(),std:once()
不要在initialize中使用 dlopen
不要在initialize中創(chuàng)建線程
使用swift