1. 啟動(dòng)時(shí)間檢測
這節(jié)課主要針對的是:
- 冷啟動(dòng):在內(nèi)存中不包含相關(guān)的數(shù)據(jù),必須從磁盤載入到內(nèi)存中,這是由系統(tǒng)決定的,但是當(dāng)APP的內(nèi)存被覆蓋的時(shí)候,APP也能決定
對應(yīng)的熱啟動(dòng)則是:殺掉進(jìn)程以后,數(shù)據(jù)仍然在,啟動(dòng)APP的時(shí)候不用重新加載數(shù)據(jù)
- 測試APP啟動(dòng)以
main函數(shù)為分界線,main函數(shù)之前的監(jiān)測一定要靠系統(tǒng)反饋,main函數(shù)以后的則可以監(jiān)測,這節(jié)課主要針對main函數(shù)之前的優(yōu)化,因?yàn)?main函數(shù)之后的操作是不統(tǒng)一的,根據(jù)業(yè)務(wù)來寫的。
如果想要了解 main 函數(shù)之前的耗時(shí),是可以通過以下配置來監(jiān)測,iOS APP的啟動(dòng)流程可以通過在xcode的Edit Scheme ->Run->Arguments -> Environment Variables中加入環(huán)境變量DYLD_PRINT_STATISTICS打印出來:

運(yùn)行APP,打印結(jié)果如下:
Total pre-main time: 31.70 milliseconds (100.0%)
dylib loading time: 27.29 milliseconds (86.0%)
rebase/binding time: 411015771.5 seconds (61552300.3%)
ObjC setup time: 78.12 milliseconds (246.4%)
initializer time: 74.97 milliseconds (236.4%)
slowest intializers :
libSystem.B.dylib : 6.10 milliseconds (19.2%)
libBacktraceRecording.dylib : 7.03 milliseconds (22.1%)
libobjc.A.dylib : 1.69 milliseconds (5.3%)
libMainThreadChecker.dylib : 57.82 milliseconds (182.3%)
從結(jié)果可以看出四個(gè)階段的名稱以及它們對應(yīng)的耗時(shí)。
dylib loading 階段是動(dòng)態(tài)鏈接庫dylib加載動(dòng)態(tài)庫的階段,包括系統(tǒng)動(dòng)態(tài)庫和我們自己的動(dòng)態(tài)庫。
rebase/binding這個(gè)階段實(shí)際就是兩個(gè)操作rebase和binding。rebase就是內(nèi)部符號(hào)偏移修正,binding是外部符號(hào)綁定。
ObjC setup這一階段主要是OC類相關(guān)的事務(wù),比如類的注冊,category、protocol的讀取等等。
intializers 程序的初始化,包括所依賴的動(dòng)態(tài)庫的初始化。在這期間會(huì)調(diào)用 Objc 類的 + load 函數(shù),調(diào)用 C++ 中帶有constructor 標(biāo)記的函數(shù)等。
上面打印出的這四個(gè)階段還是比較粗略的,實(shí)際上dyld在啟動(dòng)過程中是比較復(fù)雜的。如果將上面的DYLD_PRINT_STATISTICS換成DYLD_PRINT_STATISTICS_DETAILS,打印得會(huì)更加詳細(xì):
total time: 1.3 seconds (100.0%)
total images loaded: 398 (392 from dyld shared cache)
total segments mapped: 21, into 415 pages
total images loading time: 944.18 milliseconds (68.5%)
total load time in ObjC: 80.88 milliseconds (5.8%)
total debugger pause time: 801.87 milliseconds (58.2%)
total dtrace DOF registration time: 0.00 milliseconds (0.0%)
total rebase fixups: 16,279
total rebase fixups time: 7.47 milliseconds (0.5%)
total binding fixups: 567,346
total binding fixups time: 275.57 milliseconds (20.0%)
total weak binding fixups time: 0.03 milliseconds (0.0%)
total redo shared cached bindings time: 459.30 milliseconds (33.3%)
total bindings lazily fixed up: 0 of 0
total time in initializers and ObjC +load: 68.67 milliseconds (4.9%)
libSystem.B.dylib : 6.04 milliseconds (0.4%)
libBacktraceRecording.dylib : 7.01 milliseconds (0.5%)
libMainThreadChecker.dylib : 52.05 milliseconds (3.7%)
total symbol trie searches: 1378030
total symbol table binary searches: 0
total images defining weak symbols: 50
total images using weak symbols: 110
main 函數(shù)之后的優(yōu)化有幾個(gè)點(diǎn):
- 懶加載
- 刪除不再使用的代碼,即便那些代碼不在使用,但在它們?nèi)匀粎⑴c到了編譯中,因此也會(huì)消耗一部分時(shí)間
- 首屏頁面最好不要使用 Xib 和 StoryBoard,因?yàn)樗鼈儽旧砭托枰馕?/li>
2. 啟動(dòng)優(yōu)化:二進(jìn)制重排
二進(jìn)制重排原理:虛擬內(nèi)存和物理內(nèi)存,早期的計(jì)算機(jī)只有物理內(nèi)存,內(nèi)存條的地址叫做物理地址,早期的數(shù)據(jù)訪問都是通過物理地址來訪問的,這樣子有個(gè)問題就是拿到內(nèi)存地址就可以對數(shù)據(jù)進(jìn)行修改,因此存在數(shù)據(jù)問題和內(nèi)存不夠用的問題。操作系統(tǒng)發(fā)現(xiàn)軟件雖然很大,但是實(shí)際上用戶只使用了部分活躍功能,所以把應(yīng)用的全部數(shù)據(jù)載入內(nèi)存有點(diǎn)浪費(fèi),因此虛擬內(nèi)存就出現(xiàn)了,虛擬內(nèi)存就是讓應(yīng)用覺得自己被全部載入了內(nèi)存,實(shí)際上并沒有,每個(gè)應(yīng)用都有一個(gè)虛擬的內(nèi)存,cpu首先訪問虛擬地址,虛擬地址和物理地址會(huì)有一個(gè)對應(yīng)
物理內(nèi)存 & 虛擬內(nèi)存
物理內(nèi)存:指的是通過物理內(nèi)存條獲得的內(nèi)存空間。
虛擬內(nèi)存:跟物理內(nèi)存相反,虛擬內(nèi)存指的一種計(jì)算機(jī)系統(tǒng)內(nèi)存管理技術(shù),它使得應(yīng)用程序認(rèn)為它擁有連續(xù)可用的內(nèi)存,實(shí)際上它通常被分隔成多個(gè)物理內(nèi)存碎片。
只談概念太空洞,下面我們用圖來解釋什么是物理內(nèi)存、什么是虛擬內(nèi)存。
物理內(nèi)存的概述圖如下:

