iOS-底層原理 15:dyld加載流程

iOS 底層原理 文章匯總

本文的目的主要是分析dyld的加載流程,了解在main函數(shù)之前,底層還做了什么

引子

  • 創(chuàng)建一個project,在ViewController中重寫了load方法,在main中加了一個C++方法,即kcFUnc,請問它們的打印先后順序是什么?

    問題引入

  • 運行程序,查看 load、kcFunc、main的打印順序,下面是打印結果,通過結果可以看出其順序是 load --> C++方法 --> main

    打印結果

為什么是這么一個順序?按照常規(guī)的思維理解,main不是入口函數(shù)嗎?為什么不是main最先執(zhí)行?

下面根據(jù)這個問題,我們來探索在走到main之前,到底還做了什么。

編譯過程及庫

在分析app啟動之前,我們需要先了解iOSapp代碼的編譯過程以及動態(tài)庫靜態(tài)庫。

編譯過程

其中編譯過程如下圖所示,主要分為以下幾步:

  • 源文件:載入.h、.m、.cpp等文件
  • 預處理:替換宏,刪除注釋,展開頭文件,產(chǎn)生.i文件
  • 編譯:將.i文件轉換為匯編語言,產(chǎn)生.s文件
  • 匯編:將匯編文件轉換為機器碼文件,產(chǎn)生.o文件
  • 鏈接:對.o文件中引用其他庫的地方進行引用,生成最后的可執(zhí)行文件
    編譯過程

靜態(tài)庫 和 動態(tài)庫

  • 靜態(tài)庫:在鏈接階段,會將可匯編生成的目標程序與引用的庫一起鏈接打包到可執(zhí)行文件當中。此時的靜態(tài)庫就不會在改變了,因為它是編譯時被直接拷貝一份,復制到目標程序里的
    • 好處:編譯完成后,庫文件實際上就沒有作用了,目標程序沒有外部依賴,直接就可以運行

    • 缺點:由于靜態(tài)庫會有兩份,所以會導致目標程序的體積增大,對內存、性能、速度消耗很大

  • 動態(tài)庫:程序編譯時并不會鏈接到目標程序中,目標程序只會存儲指向動態(tài)庫的引用,在程序運行時才被載入
    • 優(yōu)勢
      • 減少打包之后app的大小:因為不需要拷貝至目標程序中,所以不會影響目標程序的體積,與靜態(tài)庫相比,減少了app的體積大小

      • 共享內存,節(jié)約資源:同一份庫可以被多個程序使用

      • 通過更新動態(tài)庫,達到更新程序的目的:由于運行時才載入的特性,可以隨時對庫進行替換,而不需要重新編譯代碼

    • 缺點:動態(tài)載入會帶來一部分性能損失,使用動態(tài)庫也會使得程序依賴于外部環(huán)境,如果環(huán)境缺少了動態(tài)庫,或者庫的版本不正確,就會導致程序無法運行

靜態(tài)庫和動態(tài)庫的圖示如圖所示


靜態(tài)庫和動態(tài)庫圖示

dyld加載流程分析

根據(jù)dyld源碼,以及libobjc、libSystem、libdispatch源碼協(xié)同分析

什么是dyld?

dyld(the dynamic link editor)是蘋果的動態(tài)鏈接器,是蘋果操作系統(tǒng)的重要組成部分,在app被編譯打包成可執(zhí)行文件格式的Mach-O文件后,交由dyld負責連接,加載程序

所以 App的啟動流程圖如下


App啟動流程

app啟動的起始點

  • 在前文的demo中,在load方法處加一個斷點,通過bt堆棧信息查看app啟動是從哪里開始的

    app啟動起點

    【app啟動起點】:通過程序運行發(fā)現(xiàn),是從dyld中的_dyld_start開始的,所以需要去OpenSource下載一份dyld的源碼來進行分析

  • 也可以通過xcode左側的堆棧信息來找到入口


    xcode堆棧信息

