重學iOS系列之APP啟動(二)dyld

導讀

? ? 本文承接自APP啟動-流程(一),有疑惑的同學可以先閱讀上一篇的內容。本文會帶大家詳細的解讀dyld-852.2源碼中關于APP啟動最重要的一個函數_main()。我們從上一節了解到_main函數的實現代碼有900行,所以不可能一行一行的來進行解讀,本文只會針對重點函數進行跟進解讀。有需要的同學自行下載源碼解讀。源碼解讀并非跟著文章看一遍就能記住學會,這個過程需要反復的跟讀,所以建議讀者將源碼下載下來,跟著筆者的進度同時對照著源碼學習效果才會最佳,也不至于看得云里霧里。

dyld下載地址:http://opensource.apple.com/tarballs/dyld

在分析具體的源碼之前,我們必須要了解一些前置知識點:

dyld的全稱是dynamic loader。dyld 是 iOS 上的二進制加載器,用于加載 Image。有不少人認為 dyld 只負責加載應用依賴的所有動態鏈接庫,這個理解是錯誤的。dyld 工作的具體流程如下:


dyld2與dyld3

在?iOS 13?之前,所有的第三方?App?都是通過?dyld 2?來啟動?App?的,主要過程如下:

1、Parse mach-o headers?????解析mach-o頭文件

2、find dependencels 根據頭文件的信息查找依賴項

3、Map mach-o files ?將mach-o文件映射到內存,簡單來講就是加載到內存中

4、Perform symbol lookups這個步驟表示執行符號查找。(例如:如果你使用了printf函數,就會查找printf是否在庫系統中,找到它的地址,將它賦值到你的程序中的函數指針)

5、Bind and rebase ?符號綁定和地址重定位(由于ASLR的原因)

6、Run initializers ?dyld會運行初始化函數,初始化動態庫,初始化主程序,然后進入運行時runtime初始化,注冊類,分類,方法唯一性檢查等(后續的文章會詳細分析runtime的初始化過程)

在iOS 13之后,dyld3開放給第三方APP使用了,也就是說,在iOS 13以上的系統里,APP是通過dyld3來啟動的。

從上圖來看dyld3是由2個部分組成,上半部分在程序啟動進程外執行的,這一步會在App下載安裝和版本更新的時候會去執行。下半部分才是在程序啟動進程內執行的。

原本在dyld2中執行的1、2、4步被放到了程序啟動進程外執行,然后向磁盤寫入閉包處理 “Write closure to disk”(啟動閉包(launch closure):這是一個新引入的概念,指的是 app 在啟動期間所需要的所有信息。比如這個 app 使用了哪些動態鏈接庫,其中各個符號的偏移量,代碼簽名在哪里等等)。這樣,啟動閉包處理就成了啟動程序的重要環節。稍后可以在APP的進程中使用 dyld 3包含的這三個部分,

啟動閉包比mach-o更簡單。它們是內存映射文件,不需要用復雜的方法進行分析。

我們可以簡單的驗證它們,這樣可以提高速度,也就是說在不需要修改代碼的情況下官方幫我們做了啟動優化。

dyld3的主要過程如下:

主程序進程外

1、Parse mach-o headers?????解析mach-o頭文件

2、find dependencels 根據頭文件的信息查找依賴項

3、Perform symbol lookups ?這個步驟表示執行符號查找。(例如:如果你使用了printf函數,就會查找printf是否在庫系統中,找到它的地址,將它賦值到你的程序中的函數指針)

4、Write closure to disk ?將1、2、3步做完的事情組裝成一個啟動閉包,并且寫入磁盤

主程序進程內

5、Read in closure ? ?從啟動閉包中讀取必要的信息數據

6、Validata closure ? ? 驗證啟動閉包

7、Map mach-o files ?將mach-o文件映射到內存,簡單來講就是加載到內存中

8、Bind and rebase ?符號綁定和地址重定位(由于ASLR的原因)

