App啟動(dòng)優(yōu)化 - 理論部分

本文分為理論【1-4】和實(shí)踐【5-6】兩部分:

  1. main()函數(shù)之前發(fā)生了什么
  2. Mach-O格式
  3. 虛擬內(nèi)存基礎(chǔ)知識(shí)
  4. 如何加載和準(zhǔn)備Mach-O二進(jìn)制文件
  5. 如何測量啟動(dòng)時(shí)間
  6. 優(yōu)化啟動(dòng)時(shí)間

一、Mach-O文件

Mach-O是運(yùn)行時(shí)可執(zhí)行文件的文件類型。

(一)Mach-O的文件類型
  • 可執(zhí)行文件:它是應(yīng)用程序中最重要的二進(jìn)制文件,也是應(yīng)用擴(kuò)展文件的主二進(jìn)制文件。
  • 動(dòng)態(tài)庫【Dylib】:它是一個(gè)動(dòng)態(tài)庫,在其他平臺(tái)上又稱為DSO或DLL,
  • 捆綁包【Bundle】:它是一種特殊的動(dòng)態(tài)庫,無法進(jìn)行鏈接,只能在運(yùn)行時(shí)使用dlopen()函數(shù)打開它。Mac OS的插件會(huì)用到。

圖像【Image】:它是指可執(zhí)行文件,動(dòng)態(tài)庫或捆綁包的任意一種類型;
框架【Framework】:它是一種帶有資源和標(biāo)頭目錄的動(dòng)態(tài)庫,存儲(chǔ)該動(dòng)態(tài)庫需要的文件。

函數(shù)定義: 
  void * dlopen( const char * pathname, int mode ); 
  函數(shù)描述: 
  dlopen函數(shù)以指定模式打開指定的動(dòng)態(tài)連接庫文件,并返回一個(gè)句柄給調(diào)用進(jìn)程。使用dlclose()來卸載打開的庫。 
  mode:分為這兩種 
  RTLD_LAZY 暫緩決定,等有需要時(shí)再解出符號 
  RTLD_NOW 立即決定,返回前解除所有未決定的符號。 
  RTLD_LOCAL 
  RTLD_GLOBAL 允許導(dǎo)出符號 
  RTLD_GROUP 
  RTLD_WORLD 

  返回值: 
  打開錯(cuò)誤返回NULL 
  成功,返回庫引用 
  編譯時(shí)候要加入 -ldl (指定dl庫) 
(二)Mach-O圖像格式
  • Mach-O圖像被分成數(shù)段;
  • 所有的段名都由大寫字母組成;
  • 每一段都是頁面大小的倍數(shù),而頁面大小由硬件決定,arm64處理器的頁面大小是16KB,其他都是4KB;

下例中TEXT段大小是3頁,DATALINKEDIT段大小都是1頁。

image_0.png

最常見的段名是TEXTDATALINKEDIT。實(shí)際上幾乎每一個(gè)二進(jìn)制文件都包含這三段,你可以添加自定義段,但一般不會(huì)給它賦值。

TEXTDATALINKEDIT段的作用:

  • TEXT:它是文件的開頭,包含了Mach的頭文件,任何機(jī)器指令以及任何只讀常量,比如C字符串。
  • DATA:它是重寫段,它包含了所有的全局變量。
  • LINKEDIT:它不包含全局變量的函數(shù),它包含變量函數(shù)信息,比如名稱和地址
  1. 分區(qū)
  • 分區(qū)是段的子范圍;
  • 分區(qū)不用遵循頁面的大小;
  • 分區(qū)的名稱都用小寫字母表示;
image_1.jpg
(三)Mach-O通用文件

假設(shè)我們生成一個(gè)64位的iOS應(yīng)用,現(xiàn)在我們有一個(gè)Mach-O文件。當(dāng)我們也想讓它在32位的設(shè)備上運(yùn)行,Xcode中會(huì)發(fā)生什么變化呢?
當(dāng)我們重新生成時(shí),Xcode會(huì)生成另一個(gè)單獨(dú)的Mach-O文件,這個(gè)是為32位生成的armv7。然后這兩個(gè)文件合并成第三個(gè)文件,這個(gè)文件叫作Mach-O通用文件。它前端有一個(gè)頭文件,所有的頭文件都有一個(gè)所有體系結(jié)構(gòu)的列表,它們的偏移值也在文件里。該頭文件也是一個(gè)頁面的大小。