分析:
在沒有虛擬內(nèi)存的概念之前,每個(gè)應(yīng)用一啟動(dòng),操作系統(tǒng)就會(huì)把整個(gè)應(yīng)用放進(jìn)物理內(nèi)存里面
物理內(nèi)存存在的問題:
- 內(nèi)存緊張 - 由于每次都是直接把整個(gè)應(yīng)用放進(jìn)物理內(nèi)存里面,很可能出現(xiàn)內(nèi)存不夠用的情況。
- 進(jìn)程安全 - 每個(gè)進(jìn)程之間的物理地址是連續(xù)的,可以拿到別的進(jìn)程的地址,容易出現(xiàn)進(jìn)程不安全的問題。
虛擬內(nèi)存的概述圖如下:

分析:
虛擬內(nèi)存的技術(shù)出現(xiàn)之后,每個(gè)進(jìn)程并不是直接全部扔進(jìn)物理內(nèi)存,而是給每個(gè)應(yīng)用分配一個(gè)虛擬的內(nèi)存,虛擬內(nèi)存通過虛擬頁表來把相應(yīng)數(shù)據(jù)放進(jìn)物理內(nèi)存里面。
虛擬內(nèi)存的技術(shù)出現(xiàn)之后,也有了內(nèi)存分頁的概念,虛擬頁表把一個(gè)進(jìn)程分成若干頁,比如:Page1、Page2、Page3......,當(dāng)啟動(dòng)進(jìn)程1的時(shí)候,只需要把Page1裝載進(jìn)物理內(nèi)存,以此類推,如上圖。
PS:大家注意上圖的顏色區(qū)分能夠很好的理解虛擬內(nèi)存相關(guān)概念。
虛擬內(nèi)存解決了物理內(nèi)存存在的兩個(gè)問題,由于每個(gè)進(jìn)程的數(shù)據(jù)被分成了很多頁裝載進(jìn)內(nèi)存,不會(huì)再出現(xiàn)內(nèi)存緊張的問題,而且一個(gè)應(yīng)用在物理內(nèi)存里面的地址是不連續(xù)的,所以無法訪問到別的進(jìn)程的地址,也保證了線程安全。
注意:雖然一個(gè)應(yīng)用在物理內(nèi)存中的內(nèi)存是不連續(xù)的,但是訪問數(shù)據(jù)的時(shí)候是連續(xù)的,原因就在于我們訪問數(shù)據(jù)訪問的是對應(yīng)的虛擬映射表,這個(gè)虛擬映射表記錄的某個(gè)方法、函數(shù)對應(yīng)在物理內(nèi)存的真實(shí)地址。
缺頁中斷(PageFault)
在上面我們說過在有了虛擬內(nèi)存技術(shù)之后,內(nèi)存以分頁的形式裝載進(jìn)物理內(nèi)存里面,比如我們現(xiàn)在啟動(dòng)一個(gè)應(yīng)用只需要先把啟動(dòng)相關(guān)的內(nèi)存Page1到Page10裝載進(jìn)物理內(nèi)存,當(dāng)用到A功能的時(shí)候發(fā)現(xiàn)數(shù)據(jù)沒有在物理內(nèi)存里面,此時(shí)操作系統(tǒng)會(huì)阻塞APP進(jìn)程觸發(fā)一次PageFault,操作系統(tǒng)會(huì)從磁盤中讀取相應(yīng)的數(shù)據(jù)到物理內(nèi)存上,再將其映射到虛擬內(nèi)存頁表上面。PageFault耗時(shí)非常短,平均在0.5ms左右,所以用戶一般感知不到,但是有一種情況可能會(huì)造成大量的PageFault,那就是啟動(dòng)時(shí)刻,所以我們就可以思考如何減少啟動(dòng)時(shí)的PageFault次數(shù),從而優(yōu)化啟動(dòng)時(shí)長。
二進(jìn)制重排
在分析二進(jìn)制重排之前,我們先了解一下 Link Map File 是個(gè)什么東西。
鏈接映射文件
鏈接映射文件:Link Map File,里面記錄的是每個(gè)類所生成的可執(zhí)行文件的路徑、CPU架構(gòu)、符號(hào)等信息,可以簡單的理解為這個(gè)文件告訴了我們一個(gè)應(yīng)用的可執(zhí)行文件的排列順序。
BuildSetting - Write Link Map File設(shè)置為YES。
如下圖所示:

編譯項(xiàng)目之后根據(jù)上圖的地址找到我們需要的 Link Map 文件,如圖所示:

linkmap 文件內(nèi)方法的排列順序遵循兩種規(guī)則:
- 文件的編譯順序,即:先按照項(xiàng)目 - Build Phases - Compile Sources中的順序排列,如下圖所示:

- 同一個(gè)文件內(nèi)部則遵循從上至下的方法書寫順序排列
這樣的排列順序就導(dǎo)致:啟動(dòng)時(shí)刻的代碼順序分別分布在了不同的文件里面,即啟動(dòng)代碼并沒有在一起,導(dǎo)致啟動(dòng)時(shí)刻有大量的 pageFault
優(yōu)化思路:將所有啟動(dòng)時(shí)刻需要調(diào)用的方法排列在一起,這就是二進(jìn)制重排。
缺頁中斷個(gè)數(shù)
在進(jìn)行優(yōu)化之前,我們首先需要檢查一下優(yōu)化之前的缺頁中斷個(gè)數(shù),打開 Instruments,選擇 System Trace,運(yùn)行之后。分析數(shù)據(jù)如圖,選擇“Main Thread”,底部的File Backed Page In即為缺頁中斷個(gè)數(shù)。
所下圖所示

當(dāng)我們測試過一次后再打開 System Trace,發(fā)現(xiàn)缺頁中斷個(gè)數(shù)少了很多,這個(gè)主要是由于緩存造成的,要想完完整整地去測試?yán)鋯?dòng)的缺頁中斷次數(shù)的話,可以在殺死app之后再打開幾個(gè)其他的APP,然后再過個(gè)一兩分鐘之后,再啟動(dòng)這個(gè)APP的話,就應(yīng)該是冷啟動(dòng)了。