9、Run initializers ?dyld會運行初始化函數,初始化動態庫,初始化主程序,然后進入運行時runtime初始化,注冊類,分類,方法唯一性檢查等(后續的文章會詳細分析runtime的初始化過程)

dyld3 被分為了三個組件:

一、一個進程外的 MachO 解析器

1、預先處理了所有可能影響啟動速度的 search path、@rpaths 和環境變量

2、然后分析 Mach-O 的 Header 和依賴,并完成了所有符號查找的工作

3、最后將這些結果創建成了一個啟動閉包

4、這是一個普通的 daemon 進程,可以使用通常的測試架構

二、一個進程內的引擎,用來運行啟動閉包

1、這部分在進程中處理

2、驗證啟動閉包的安全性,然后映射到 dylib 之中,再跳轉到 main 函數

3、不需要解析 Mach-O 的 Header 和依賴,也不需要符號查找。

三、一個啟動閉包緩存服務

1、系統 App 的啟動閉包被構建在一個 Shared Cache 中, 我們甚至不需要打開一個單獨的文件

2、對于第三方的 App,我們會在 App 安裝或者升級的時候構建這個啟動閉包。

3、在 iOS、tvOS、watchOS中,這這一切都是 App 啟動之前完成的。在 macOS 上,由于有 Side Load App,進程內引擎會在首次啟動的時候啟動一個 daemon 進程,之后就可以使用啟動閉包啟動了。

dyld 3 把很多耗時的查找、計算和 I/O 的事前都預先處理好了,這使得啟動速度有了很大的提升。



_main函數

函數的每個參數意義如下:

// dyld的main函數 dyld的入口方法kernel加載dyld并設置設置一些寄存器并調用此函數,之后跳轉到__dyld_start

// mainExecutableSlide 主程序的slider,用于做重定向 會在main方法中被賦值

// mainExecutableMH 主程序MachO的header

// argc 表示main函數參數個數

// argv 表示main函數的參數值 argv[argc] 可以獲取到參數值

// envp[] 表示以設置好的環境變量

// apple 是從envp開始獲取到第一個值為NULL的指針地址

uintptr_t

_main(constmacho_header* mainExecutableMH,uintptr_tmainExecutableSlide,?

intargc,constchar* argv[],constchar* envp[],constchar* apple[],?

uintptr_t* startGlue)

源碼分析_main()函數:

配置運行環境

檢查是否開啟debug追蹤,追蹤的是啟動可執行文件的過程。

檢查是否有內核相關的標記,setFlags內部會檢查dyld3是否已經初始化,沒有初始化則不設置flags

檢查并查看內核是否禁用了JOP(Jump-Oriented Programming)指針簽名,指針簽名是用于防范內核攻擊的一個手段。

從環境變量中獲取主要可執行文件的?cdHash?值。這個哈希值?mainExecutableCDHash?在后面用來校驗?dyld3?的啟動閉包

獲取當前設備的一些信息,比如:cpu架構類型,基本信息,mach進程通信端口等


通知內核開始加載dyld和主程序的可執行文件了

獲取主程序的macho_header結構

獲取主程序的slide值,slide其實就是mach-o映射到內存的偏移量

查找可執行文件支持的平臺(看綠色高亮的FIXME這行,其實這部分代碼可以刪除,因為在內核中已經處理過了)

接著沒截出來的那一段代碼是基于OS系統的判斷邏輯,本文不做分析。


設置上下文信息,保存回調函數的地址,以便后續直接調用。可以看看setContext的源碼如下:


根據環境變量從內核拿到可執行文件的路徑。