image_2.png

通過上面我們知道,Mach-O圖像的每段都是頁面大小的倍數(shù),而且頭文件也需要一個(gè)頁面的大小,這樣會(huì)浪費(fèi)很多空間。那為何還要這樣做呢?這就涉及到虛擬內(nèi)存。

二、虛擬內(nèi)存

在軟件工程里有句格言,任何問題都可以通過添加一個(gè)間接層加以解決。而虛擬內(nèi)存所解決的問題就是,所有這些進(jìn)程存在時(shí)該如何管理所有的物理內(nèi)存。為了解決這個(gè)問題,添加了一個(gè)小的間接層,每個(gè)進(jìn)程都是一個(gè)邏輯地址空間,映射到RAM的某個(gè)物理頁面,這種映射不一定是一對一的。邏輯地址可以不對應(yīng)任何物理RAM,也可以多個(gè)邏輯地址對應(yīng)同一個(gè)物理RAM,這樣帶來很多中可能。那能利用虛擬內(nèi)存做什么呢?
首先如果有一個(gè)邏輯地址不映射任何物理RAM,當(dāng)進(jìn)程要訪問該地址時(shí)就會(huì)產(chǎn)生頁面錯(cuò)誤,內(nèi)核將停止該線程,并試圖找出解決方案。
下一點(diǎn)是如果有兩個(gè)進(jìn)程,對應(yīng)兩個(gè)邏輯地址,這兩個(gè)邏輯地址映射同一個(gè)物理頁面,這兩個(gè)進(jìn)程共享相同的RAM位,進(jìn)程之間開始共享。
另一個(gè)有趣的功能是基于文件的映射,不用把整個(gè)文件讀入RAM,而是可以調(diào)用mmap()函數(shù)告訴虛擬內(nèi)存系統(tǒng),我想把這部分文件映射到進(jìn)程里的這段地址。這么做的原因是,不用讀取整個(gè)文件,通過設(shè)置該映射,第一次訪問這些不同的地址時(shí),如同已經(jīng)在內(nèi)存里讀過,每次訪問未訪問過的地址時(shí),都會(huì)導(dǎo)致頁面錯(cuò)誤,內(nèi)核會(huì)讀該錯(cuò)誤頁面。這樣將會(huì)造成讀取文件遲緩。
現(xiàn)在我們結(jié)合前面講的關(guān)于Mach-O的內(nèi)容,可以知道任何Dylib和圖像的TEXT段都可以映射到多個(gè)進(jìn)程,這將會(huì)造成讀取遲緩,而這些頁面可以在進(jìn)程間共享。那么DATA段呢?
DATA用來讀寫,有一個(gè)策略叫寫入時(shí)復(fù)制,這和Apple文件系統(tǒng)的克隆很相似。寫入時(shí)復(fù)制所做的就是它積極地在所有進(jìn)程里共享DATA頁面。一個(gè)進(jìn)程會(huì)發(fā)生什么,只要它們只是從共享內(nèi)容的全局變量中讀取就可以了。但是一旦有進(jìn)程想要寫入其他DATA頁面,就會(huì)發(fā)生寫入時(shí)復(fù)制。
寫入時(shí)復(fù)制使內(nèi)核把該頁面復(fù)制到另一個(gè)物理RAM中,并將映射重定向到該頁面。所以該進(jìn)程有了該頁面的副本。這會(huì)給我們帶來臟頁面和凈頁面,而副本被認(rèn)為是臟頁面。臟頁面是指含有進(jìn)程的特定信息。凈頁面是指內(nèi)核可以按照需要重新建立的頁面,比如重新讀取磁盤。所以臟頁面比凈頁面要昂貴很多。
最后一點(diǎn)是頁面也有權(quán)限界限,這指的是可以標(biāo)記一個(gè)頁面可讀、可寫或可執(zhí)行、或者它們的任意組合。

虛擬內(nèi)存的作用:
1. 虛擬內(nèi)存是間接層
2. 將每個(gè)進(jìn)程的地址映射到物理RAM(頁面粒度)