dyld::_main函數(shù)源碼分析

  • dyld-750.6源碼中查找_dyld_start,查找arm64架構發(fā)現(xiàn),是由匯編實現(xiàn),通過匯編注釋發(fā)現(xiàn)會調用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)方法,是一個C++方法(以arm64架構為例)

    dyldbootstrap::start源碼

  • 源碼中搜索dyldbootstrap找到命名作用空間,再在這個文件中查找start方法,其核心是返回值的調用了dyldmain函數(shù),其中macho_headerMach-O的頭部,而dyld加載的文件就是Mach-O類型的,即Mach-O類型是可執(zhí)行文件類型,由四部分組成:Mach-O頭部、Load Command、section、Other Data,可以通過MachOView查看可執(zhí)行文件信息

    _main源碼實現(xiàn)

  • 進入dyld::_main的源碼實現(xiàn),特別長,大約600多行,如果對dyld加載流程不太了解的童鞋,可以根據(jù)_main函數(shù)的返回值進行反推,這里就多作說明。在_main函數(shù)中主要做了一下幾件事情:

    • 【第一步:環(huán)境變量配置】:根據(jù)環(huán)境變量設置相應的值以及獲取當前運行架構

      第一步

    • 【第二步:共享緩存】:檢查是否開啟了共享緩存,以及共享緩存是否映射到共享區(qū)域,例如UIKitCoreFoundation

      第二步

    • 【第三步:主程序的初始化】:調用instantiateFromLoadedImage函數(shù)實例化了一個ImageLoader對象

      第三步

    • 【第四步:插入動態(tài)庫】:遍歷DYLD_INSERT_LIBRARIES環(huán)境變量,調用loadInsertedDylib加載

      第四步

    • 【第五步:link 主程序

      第五步

    • 【第六步:link 動態(tài)庫

      第六步

    • 【第七步:弱符號綁定

      第七步

    • 【第八步:執(zhí)行初始化方法

      第八步

    • 【第九步:尋找主程序入口main函數(shù)】:從Load Command讀取LC_MAIN入口,如果沒有,就讀取LC_UNIXTHREAD,這樣就來到了日常開發(fā)中熟悉的main函數(shù)了

      第九步

下面主要分析下【第三步】和【第八步】

第三步:主程序初始化

  • sMainExecutable表示主程序變量,查看其賦值,是通過instantiateFromLoadedImage方法初始化

    instantiateFromLoadedImage初始化主程序

  • 進入instantiateFromLoadedImage源碼,其中創(chuàng)建一個ImageLoader實例對象,通過instantiateMainExecutable方法創(chuàng)建

    instantiateFromLoadedImage源碼實現(xiàn)

  • 進入instantiateMainExecutable源碼,其作用是為主可執(zhí)行文件創(chuàng)建映像,返回一個ImageLoader類型的image對象,即主程序。其中sniffLoadCommands函數(shù)時獲取Mach-O類型文件Load Command的相關信息,并對其進行各種校驗

    instantiateMainExecutable源碼實現(xiàn)