移除過渡代碼(修復了rdar://problem/13868260這個bug)


由于是多平臺共用一套代碼,內核傳遞出來的exec路徑是不全的,如果是iphone真機環境,則會拼接一段路徑的前綴,大家肯定打印過很多Document的路徑,真機都是/var/開頭的和模擬器的不一樣。

判斷?exec?路徑是否為絕對路徑,如果為相對路徑,使用?cwd?轉化為絕對路徑

為了后續的日志打印從?exec?路徑中取出進程的名稱 (strrchr?函數是獲取第二個參數出現的最后的一個位置,然后返回從這個位置開始到結束的內容)


配置進程的受限模式,設置gLinkContext的環境變量,跟簽名以及代碼注入有關。查看詳細的configureProcessRestrictions代碼實現,會發現一個新系統AMFI (AppleMobileFileIntegrity)。

簡單介紹下AMFI是一個內核擴展,最初是在iOS中引入的。在版本10.10中,它也被添加到macOS中。它擴展了MACF(強制訪問控制框架),就像沙盒一樣,它在實施SIP和代碼簽名方面起著關鍵作用(這一塊涉及的內容非常多,不做詳細分析)

dyld3::internalInstall()這個函數在xcode默認設置下是返回yes的。

ClosureMode是個calss類型的枚舉,有4種模式:

Unset????表示我們沒有提供env變量或boot arg來顯式選擇模式

On????表示我們將DYLD_USE_CLOSURES設置為1,或者我們沒有將DYLD_USE_CLOSURES設置為0,但在iOS上設置了-force_dyld3=1環境變量或者一個外部緩存(啟動閉包)

Off????意味著我們設置了DYLD_USE_CLOSURES=0,或者我們沒有設置DYLD_USE_CLOSURES=1,但在iOS上設置了-force\u dyld2=1環境變量或者一個內部緩存

PreBuiltOnly????意味著只使用共享緩存閉包,而不嘗試構建新的緩存閉包

ClosureKind 也是個class類型的枚舉,有3種狀態:unset, full, minimal

默認值都是unset

檢查是否強制使用共享緩存,在iOS上為了節省內存,是要求強制使用共享緩存的。



創建了緩存閉包路徑的數組變量,如果出錯,則直接退出進程。

成功則將閉包模式設置為on。

到此,運行環境全部設置完成。細心的同學應該會注意到,整個過程中有很多DYLD_****開頭的環境變量。其實這些都是可以在Xcode中配置使其在上面的流程中生效的,我們打開工程然后依次點擊“Product”->“Scheme”->“Edit Scheme…”,如下圖所示。


然后運行Xcode即可看到控制臺打印的詳細信息。有很多這樣的DYLD_*開頭的環境變量,感興趣的同學可以自行測試。

加載共享緩存

這里補充一下共享緩存的知識點:

在iOS系統中,每個程序依賴的動態庫都需要通過dyld(位于/usr/lib/dyld)一個一個加載到內存,然而如果在每個程序運行的時候都重復的去加載一次,勢必造成運行緩慢,為了優化啟動速度和提高程序性能,共享緩存機制就應運而生。所有默認的動態鏈接庫被合并成一個大的緩存文件,放到/System/Library/Caches/com.apple.dyld/目錄下,按不同的架構保存分別保存著。

如果沒有緩存庫存在的話,那么我們手機上的每一個App,如果要用到系統動態庫的話,是需要每一個App都要去加載一次的,一樣的資源被加載多次(加載幾次就需要消耗幾份內存),無論是空間還是執行效率,都是造成了浪費。

如果有共享緩存庫(系統會提前將一些常用的庫加載到內存中)存在的話,那么我們手機上的每一個App,如果要用到系統動態庫的話,只需要先去內存中找,找到了就直接鏈接就行,沒有找到的花,加載一份到共享緩存的內存空間中,然后再鏈接這個庫。節省了內存,還提高了運行速度。

前面說過,iOS系統是強制使用共享緩存的,所以checkSharedRegionDisable里面什么都不會做,然后將共享緩存映射到當前進程的內存空間內。

真正加載共享緩存的是這句代碼:mapSharedCache(); 里面調用了loadDyldCache(),從if else代碼可以看出,共享緩存加載又分為三種情況:

僅加載到當前進程,調用mapCachePrivate()。

共享緩存已加載,不做任何處理。

當前進程首次加載共享緩存,調用mapCacheSystemWide()。

mapCachePrivate()、mapCacheSystemWide()里面就是具體的共享緩存解析邏輯,感興趣的同學可以自己深入詳細分析。

dyld3

查找啟動閉包

如果沒有設置閉包模式,則檢查環境變量里的字段以及緩存類型來判斷設置哪種模式。

4種模式在前文有詳細介紹。


宏檢查是否是真機,創建一些零時變量存儲啟動閉包的信息,檢查共享緩存中是否有啟動閉包,前文有提到,系統app會將啟動閉包保存到共享緩存中,所以是有必要做這項檢查的。

從 findClosure() 實現里面也可以看出:

(anything in /System/ should have a closure)任何在/System/ 路徑下的可執行文件都有一個啟動閉包。

重新回到_main()函數

接下來是一系列重新構建閉包的判斷。所有的if else 都是在為allowClosureRebuilds服務的。

需要特別注意的是6997行用紅色框框出來的closureValid()函數,這個函數里面做了大量的判斷,來判斷緩存里是否存在閉包。注意,這里的緩存和上文的共享緩存是有區別的!

下面是具體的實現,由于實現代碼太長,把很多分支都隱藏還是無法展示全部代碼,所以這里只截取了前半部分代碼,不過我會把有注釋的判斷都列出來:


如上圖,在_main()函數里獲取的CDHash在這里發揮了作用,代碼能執行到這說明是有緩存閉包的,后續的判斷都是針對這個存在的閉包,判斷是否有效。

下面列出剩余的注釋,一行注釋就是一個判斷:

//If we found cd hashes, but they were all invalid, then print them out - 如果我們發現了cdHash,但它們都是無效的,那么就把它們打印出來

// verify UUID of main executable is same as recorded in closure - 驗證主可執行文件的UUID是否與閉包中記錄的相同

// verify DYLD_* env vars are same as when closure was built -?驗證DYLD_*env變量是否與構建閉包時相同

// verify files that are supposed to be missing actually are missing -?驗證應該丟失的文件是否確實丟失

// verify files that are supposed to exist are there with the -?驗證假定存在的文件是否與 (這里的注釋不全,根據打印log,應該是驗證文件完整性的)

// verify closure did not require anything unavailable -?驗證閉包是否有依賴任何不可用的內容


分析完closureValid(),我們回到_main()函數中,繼續閉包相關的源碼閱讀:

這里驗證了上文提到的只有第三方app的啟動閉包會保存在磁盤中。

主要邏輯是 獲取主程序啟動閉包CDHash,然后做如下判斷:

1、如果CDHash不為空,則再判斷是否啟動了dyld3,如果啟動了則什么都不做,從打印我們知道,在啟動dyld3的情況下內部系統是允許磁盤上的啟動閉包的。如果沒有啟動dyld3,則設置canUseClosureFromDisk = false,也就是說不允許使用磁盤上的啟動閉包。

2、如果CDHash為空,則判斷主程序啟動閉包是否為空,如果不為空則將mainClosure設置為空指針。結合注釋我們可以了解到cdHash和啟動閉包是必須同時存在的,如果CDHash為空,則找到的緩存閉包也不能使用。

構建啟動閉包

rdar的bug先不管(其實是筆者水平不夠沒弄懂)。

然后如果發現沒有找到一個有效的啟動閉包,則嘗試自己構建一個。

buildLaunchClosure()這個函數就是構建閉包的函數,內部實現比較長比較復雜,在本文不做詳細的分析。后續有空的話會新開一篇專門分析啟動閉包構建和加載。

接下來判斷sJustBuildClosure是否為yes,如果為yes意思就是只構建閉包,不做加載動作,上文提到APP在下載完成以及升級完成后會重新構建啟動閉包,在這種情況下dyld構建完閉包后就直接退出了。

加載閉包

這里的邏輯比較簡單,launchWithClosure()加載啟動閉包,判斷是否成功,如果失敗,判斷是否啟動閉包是否過期,過期了重新構建一個,構建完成后再重新加載閉包。

launchWithClosure()函數內部實現很長,粗略的看了下具體的實現,簡單說一下:

拿到所有的image數組(最多三個:緩存動態庫、其他操作系統動態庫和主程序),

然后調用了dyld3 的一個Load的loader()方法,然后遞歸的加載依賴庫,并且將加載的庫添加到allImages數組中保存。

找到dyld的入口,把所有鏡像的信息傳遞給dyld,然后run?initializers。

加載完閉包后返回一個result,這是一個假main()函數的入口,內部實現就是return 0;

到此,dyld3的啟動過程就完成了。

但是_main()函數還未結束,后續的代碼是使用dyld2的方式加載流程。模擬器以及iOS13以下是沒有dyld3的,所以是走的dyld2啟動流程。

dyld2

后面我們繼續分析dyld2的流程:

實例化主程序

// add dyld itself to UUID list

addDyldImageToUUIDList();

這里會添加?dyld?的鏡像文件到?UUID?列表中,主要的目的是啟用堆棧的符號化。

這里有個SUPPORT_ACCELERATE_TABLES宏,判斷是否支持加速器表。從注釋我們可以得出arm64架構的情況下是不使用加速器表的。(后面還會有個有意思的判斷)

然后接著就是reloadAllImages:的一個標簽。可以使用goto語句直接跳轉到這個標簽,然后從這開始執行代碼。

這里插一個知識點:ImageLoader

ImageLoader 是一個用于加載可執行文件的基類,它負責鏈接鏡像,但不關心具體文件格式,因為這些都交給子類去實現。每個可執行文件都會對應一個 ImageLoader實例。ImageLoaderMachO 是用于加載 Mach-O 格式文件的 ImageLoader 子類,而 ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 都繼承于 ImageLoaderMachO,分別用于加載那些 __LINKEDIT 段為傳統格式和壓縮格式的 Mach-O 文件。


這一步將主程序的Mach-O加載進內存,并實例化一個ImageLoader。instantiateFromLoadedImage()首先調用isCompatibleMachO()檢測Mach-O頭部的magic、cputype、cpusubtype等相關屬性,判斷Mach-O文件的兼容性,如果兼容性滿足,則調用ImageLoaderMachO::instantiateMainExecutable()實例化主程序的ImageLoader。

ImageLoaderMachO::instantiateMainExecutable()函數里面首先會調用sniffLoadCommands()函數來獲取一些數據,包括:

compressed:若Mach-O存在LC_DYLD_INFO和LC_DYLD_INFO_ONLY加載命令,則說明是壓縮類型的Mach-O

segCount:根據 LC_SEGMENT_COMMAND 加載命令來統計段數量,這里拋出的錯誤日志也說明了段的數量是不能超過255個

libCount:根據 LC_LOAD_DYLIB、LC_LOAD_WEAK_DYLIB、LC_REEXPORT_DYLIB、LC_LOAD_UPWARD_DYLIB 這幾個加載命令來統計庫的數量,庫的數量不能超過4095個

當sniffLoadCommands()解析完以后,根據compressed的值來決定調用哪個子類進行實例化。

這里總結為4步:

ImageLoaderMachOCompressed::instantiateStart()創建ImageLoaderMachOCompressed對象。

image->disableCoverageCheck()禁用段覆蓋檢測。

image->instantiateFinish()首先調用parseLoadCmds()解析加載命令,然后調用this->setDyldInfo()設置動態庫鏈接信息,最后調用this->setSymbolTableInfo() 設置符號表相關信息,代碼片段如下:

image->setMapped()函數注冊通知回調、計算執行時間等等。

在調用完ImageLoaderMachO::instantiateMainExecutable()后繼續調用addImage(),將image加入到sAllImages全局鏡像列表,并將image映射到申請的內存中。

加載插入的動態庫

這一步是加載環境變量DYLD_INSERT_LIBRARIES中配置的動態庫,先判斷環境變量DYLD_INSERT_LIBRARIES中是否存在要加載的動態庫,如果存在則調用loadInsertedDylib()依次加載。

loadInsertedDylib()內部設置了一個LoadContext參數后,調用了load()函數,

load()函數的實現為一系列的loadPhase*()函數,loadPhase0()~loadPhase1()函數會按照下面所示順序搜索動態庫,并調用不同的函數來繼續處理。

DYLD_ROOT_PATH ? --> ?LD_LIBRARY_PATH ?--> ?DYLD_FRAMEWORK_PATH ||?DYLD_LIBRARY_PATH ?--> ?raw path ?--> ?DYLD_FALLBACK_LIBRARY_PATH

當內部調用到loadPhase5load()函數的時候,會先在共享緩存中搜尋,如果存在則使用ImageLoaderMachO::instantiateFromCache()來實例化ImageLoader,否則通過loadPhase5open()打開文件并讀取數據到內存后,再調用loadPhase6(),通過ImageLoaderMachO::instantiateFromFile()實例化ImageLoader,最后調用checkandAddImage()驗證鏡像并將其加入到全局鏡像列表中。


鏈接主程序

在調用link()進行鏈接主程序之前,會有一個宏判斷,如果支持加速器表并且主程序已經rebase了,則重新rebase一次,用于ASLR的工作。

這一步調用link()函數先將傳入的image添加進sAllImages全局靜態數組,接著將image添加進sImageRoots(后續遞歸初始化所有image的時候有用到)數組,然后將實例化后的主程序進行動態修正,讓二進制變為可正常執行的狀態。link()函數內部調用了ImageLoader::link()函數

從源代碼可以看到,這一步主要做了以下幾個事情:

recursiveLoadLibraries() 根據LC_LOAD_DYLIB加載命令把所有依賴庫加載進內存。

recursiveUpdateDepth() 遞歸刷新依賴庫的層級。

recursiveRebase() 由于ASLR的存在,必須遞歸對主程序以及依賴庫進行重定位操作。

recursiveBind() 把主程序二進制和依賴進來的動態庫全部執行符號表綁定。

weakBind() 如果鏈接的不是主程序二進制的話,會在此時執行弱符號綁定,主程序二進制則在link()完后再執行弱符號綁定,后面會進行分析。

recursiveGetDOFSections()、context.registerDOFs() 注冊DOF(DTrace Object Format)節。

鏈接插入的動態庫

這一步與鏈接主程序一樣,將前面調用addImage()函數保存在sAllImages中的動態庫列表循環取出并調用link()進行鏈接,需要注意的是,sAllImages中保存的第一項是主程序的鏡像,所以要從i+1的位置開始,取到的才是動態庫的ImageLoader:

ImageLoader* image = sAllImages[i+1];

接下來循環調用每個鏡像的registerInterposing()函數,該函數會遍歷Mach-O的LC_SEGMENT_COMMAND加載命令,讀取__DATA,__interpose,并將讀取到的信息保存到fgInterposingTuples中,為后續的bind做準備工作。


這里宏判斷加速器表,然后如果加速器表和隱試插入庫同時存在,則禁用加速器表,并且使用goto 語句跳轉到reloadAllImages重新加載。

遞歸Bind

先調用applyInterposingToDyldCache()申請將所有鏈接的庫插入共享緩存,內部將會找到鏈接庫的symbol符號表,為后面的Rebase做準備。

接著執行主程序的recursiveBindWithAccounting()函數,該函數內部其實就是調用了recursiveBind(),然后for循環對插入的庫執行recursiveBind(),recursiveBind內部先進行遞歸所有依賴庫調用recursiveBind,然后調用doBind()函數,該函數有2個不一樣的實現,分別在ImageLoaderMachOClassic.cpp 和?ImageLoaderMachOCompressed.cpp文件中。

ImageLoaderMachOClassic.cpp文件的實現:調用doBindExternalRelocations 以及?bindIndirectSymbolPointers執行真正的符號綁定。

ImageLoaderMachOCompressed.cpp文件的實現:調用eachBind() 和eachLazyBind(),具體處理函數是bindAt()。

執行弱符號綁定

weakBind()首先通過getCoalescedImages()合并所有動態庫的弱符號到一個列表里,然后調用initializeCoalIterator()對需要綁定的弱符號進行排序,接著調用incrementCoalIterator()讀取dyld_info_command結構的weak_bind_off和weak_bind_size字段,確定弱符號的數據偏移與大小,最終進行弱符號綁定。


執行初始化方法(重點)

這一步由initializeMainExecutable()完成。我們看看內部的具體實現:

先看注釋?run initialzers for any inserted dylibs 將已經插入的動態庫執行初始化操作。

然后用一個for循環拿到每一個動態庫的指針調用runInitializers()函數。

繼續看注釋?run initializers for main executable and everything it brings up ,執行主程序的初始化。

所以dyld會優先初始化鏈接的動態庫,然后再初始化主程序。

這點很重要,后面我會從源碼分析為什么dyld要這樣做!


runInitializers()內部調用了processInitializers()

函數的注釋解釋:向上動態庫初始化的執行為時過早。為了處理向上鏈接而不是向下鏈接的懸空動態庫,所有向上鏈接的動態庫都將其初始化延遲到通過向下動態庫的遞歸完成之后。

什么意思呢,動態庫直接也是有依賴關系的,舉個例子:現在要初始化動態庫A,但是動態庫A依賴了動態庫B,動態庫B又依賴了動態庫C,D等,那么這里就會遞歸的先初始化C、D,然后再初始化B,最后才初始化A。

processInitializers內部調用了recursiveInitialization()來進行遞歸初始化,我們再來看看recursiveInitialization的實現

為了方便截圖,筆著把for循環遞歸調用邏輯給折疊了,從注釋也可以知道低級的庫優先初始化。這跟我們上面的分析是一樣的。然后我們重點看看紅框里的代碼,context上下文我們之前分析過了,里面保存了很多函數的地址,參數等。

紅框內先調用了notifySingle,注意第一個參數dyld_image_state_dependents_initialized,說明這次是調用的依賴庫的初始化,然后接著調用了doInitialization(),從函數名稱完全可以猜到這個函數才是真正去執行初始化操作的。接著再次調用notifySingle,看第一個參數dyld_image_state_initialized,這次是當前庫的初始化。我們先分析notifySingle的內部實現(非常重要!!!),后面再分析doInitialization。

notifySingle函數里面唯一跟init有關的能調用的函數就是上圖紅色框里的sNotifyObjCInit。全局搜索sNotifyObjCInit。只發現registerObjCNotifiers函數內部的賦值操作。

// record functions to call

sNotifyObjCMapped = mapped;

sNotifyObjCInit = init;

sNotifyObjCUnmapped = unmapped;

看注釋:記錄函數用于調用。這里賦值了3個函數,一個mapped,一個init,一個unmapped。

接著全局搜索registerObjCNotifiers,發現是_dyld_objc_notify_register函數調用的。

全局搜索一下,發現在dyldAPIsInLibSystem.cpp文件中還有一個具體的實現。但是沒有發現有任何函數調用過_dyld_objc_notify_register。

上圖說明這個函數可能跟LibSystem.dylib有關。

那么到底誰調用了_dyld_objc_notify_register()呢?靜態分析已經無法得知,只能對_dyld_objc_notify_register()下個符號斷點觀察一下了,

點擊Xcode的“Debug”菜單,然后點擊“Breakpoints”,接著選擇“Create Symbolic Breakpoint...”。然后添加_dyld_objc_notify_register,運行工程,如下圖所示

請看打印信息,調用順序如下:


最后發現是_objc_init()方法調用的,而且也驗證了上面真正做初始化的函數是doInitialization()

我們把上面的幾個庫(libSystem、libdispatch、libobjc)的源碼都下載下來驗證一下!

從libSystem_init()到_objc_init(),內部調用順序和我們打印的確實一樣,最終調用了_dyld_objc_notify_register(),然后_objc_init函數就結束了。

那么問題來了,libSystem_initializer()什么時候調用的呢?

其實這個答案在前文分析initializeMainExecutable()源碼的時候就已經講過了。dyld會優先初始化動態庫,然后初始化主程序。libSystem_initializer()就是在這個時候調用的。

我們回到_dyld_objc_notify_register函數來,看看這3個參數,都是函數指針

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

最后得到

sNotifyObjCMapped = map_images;

sNotifyObjCInit = load_images;

sNotifyObjCUnmapped = unmap_image;

也就是說sNotifyObjCInit的調用其實就是調用load_images();

由于篇幅的原因,load_images的分析我們放到下一節。

前文提到doInitialization()才是真正執行初始化的函數,我們回過頭來繼續分析doInitialization()的具體實現:

內部調用doImageInit() 和?doModInitFunctions()

這2個函數內部都有libSystemInitialized的判斷,要求libSystem這個動態庫必須先初始化,這也驗證了我們前面的結論是對的。

然后調用func執行初始化方法。假設當前是libSystem庫,那么這個func就是libSystem_initializer()

到此,所有的初始化方法調用已經閉環。


查找入口點并返回

這一步調用主程序鏡像的getEntryFromLC_MAIN(),從加載命令讀取LC_MAIN入口,如果沒有LC_MAIN就調用getEntryFromLC_UNIXTHREAD()讀取LC_UNIXTHREAD,找到后就跳到入口點指定的地址并返回。

至此,整個dyld的加載過程就分析完成了。

總結下:

dyld3的加載流程如下:

第一步:設置運行環境。

第二步:加載共享緩存。

第三步:檢查啟動閉包。

第四步:構建啟動閉包。

第五步:加載啟動閉包。(這一步包括dyld2中的3、4、5、6、7、8)

第六步:找到入口點并返回。

dyld2的加載流程如下:

第一步:設置運行環境。

第二步:加載共享緩存。

第三步:實例化主程序。

第四步:加載插入的動態庫。

第五步:鏈接主程序。

第六步:鏈接插入的動態庫。

第七步:執行弱符號綁定

第八步:執行初始化方法。

第九步:查找入口點并返回。

那么我們現在既然了解了dyld的加載流程,那么在這個階段我們能做什么具體的優化呢?

1、避免鏈接無用的 frameworks,在 Xcode 中檢查一下項目中的「Linked Frameworks and Librares」部分是否有無用的鏈接

2、避免在啟動時加載動態庫,將項目的 Pods 以靜態編譯的方式打包,尤其是 Swift 項目,這地方的時間損耗是很大的

3、硬鏈接你的依賴項,這里做了緩存優化

也許有人會有疑惑,現在都使用了 dyld3 了,我們就不需要做 Static Link 了,其實還是需要的,這里放2張對比圖給大家看看。

Dyld 2 和 Dyld 3啟動時間的對比:

Static linking 和 Dyld 3啟動時間對比:

結果很明顯,可以看出,在冷啟動時Dyld3 比 Dyld2快20%,靜態鏈接依舊是比dyld3要快,提升了23%左右。

dyld2加載流程圖如下:

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

推薦閱讀更多精彩內容