虛擬內(nèi)存的特征:
1. 頁面錯(cuò)誤
2. 相同的RAM頁面出現(xiàn)在多個(gè)進(jìn)程中
3. 文件支持的頁面
    3.1  mmap()
    3.2  懶讀取
4. 寫入時(shí)復(fù)制(COW)
5. 臟頁面與干凈頁面
6. 權(quán)限:rwx

以上就是Mach-O格式和虛擬內(nèi)存的內(nèi)容。現(xiàn)在看看它們是如何一起工作的,在之前,我們先看看Dyld(全稱the dynamic link editor,即動(dòng)態(tài)鏈接器,其本質(zhì)Mach-O文件,專門用來加載動(dòng)態(tài)庫的庫)是如何操作的,它在Mach-O和虛擬內(nèi)存之間是如何映射的。

三、Dyld的操作過程

現(xiàn)有一個(gè)Dylib文件,如下圖所示:

image_3.png

我們沒有把它讀到內(nèi)存中,而是把它映射到內(nèi)存,所以在內(nèi)存里該Dylib文件本應(yīng)該占用8個(gè)頁面。可以看到,不同的是有這些“全零填充”。
大部分全局變量的初始值都是零,所以靜態(tài)鏈接器進(jìn)行了優(yōu)化,把所有值為0的全局變量都移到了尾端,然后不占用任何磁盤空間。取而代之,我們利用虛擬內(nèi)存的特性,在該頁面第一次被訪問時(shí),告訴虛擬內(nèi)存把它填滿0。所以它不需要讀取。Dyld必須要做的第一件事是在內(nèi)存中查看該進(jìn)程的Mach頭文件。它將查看內(nèi)存的頂盒,此時(shí)那里是空的,沒有內(nèi)容映射到物理頁面上,所以產(chǎn)生頁面錯(cuò)誤。到那時(shí)內(nèi)核意識(shí)到它被映射到了一個(gè)文件,所以它將讀取文件的第一頁,將其放入物理RAM,設(shè)置其映射。

image_4.png

現(xiàn)在Dyld可以真正通過Mach頭文件開始讀取。它通過讀取Mach頭文件,Mach頭文件讓DyldLINKEDIT段上查看這條信息。再一次,Dyld跳下去查看進(jìn)程1的底盒。這又會(huì)產(chǎn)生頁面錯(cuò)誤,內(nèi)核又讀入RAM的另一個(gè)LINKEDIT的物理頁面。

image_4.png

Dyld現(xiàn)在可以期望一個(gè)LINKEDIT。此刻在進(jìn)程中,LINKEDIT將會(huì)告訴DyldDATA頁面做一些修正,讓Dylib可運(yùn)行。所以同樣的事情又發(fā)生了,Dyld現(xiàn)從DATA頁面讀取數(shù)據(jù),但是有一點(diǎn)不同,Dyld想要寫回一些內(nèi)容修改DATA頁面,此刻寫入時(shí)復(fù)制出現(xiàn)了。這個(gè)頁面變成了臟頁面。所以臟RAM的8個(gè)頁面將會(huì)是什么?若我只用malloc()函數(shù)分配8頁內(nèi)存,然后讀了一些內(nèi)容進(jìn)去,我將會(huì)有8個(gè)頁面的臟RAM。但現(xiàn)在我只有1頁的臟RAM和2個(gè)凈頁面。

image_5.png

如果第二個(gè)進(jìn)程加載同一個(gè)Dylib將會(huì)發(fā)生什么?在第二個(gè)進(jìn)程里,Dyld會(huì)經(jīng)歷相同的步驟,首先它查看Mach頭文件,但內(nèi)核在RAM某處已經(jīng)有這頁了,所以內(nèi)核只是簡單地把映射重定向,重利用該頁面,并沒有任何IO操作。LINKEDIT也是如此,更加快速。我們來看DATA頁面,此時(shí)內(nèi)核必須要看看在DATA頁面,干凈的副本是否還存在RAM其他地方,如果還在,就可以重利用;如果不在,就必須要重新讀取。

image_5.png
image_6.png
image_7.png
image_8.png

在該進(jìn)程中,Dyld會(huì)讓RAM變臟。

image_8.png