第八步:執(zhí)行初始化方法

  • 進入initializeMainExecutable源碼,主要是循環(huán)遍歷,都會執(zhí)行runInitializers方法

    initializeMainExecutable源碼實現(xiàn)

  • 全局搜索runInitializers(cons,找到如下源碼,其核心代碼是processInitializers函數(shù)的調用

    runInitializers源碼實現(xiàn)

  • 進入processInitializers函數(shù)的源碼實現(xiàn),其中對鏡像列表調用recursiveInitialization函數(shù)進行遞歸實例化

    processInitializers源碼實現(xiàn)

  • 全局搜索recursiveInitialization(cons函數(shù),其源碼實現(xiàn)如下

    recursiveInitialization源碼實現(xiàn)

在這里,需要分成兩部分探索,一部分是notifySingle函數(shù),一部分是doInitialization函數(shù),首先探索notifySingle函數(shù)

notifySingle 函數(shù)
  • 全局搜索notifySingle(函數(shù),其重點是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());這句

    notifySingle源碼實現(xiàn)

  • 全局搜索sNotifyObjCInit,發(fā)現(xiàn)沒有找到實現(xiàn),有賦值操作

    registerObjCNotifiers源碼實現(xiàn)

  • 搜索registerObjCNotifiers在哪里調用了,發(fā)現(xiàn)在_dyld_objc_notify_register進行了調用

    _dyld_objc_notify_register源碼實現(xiàn)

    注意:_dyld_objc_notify_register的函數(shù)需要在libobjc源碼中搜索

  • objc4-781源碼中搜索_dyld_objc_notify_register,發(fā)現(xiàn)在_objc_init源碼中調用了該方法,并傳入了參數(shù),所以sNotifyObjCInit賦值的就是objc中的load_images,而load_images會調用所有的+load方法。所以綜上所述,notifySingle是一個回調函數(shù)

    _objc_init源碼實現(xiàn)

load函數(shù)加載

下面我們進入load_images的源碼看看其實現(xiàn),以此來證明load_images中調用了所有的load函數(shù)

  • 通過objc源碼中_objc_init源碼實現(xiàn),進入load_images的源碼實現(xiàn)

    load_images源碼實現(xiàn)

  • 進入call_load_methods源碼實現(xiàn),可以發(fā)現(xiàn)其核心是通過do-while循環(huán)調用+load方法

    call_load_methods源碼實現(xiàn)

  • 進入call_class_loads源碼實現(xiàn),了解到這里調用的load方法證實我們前文提及的類的load方法

    call_class_loads源碼實現(xiàn)

所以,load_images調用了所有的load函數(shù),以上的源碼分析過程正好對應堆棧的打印信息

堆棧信息

【總結】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 函數(shù)
  • 走到objc_objc_init函數(shù),發(fā)現(xiàn)走不通了,我們回退到recursiveInitialization遞歸函數(shù)的源碼實現(xiàn),發(fā)現(xiàn)我們忽略了一個函數(shù)doInitialization

    recursiveInitialization源碼實現(xiàn)

  • 進入doInitialization函數(shù)的源碼實現(xiàn)

    doInitialization源碼實現(xiàn)

    這里也需要分成兩部分,一部分是doImageInit函數(shù),一部分是doModInitFunctions函數(shù)

    • 進入doImageInit源碼實現(xiàn),其核心主要是for循環(huán)加載方法的調用,這里需要注意的一點是,libSystem的初始化必須先運行
      doImageInit源碼實現(xiàn)
    • 進入doModInitFunctions源碼實現(xiàn),這個方法中加載了所有Cxx文件
      doModInitFunctions源碼實現(xiàn)

      可以通過測試程序的堆棧信息來驗證,在C++方法處加一個斷點
      C++斷點堆棧信息

走到這里,還是沒有找到_objc_init的調用?怎么辦呢?放棄嗎?當然不行,我們還可以通過_objc_init加一個符號斷點來查看調用_objc_init前的堆棧信息,

  • _objc_init加一個符號斷點,運行程序,查看_objc_init斷住后的堆棧信息

    _objc_init符號斷點堆棧信息

  • libsystem中查找libSystem_initializer,查看其中的實現(xiàn)

    libSystem_initializer源碼實現(xiàn)

  • 根據(jù)前面的堆棧信息,我們發(fā)現(xiàn)走的是libSystem_initializer中會調用libdispatch_init函數(shù),而這個函數(shù)的源碼是在libdispatch開源庫中的,在libdispatch中搜索libdispatch_init

    libdispatch_init源碼實現(xiàn)

  • 進入_os_object_init源碼實現(xiàn),其源碼實現(xiàn)調用了_objc_init函數(shù)

    _os_object_init源碼實現(xiàn)

    結合上面的分析,從初始化_objc_init注冊的_dyld_objc_notify_register的參數(shù)2,即load_images,到sNotifySingle --> sNotifyObjCInie=參數(shù)2sNotifyObjcInit()調用,形成了一個閉環(huán)

所以可以簡單的理解為sNotifySingle這里是添加通知即addObserver,_objc_init中調用_dyld_objc_notify_register相當于發(fā)送通知,即push,而sNotifyObjcInit相當于通知的處理函數(shù),即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)

第九步:尋找主入口函數(shù)

  • 匯編調試,可以看到顯示來到+[ViewController load]方法

    匯編調試-load

  • 繼續(xù)執(zhí)行,來到kcFunc的C++函數(shù)

    匯編調試-kcFunc

  • 點擊stepover,繼續(xù)往下,跑完了整個流程,會回到_dyld_start,然后調用main()函數(shù),通過匯編完成main的參數(shù)賦值等操作

    匯編調試回到_dyld_start

    dyld匯編源碼實現(xiàn)
    dyld中main部分的匯編源碼實現(xiàn)

注意:main是寫定的函數(shù),寫入內存,讀取到dyld,如果修改了main函數(shù)的名稱,會報錯

報錯信息

所以,綜上所述,最終dyld加載流程,如下圖所示,圖中也詮釋了前文中的問題:為什么是load-->Cxx-->main的調用順序

dyld加載流程

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容