前言
小測驗:
- 透過以下三種函數請問加載順序為何?
- +load方法,main()函數,C++方法
-
通過運行程序可以知道,打印順序為
load → C++ → main函數
為了瞭解為何運行的程序是這樣的順序,我們進行以下分析
在分析程序前,我們先補充一些底層知識
編譯過程
- 在我們將運行程序前,會進行編譯的動作,主要有以下幾步
-
源文件
:載入.h .m .cpp文件 -
預編譯
:替換宏,刪除註釋,展開頭文件,產生.i文件 -
編譯
:將.i文件轉換為匯編語言,產生.s文件 -
匯編
: 將匯編文件轉換為其他機器碼文件,產生.o文件 -
鏈接
:對.o文件中引用其他庫的地方進行引用,生成最後的可執行文件
-
- 編譯過程流程圖
靜態庫與動態庫
-
靜態庫
:在鏈接時會將編譯後的彙編代碼生成出來的目標程序與引用的庫文件一起鏈接打包為可執行文件,此時靜態庫就不會改變,而是拷貝了一份到目標程序裡。-
優勢
:編譯後,對於目標程序沒有多餘的依賴,可直接運行。 -
劣勢
:由於靜態庫會有兩份,所以會導致目標程序體積增大,對於內存,性能,速度消耗很大
-
-
動態庫
:程序編譯時並不會鏈接到目標程序中,目標程序只會存儲指向動態庫的引用,在程序運行時才被載入-
優勢
:- 減少打包後的app的大小:因為不需要拷貝到目標程序中,所以不會影響到目標程序的體積,與靜態庫相比,減少了app的體積大小。
- 共享內存:同一份庫可以被多個程序使用。
- 方便更新程序:由於運行時才載入的特性,可以隨時對庫進行替換,不需要重新編譯代碼。
-
劣勢
:動態載入會帶來一部分性能損失,使用動態庫也會是目標程序對於外部依賴,如果缺少了動態庫則回造成程序無法運行。
-
dyld加載流程分析
以下將根據dyld源碼,搭配
libobjc
、libSystem
、libdispatch
源碼分析dyld(dynamic link editor) 是蘋果動態鏈接器,在app被編譯打包成Mach-O文件時,交由dyld負責鏈接,加載程序
APP啟動流程圖如下
app啟動的起始點
- 在上述的測驗中,我們想知道app的起始點,可以從我們已知的入手,也就是剛剛的第一個調用的函數+load(),進行分析。
- 通過bt打印堆棧信息,查看app啟動是從哪裡開始的
- 可以看到是從dyld中的_dyld_start開始的,所以需要去下列網址下載原始碼進行分析
- 另外也可以透過_dyld_start
dyld::_main源碼分析
- 如下為版本
dyld750.6
源碼查找_dyld_start
可以在匯編代碼的arm64架構下看到註釋 也就是程式會運行到dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
這個函數為C++
函數
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
- 源碼中搜索
dyldbootstrap
找到命名作用空間,在這個文件中查找start
方法,其核心就是返回值就是調用dyld::main
函數,其中macho_header是Mach-O的頭部,而dyld
加載的文件就是Mach-O類型
的,即Mach-O類型是可執行文件類型,Mach-O header,Load command ,section,Other Data
,可以通過MachOViewt查看。
- 可透過MachOview查看文件各個區域
【第一步】配置環境變量
- 根據環境變量設置相應的值以獲取當前運行的架構。
【第二步】共享緩存
- 檢查是否開啟共享緩存,以及共享緩存是否緩存映射到共享區域,例如UIKit,coreFoundation
【第三步】主程序初始化
- 調用instantiateFromLoadedImage函數實例化一個ImageLoader對象
【第四步】插入動態庫
- 遍歷sEnv.DYLD_INSERT_LIBRARIES環境變量,調用加載loadInsertedDylib方法
【第五步】鏈接主程序
- 鏈接主程序
【第六步】鏈接動態庫
【第七步】弱符號綁定
【第八步】執行初始化方法
【第九步】尋找主程序入口main函數:
- 從Load Command讀取LC_MAIN入口,如果沒有就讀取LC_UNIXTHREAD,這樣就來到了日常開發的main函數了。
- 以下主要將分析第三步及第八步
- 【第三步】主程序初始化
-
sMainExecutable
表示主程序變量,查看其賦值,是通過instantiateFromLoadedImage
方法初始化
- 進入
instantiateFromLoadedImage
源碼,其中創建一個ImageLoader
實例對象,通過instantiateMainExecutable
方法創建
- 進入
instantiateMainExecutable
源碼,其作用是為主可執行文件創建映像,返回一個ImageLoader
類型的image對象,即主程序。其中sniffLoadCommands
函數時獲得Mach-O類型文件
的Load Command的相關信息,並對其進行各種校驗。
- 【第八步】執行初始化方法
- 進入
initializeMainExecutable
源碼,主要是循環遍歷,都會執行runInitializers
方法
- 全局搜索
runInitializers(cons
,找到如下源碼,其核心代碼是processInitializers
函數的調用
- 進入
processInitializers
函數的源碼實現,其中對鏡像列表調用recursiveInitialization
函數進行遞歸實例化。
- 全局搜索
recursiveInititalization(cons
函數,其源碼實現如下
- 在這裡,需要分成兩個部分探索,一部分是
notifySingle
函數,一部分是doInitialization
函數首先探索notifySingle
函數
notifySingle函數
- 全局搜索
notifySingle(
函數,其重點是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
這句
- 全局搜索
sNotifyObjCInit
發現沒有找到實現,有賦值操作
- 搜索registerObjCNotifiers在哪裡調用了,發現在
_dyld_objc_notify_register
進行了調用
注意:_dyld_objc_notify_register
的函數需要在libobjc
源碼中搜索
- 在
objc4-781
源碼中搜索_dyld_objc_notify_register
,發現在_objc_init
源碼中調用了該方法,並傳入了參數,所以sNotifyObjCInit
的賦值就是objc中的load_images,而load_images
會調用所有的+load
方法,綜上所述,notifySingle
是一個回調函數
。
load函數加載
下面我們進入load_images
的源碼看看其實現,以此來證明load_images
中調用了所有的load函數
- 通過objc源碼中
_objc_init
源碼實現,進入load_images
的源碼實現
- 進入
call_load_methods
源碼實現,可以發現其核心是通過do-while
循環調用+load
方法
- 進入call_class_loads源碼實現,了解到這裏調用的load方法證實我們前文提及的類load方法
- 所以,load_images調用了所有的load函數,以上的源碼分析過程正好對應堆棧的打印信息
- 【總結】load的源碼鏈結為:
_dyld_start
--> dyldbootstrap::start
--> dyld::_main
--> dyld::initializeMainExecutable
--> ImageLoader::runInitializers
--> ImageLoader::processInitializers
--> ImageLoader::recursiveInitialization
--> dyld::notifySingle
(回調函數) --> sNotifyObjCInit
--> load_images(libobjc.A.dylib)
- 那麼
_objc_init
是什麼時候調用的呢?
doInitialization 函數
- 走到
objc
的_objc_init
函數,發現不通了,我們退回到recursiveInitialization
遞歸函數的源碼實現,發現我們忽略一個函數doInitialization
- 進入
doInitialization
函數的源碼實現
- 這裏也需要分成兩部份,一部分是
doImageInit
函數,一部分是doModInitFunctions
函數。 - 進入
doImageInit
源碼實現,其核心主要是for循環加載方法的調用
,這裏需要注意的一點是,libSystem
的初始化必須先運行
。
- 進入
doModInitFunctions
源碼實現,這個方法中加載了所有Cxx
文件
- 可以通過測試程序的堆棧信息來驗證,在C++方法處加一個斷點
- 走到這裡,還是沒有找到
_objc_init
的調用,我們通過_objc_init
加一個符號斷點來查看調用_objc_init
前的堆棧信息。 -
_objc_init
加一個符號斷點,運行程序,查看_objc_init
斷住後的堆棧信息
- 在
libsystem
中查找libSystem_initializer
,查看其中實現
- 根據前面的堆棧信息,我們發現走的是
libSystem_initializer
中會調用libdispatch_init
函數,而這個函數的源碼是在libdsipatch開源庫中的,在libdispatch中搜尋libdispatch_init
- 進入
_os_object_init
源碼實現,其源碼實現調用了_objc_init
函數
結合上面的分析,從初始化
_objc_init
註冊的_dyld_objc_notify_register
的參數2,即load_images
,到sNotifySingle
—>sNotifyObjCInie=參數2
到sNotifiyObjcInit()
調用,形程了一個閉環
所以可以簡單的理解
sNotifySingle
這裡是添加通知即addObsever
,_objc_init
中調用_dyld_objc_notify_register
相當於發送通知,即push
,而sNotifyObjcInit
相當於通知的處理函數,即selector
【總結】:_objc_init的源碼鏈:
_dyld_start
--> dyldbootstrap::start
--> dyld::_main
--> dyld::initializeMainExecutable
--> ImageLoader::runInitializers
--> ImageLoader::processInitializers
--> ImageLoader::recursiveInitialization
--> doInitialization
-->libSystem_initializer
(libSystem.B.dylib) --> _os_object_init
(libdispatch.dylib) --> _objc_init
(libobjc.A.dylib)
第九步:尋找主入口函數
- 匯編調適,可以看到顯示來到
+[ViewController load]
方法
- 繼續執行,來到kcFuna的C++函數
- 點擊stepover繼續往下,讀取寄存器rax 可以看到會進入main函數
- dyld彙編源碼實現
注意:main是寫死的函數,寫入內存,讀取到dyld,如果修改了main函數的名稱,會報錯如下
所以,最終dyld加載流程,如下圖所示