最后一步是LINKEDIT,只在Dyld進(jìn)行操作時(shí)被需要。所以它可以提醒內(nèi)核,當(dāng)它完成時(shí),它不再需要這些LINKEDIT頁面,當(dāng)別人需要RAM時(shí),可以回收它們。現(xiàn)在有兩個(gè)進(jìn)程在共享這些Dylib,每個(gè)進(jìn)程都本應(yīng)該有8個(gè)頁面,也就是一共有16個(gè)臟頁面。但現(xiàn)在我們只有2個(gè)臟頁面和1個(gè)干凈的、共享頁面。

image_9.png

以上講了Dyld如何將Mach-O映射到虛擬內(nèi)存中,下面我們看看安全如何影響Dyld的。

四、安全

有兩點(diǎn)安全問題會(huì)影響到Dyld

  1. ASLR地址空間布局隨機(jī)化
    這是20年前的舊技術(shù),基本概念是把加載地址隨機(jī)化。
  2. 代碼簽名
    在Xcode中,代碼簽名是指對整個(gè)文件運(yùn)行一個(gè)加密哈希算法,然后在文件上簽名。為了在運(yùn)行時(shí)進(jìn)行驗(yàn)證,整個(gè)文件都必須要重新讀取。所以在編譯階段,我們讓Mach-O文件的每一個(gè)頁面都進(jìn)行自己的加密哈希算法,所有哈希都存儲(chǔ)在LINKEDIT里。這使得你的每個(gè)未被修改的頁面在被讀取的過程中都能得到及時(shí)驗(yàn)證。

現(xiàn)在我們來研究從exec()main()

五、exec()

exec()是一個(gè)系統(tǒng)調(diào)用函數(shù),它用新程序替換當(dāng)前進(jìn)程中的程序。當(dāng)進(jìn)你入內(nèi)核,想把這個(gè)進(jìn)程換成這個(gè)新程序時(shí):
首先內(nèi)核會(huì)抹去整個(gè)地址,映射到你指定的可執(zhí)行程序。ASLR把它映射到一個(gè)隨機(jī)地址。

image_10.png

下一步是從該隨機(jī)地址回溯到零地址,把整個(gè)區(qū)域標(biāo)記為不可訪問,意思是指不可讀、不可寫、不可執(zhí)行。該區(qū)域在32位處理器下至少4KB大小,64位處理器下至少4GB大小。這樣可以捕捉任何空指針引用,捕捉任何指針截?cái)唷?/p>

image_11.png
 


六、關(guān)于Dylibs

Unix誕生的前幾十年,一切都很簡單,我只需映射一個(gè)程序,把指針引用指向它,開始運(yùn)行它即可。然后共享庫被發(fā)明出來。那么誰來加載Dylibs呢?人們很快意識(shí)到情況太過復(fù)雜,不想讓內(nèi)核做這件事。所以人們新建了幫助程序,在我們的平臺(tái)上叫作Dyld,在其他Unix平臺(tái)又叫作LD.SO
因此當(dāng)內(nèi)核完成進(jìn)程的映射時(shí),它現(xiàn)在將另一個(gè)名為DyldMach-O文件映射到另一個(gè)隨機(jī)地址的進(jìn)程中。把PC指向Dyld,讓Dyld完成進(jìn)程的啟動(dòng)。現(xiàn)在Dyld在運(yùn)行進(jìn)程,它的工作是加載所有依賴的動(dòng)態(tài)庫,讓它們完全準(zhǔn)備好開始運(yùn)行。

image_12.png

七、Dyld步驟

讓我們來瀏覽這些步驟,底部有很多步驟和一個(gè)時(shí)間線,我們?yōu)g覽這些的時(shí)候,也會(huì)瀏覽時(shí)間線。

  • Map all dependent dylibs, recurse Rebase all images
  • Bind all images
  • ObjC prepare images
  • Run initializers
image_13.png
(一) 加載動(dòng)態(tài)庫

首先Dyld是否需要映射所有依賴的動(dòng)態(tài)庫?什么是依賴的動(dòng)態(tài)庫?
要找到它們,首先要讀取內(nèi)核中已經(jīng)映射好的主可執(zhí)行文件的頭部,在該頭文件中是一個(gè)所有依賴庫的列表。因此必須將其解析出來。所以必須要找到每一個(gè)動(dòng)態(tài)庫。一旦找到每個(gè)動(dòng)態(tài)庫,必須打開并運(yùn)行每個(gè)文件的開頭,需要確保是這是一個(gè)Mach-O文件,對它進(jìn)行驗(yàn)證,找到它的編碼簽名,將這個(gè)編碼簽名注冊到內(nèi)核中。
然后它可以在這個(gè)動(dòng)態(tài)庫中的每一段調(diào)用mmap()函數(shù)

image_14.png

總結(jié):

- 解析依賴的動(dòng)態(tài)庫列表;
- 找到必須的`Mach-O`文件;
- 打開并讀取文件的開頭;
- 驗(yàn)證`Mach-O`文件;
- 注冊代碼簽名;
- 為每一段調(diào)用`mmap()`函數(shù);
(二) 遞歸加載

假如你的應(yīng)用依賴A.dylibB.dylib兩個(gè)動(dòng)態(tài)庫,而A.dylibB.dylib自身也可能依賴其他dylib。所以Dyld必須為每一個(gè)dylib再做一次同樣的事,而每個(gè)dylib可能依賴于已經(jīng)加載的東西或新的東西,所以Dyld必須確定它是否已經(jīng)被加載,如果沒有被加載,Dyld需要加載它。所以如此繼續(xù)這種操作,最終所有依賴的都被加載了。
通常一個(gè)系統(tǒng)里的普通進(jìn)程,都會(huì)加載1至400個(gè)動(dòng)態(tài)庫,這個(gè)加載數(shù)量很大。還好這些動(dòng)態(tài)庫大部分都是OS庫,OS系統(tǒng)在構(gòu)建時(shí),會(huì)預(yù)計(jì)算和預(yù)緩存那些Dyld加載內(nèi)容所要做的工作。所以O(shè)S庫加載很快。

image_15.png

現(xiàn)在所有的動(dòng)態(tài)庫都已經(jīng)加載完成,但是它們都彼此獨(dú)立,我們必須要把它們捆綁在一起,這就是所謂的修復(fù)(fix-ups)

(三) 修復(fù)(fix-ups)

關(guān)于修復(fù),有一點(diǎn)我們已經(jīng)知道,由于代碼簽名的存在我們無法修改指令。那么如果不能修改它調(diào)用的指令,動(dòng)態(tài)庫如何調(diào)用另一個(gè)動(dòng)態(tài)庫呢?這又用到了間接引用的技術(shù)。
所以我們的code-gen稱為動(dòng)態(tài)PIC,即地址無關(guān)代碼。這意味著代碼可以動(dòng)態(tài)地加載到該地址,也就是說地址間接地被分配。這所意味的是為了讓一個(gè)調(diào)用另一個(gè),code-gen實(shí)際上在DATA段里新建一個(gè)指針,并且該指針指向了我們想調(diào)用的位置
。代碼加載該指針,并且跳向該指針。所以所有的Dyld都在修復(fù)指針和數(shù)據(jù)。

image_16.png

現(xiàn)在主要有兩種修復(fù),重設(shè)基址和綁定。它們的區(qū)別是什么呢?

  • 重設(shè)基址:是指如果有一個(gè)指針指向圖像范圍內(nèi),需要做出的所有的修改。
  • 綁定:是指如果指針指向圖像范圍外,他們必須進(jìn)行不同的修復(fù)。
image_17.png

下面我們一起看看其步驟:
我們可以在任何二進(jìn)制文件上運(yùn)行dyldinfo指令,就可以看到dyld必須為該二進(jìn)制文件做的所有修復(fù)工作。

[~]> xcrun dyldinfo -rebase -bind -lazy_bind DongDong.app/DongDong
 for arch armv7:
 rebase information (from compressed dyld info):
 segment section          address     type         value
 __DATA  __nl_symbol_ptr  0x002F800C  pointer  0x002FC9E0
 __DATA  __nl_symbol_ptr  0x002F8010  pointer  0x002FC458
 __DATA  __nl_symbol_ptr  0x002F8014  pointer  0x002FEFE8
 __DATA  __nl_symbol_ptr  0x002F8018  pointer  0x002EDB00
 __DATA  __nl_symbol_ptr  0x002F8050  pointer  0x00322A6C
 __DATA  __nl_symbol_ptr  0x002F8054  pointer  0x002FC878
 ......
 
 bind information:
 segment section          address        type    addend dylib            symbol
 __DATA  __nl_symbol_ptr  0x002F833C    pointer      0 Alamofire        _$s9Alamofire12JSONEncodingVAA17ParameterEncodingAAWP
 __DATA  __nl_symbol_ptr  0x002F8340    pointer      0 Alamofire        _$s9Alamofire12JSONEncodingVN
 __DATA  __objc_classrefs 0x0031C4C8    pointer      0 CFNetwork        _OBJC_CLASS_$_NSHTTPURLResponse
 __DATA  __objc_classrefs 0x0031C4B4    pointer      0 CFNetwork        _OBJC_CLASS_$_NSMutableURLRequest
 __DATA  __objc_classrefs 0x0031C57C    pointer      0 CFNetwork        _OBJC_CLASS_$_NSURLConnection
 __DATA  __objc_classrefs 0x0031C4B0    pointer      0 CFNetwork        _OBJC_CLASS_$_NSURLSession (weak import)
 ......
 
 lazy binding information (from lazy_bind part of dyld info):
 segment section          address    index  dylib            symbol
 __DATA  __la_symbol_ptr  0x002F8574 0x0000 libswiftFoundation _$s10Foundation10URLRequestV19_bridgeToObjectiveCSo12NSURLRequestCyF
 __DATA  __la_symbol_ptr  0x002F8578 0x004D libswiftFoundation _$s10Foundation10URLRequestV3url11cachePolicy15timeoutIntervalAcA3URLV_So017NSURLRequestCacheE0VSdtcfC
 __DATA  __la_symbol_ptr  0x002F857C 0x00BC libswiftFoundation _$s10Foundation10URLRequestVMa
 __DATA  __la_symbol_ptr  0x002F8580 0x00E3 libswiftFoundation _$s10Foundation12CharacterSetV11whitespacesACvgZ
 __DATA  __la_symbol_ptr  0x002F8584 0x011C libswiftFoundation _$s10Foundation12CharacterSetVMa
 __DATA  __la_symbol_ptr  0x002F8588 0x0145 libswiftFoundation _$s10Foundation17NSLocalizedString_9tableName6bundle5value7commentS2S_SSSgSo8NSBundleCS2StF
 ......

(四) 重設(shè)基址

在過去你可以為每一個(gè)dylib指定首選加載地址,該首選加載地址是一個(gè)靜態(tài)鏈接器,和Dyld一起工作。這樣,若把它加載到該首選加載地址,則所有本應(yīng)該在內(nèi)部編碼的指針和數(shù)據(jù)都是正確的,那么Dyld就不用做任何修復(fù)。但是現(xiàn)在,因?yàn)橛辛?code>ASLR,dylib被加載到隨機(jī)地址上。
它被滑動(dòng)到其他地址,也就是說所有那些指針和數(shù)據(jù)都還依然指向舊地址。所以為了修復(fù)它們,我們需要計(jì)算滑動(dòng)值,也就是移動(dòng)距離,并且將該滑動(dòng)值添加到每一個(gè)內(nèi)部指針上。
因此重設(shè)基址是指遍歷所有內(nèi)部數(shù)據(jù)指針,然后為它們添加一個(gè)滑動(dòng)值。所以這個(gè)概念很簡單,讀、添加、寫,讀、添加、寫。但是這些數(shù)據(jù)指針在哪里呢?這些指針在段中的位置都編碼在LINKEDIT段里。此時(shí),所有映射都已經(jīng)結(jié)束,當(dāng)我們開始重設(shè)基址時(shí),實(shí)際上在所有DATA頁面都產(chǎn)生了頁面錯(cuò)誤。然后對頁面進(jìn)行修改時(shí),產(chǎn)生寫入時(shí)復(fù)制。
由于所有的這些IO操作,重設(shè)基址有時(shí)會(huì)非常昂貴。但是有一個(gè)技巧,就是按順序操作,從內(nèi)核的角度來看,它認(rèn)為數(shù)據(jù)錯(cuò)誤是按順序產(chǎn)生的。當(dāng)它如此認(rèn)為時(shí),內(nèi)核會(huì)進(jìn)行預(yù)讀,這樣I/O成本會(huì)降低很多。

image_18.png

下面我們來看另一種修復(fù)---綁定

(五) 綁定

綁定是針對那些指向動(dòng)態(tài)庫范圍外的指針而言的。這些指針通過名稱進(jìn)行綁定,實(shí)際上都是字符串。本例中,LINKEDIT段里的malloc,也就是說該數(shù)據(jù)指針需要指向malloc。所以運(yùn)行時(shí),dylib需要找到實(shí)現(xiàn)該符號的位置,這需要很多的計(jì)算,遍歷查找符號表。一旦找到,就把值存儲(chǔ)到該數(shù)據(jù)指針中。所以這種方式的計(jì)算復(fù)雜度要比重設(shè)基址高很多。但是I/O很少,因?yàn)橹卦O(shè)基址已經(jīng)完成大部分的I/O

image_19.png
(六) 通知ObjC運(yùn)行時(shí)

ObjC有很多DATA結(jié)構(gòu),DATA結(jié)構(gòu)類也就是指向其方法的指針,以及super gloss的指針等等。幾乎所有這些都通過重設(shè)基址或綁定被修復(fù)。但在ObjC運(yùn)行時(shí)還需要一些額外的操作。首先ObjC是一門動(dòng)態(tài)語言,可以把一個(gè)類用名稱實(shí)例化。即ObjC在運(yùn)行時(shí),必須要維護(hù)一張表格,這張表中包含了其映射類的所有名稱。每次加載的名稱都將定義一個(gè)類,并將該類的名稱注冊到一個(gè)全局的表中。接下來,在C++中,你們可能聽說過關(guān)于脆弱的ivar問題。
ObjC中不存在脆弱的基類問題,因?yàn)槲覀冏龅钠渲幸环N修復(fù)就是,在加載時(shí)動(dòng)態(tài)地改變所有ivar的偏移值。在ObjC里,我們可以定義改變另一個(gè)類中方法的分類。有時(shí)這些分類在一些類中,而這些類不在另一個(gè)動(dòng)態(tài)庫的圖像中,此刻應(yīng)用那些方法修復(fù)。最后,ObjC基于選擇器是唯一的,所以我們需要唯一的選擇器。

image_20.png
(七) 初始化器

現(xiàn)在我們完成了所有的DATA修復(fù),現(xiàn)在我們可以進(jìn)行所有可以靜態(tài)描述的DATA修復(fù)。現(xiàn)在是進(jìn)行動(dòng)態(tài)DATA修復(fù)的時(shí)機(jī)。
在C++里,有一個(gè)初始化器,可以指定等于任何你想要的表達(dá)式。那個(gè)任意的表達(dá)式此時(shí)需要運(yùn)行,現(xiàn)在就運(yùn)行了。因此,C++編譯器生成初始化器來完成那些任意DATA的初始化。
ObjC有一種方法,叫+load方法,現(xiàn)在+load方法已經(jīng)被否決,不建議使用。建議使用+initialize方法。若有+load方法,此時(shí)它開始運(yùn)行。
看下面的這張圖,頂端是主可執(zhí)行文件,所有的動(dòng)態(tài)庫都依照這張圖,必須要運(yùn)行初始化器。按什么順序運(yùn)行呢?我們選擇從下往上,原因在于當(dāng)初始化器運(yùn)行時(shí),可能會(huì)調(diào)用一些動(dòng)態(tài)庫,你需要確保那些動(dòng)態(tài)庫已經(jīng)準(zhǔn)備好被調(diào)用。所以從下開始運(yùn)行初始化器,一直向上到應(yīng)用類,可以很安全地調(diào)用依賴的內(nèi)容。所以一旦所有初始化器完成時(shí),現(xiàn)在我們終于可以調(diào)用主Dyld程序了。

image_21.png

八、main()函數(shù)之前發(fā)生了什么

通過上面的知識(shí),我們了解了進(jìn)程是如何啟動(dòng)的,知道了Dyld是一個(gè)幫助程序。

  • 加載所有的依賴庫;
  • 修復(fù)DATA頁面的所有指針;
  • 運(yùn)行所有的初始化器;
  • 跳到主函數(shù);

理論部分到此結(jié)束,那么如何把這些理論應(yīng)用到實(shí)際中呢?
請閱讀下一章的實(shí)戰(zhàn)內(nèi)容:App啟動(dòng)優(yōu)化 --- 實(shí)踐部分

九、參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容