Objective-C中的load方法執行的來龍去脈

引子

我們都知道: Objective-C中類Class+load方法會在類第一次加載到內存時, 并且APP的整個生命周期只會執行一次. 但是知其然最好知其所以然, 今天來分析一下+load方法執行的來龍去脈.

準備工作, 本文涉及到的Apple 開源源碼如下:

  • dyld-635.2
  • objc4-750

上一篇文章<<iOS APP啟動前后發生了什么?>>開篇, 有如下的調用棧:

0 +[AppDelegate load]
1 call_load_methods
2 load_images
// 這里是一個斷層
3 dyld::notifySingle(dyld_image_states, ImageLoader const*)
4 ImageLoader::recursiveInitialization(...)
5 ImageLoader::processInitializers(...)
6 ImageLoader::runInitializers(...)
7 dyld::_main(...)
8 dyldbootstrap::start(...)
9 _dyld_start

我們能看到實際最后會調用+[Class load]類方法, 我們發現從調用棧那里有一個斷層, 3 dyld::notifySingle(dyld_image_states, ImageLoader const*) -> +[AppDelegate load]的過程, 明顯不是在dyld, ImageLoader庫中, 而是在runtime中的方法, 重要的原因就是dyld::notifySingle是對外發送兩個一個通知, 而loadImage是針對通知注冊的handler.

而前文在講到, 當運行到后面會在runtime初始化時調用_objc_init, 這個方法最后會調用dyld::_dyld_objc_notify_register方法注冊三個hanlder, 其中有一個方法就是runtimeload_images, 因此dyld::notifySingle實際是發出了某個通知, 觸發load_images.

void _objc_init(void) {
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    // 這里是在dyld中加入一個監聽器, 一旦dyld監聽到有新的鏡像加載到runtime時, 就調用 load_images 方法, 并傳入最新鏡像的信息類別 infoList
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

dyld_image的state的監聽與通知

為了證明我們前面的內容, 我們需要在源碼中去找到線索.

我們打開dyld的源碼dyld_priv.h, 中的關于dyld_image_states的定義:

// DEPRECATED 
// dyld_image 整個生命周期中會經歷的狀態
enum dyld_image_states {
    dyld_image_state_mapped                 = 10,       // No batch notification for this - 是否已經映射
    dyld_image_state_dependents_mapped      = 20,       // Only batch notification for this - 依賴是否映射
    dyld_image_state_rebased                = 30,       // rebase
    dyld_image_state_bound                  = 40,       // 已經bound
    dyld_image_state_dependents_initialized = 45,       // Only single notification for this
    dyld_image_state_initialized            = 50,       // -- 已經初始化!!!!! 重要的狀態
    dyld_image_state_terminated             = 60        // Only single notification for this
};

而且dyld_image.state的狀態切換都是在dyld::_main(...)方法中進行的, 我們將該方法簡寫如下:

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[], uintptr_t* startGlue){
    
    ...

    // dyld::instantiateFromLoadedImage ->  ImageLoaderMachOClassic::instantiateMainExecutable(create image for main executable) -> setMapped -> dyld_state = dyld_image_state_mapped-> 發出notification
    // 初始化完成以后, sMainExecutable被push到 sAllImages, 并且將它的關鍵信息插入到MappedRanges鏈表(這個鏈表中的內容已經mapped完畢)
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath); // dyld_image_state_mapped 并通知

    ...

    /*
    注意, 這里執行 link(...) 時, linkingMainExecutable = true!!!
    1. recursiveLoadLibraries -> dyld_image_state_dependents_mapped 并通知
    2. recursiveRebase -> dyld_image_state_rebased 并通知
    (不會執行: 3. recursiveBindWithAccounting -> recursiveBind -> dyld_image_state_bound)
    (不會執行: 4. weakBind -> 通知 dyld_image_state_bound)
    */
    link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

    ...

    gLinkContext.linkingMainExecutable = false;

    // 從這里開始 linkingMainExecutable = false, 也就是 MainImageLoader完成link操作!!! 切換 MainImageLoader.fstate = dyld_image_state_bound, 并發送 dyld_image_state_bound_notify 通知
    // Bind and notify for the main executable now that interposing has been registered
    uint64_t bindMainExecutableStartTime = mach_absolute_time();
    sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
    uint64_t bindMainExecutableEndTime = mach_absolute_time();
    ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
    gLinkContext.notifyBatch(dyld_image_state_bound, false);

    ...

    /*
    這里開始執行各個dyld_image的initializers方法, 當初始化完成以后, 就MainImageLoader.fstate = dyld_image_state_initialized, 并發送 dyld_image_state_initialized_notify 通知
    1. initializeMainExecutable
    2. sMainExecutable->runInitializers
    3. sMainExecutable->processInitializers
    4. context.notifyBatch(dyld_image_state_initialized, false);
    */
    initializeMainExecutable(); 

    ...

    return Main函數的入口
}

在梳理之前, 我們需要有一個簡單的概念, 關于link(..)過程中的rebasebind.

mach-odyld_image二進制文件被加載到內存中以后, 由于地址空間加載隨機化(ASLR, Address Space Layout Randomization)的緣故, 二進制文件最終的加載地址與預期地址之間會存在偏移, 所以需要進行rebase操作, 對那些指向文件內部符號的指針進行修正, 在 link 函數中該項操作由 recursiveRebase 函數執行. rebase 完成之后, 就會進行 bind 操作, 修正那些指向其他二進制文件所包含的符號的指針, 由 recursiveBind函數執行。 當rebase以及bind結束時, link函數就完成了它的使命.

我們能看到在dyld::_main(...)函數中dyld_image會隨著過程切換自己的state狀態, 并且對外發出相關狀態的通知.

同時我們在源碼中有如下代碼:

// DEPRECATED -- 當 dyld_image 的state狀態變化以后, 調用的回調函數callback格式如下
typedef const char* (*dyld_image_state_change_handler)(enum dyld_image_states state, uint32_t infoCount, const struct dyld_image_info info[]);

// 注冊的方法的 函數指針當  mapped/ init/ unmapped 狀態時, 分別調用的callback格式如下
typedef void (*_dyld_objc_notify_mapped)(unsigned count, const char* const paths[], const struct mach_header* const mh[]);
typedef void (*_dyld_objc_notify_init)(const char* path, const struct mach_header* mh);
typedef void (*_dyld_objc_notify_unmapped)(const char* path, const struct mach_header* mh);

// 
// Note: only for use by objc runtime 
// 這個方法只有在 runtime 的 _objc_init 方法中調用
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded.  

// 1. During the call to _dyld_objc_notify_register(), dyld will call the "mapped" function with already loaded objc images.  
// 2. During any later dlopen() call, dyld will also call the "mapped" function.  (每一次調用dlopen(), 都會調用'mapped' function)
// 3. Dyld will call the "init" function when dyld would be called initializers in that image.  This is when objc calls any +load methods in that image. - 當 image狀態變化成 initializer 時候, 會調用`init` callback, 這個callback在實際代碼中是調用的 objc4.750 的 `loadImages` 方法, 這個方法內部會調用這個`image`中的每個`Class`的`+load`方法

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped) {
    dyld::registerObjCNotifiers(mapped, init, unmapped);
}

注意上面這個方法_dyld_objc_notify_register是在dyld::_main方法中的mainImageLoaderlink(...)方法結束以后, 由于依賴的庫中有libSystemlibCloure從而加載runtime_objc_init(...)方法結束時候才調用, 因此在執行_dyld_objc_notify_register以后, 相當于runtime就會監聽所有在runtime之后被加載的dyld_image, 根據他們的的狀態, 去調用注冊的3個回調函數, 這里我們重點關注load_images方法.

load_imagesruntime_objc_init(...)被注冊以后,一旦dyld中有新的image狀態成為init(也就是dyld_image_state_initialized, 此時表示該image已經完成link), 就會調用load_images方法, 對這個完全初始化成功的image中的內容做一些處理.

objc中的load_images

load_images方法的源碼如下:

/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock

 有新的鏡像image被加載到 runtime 時,調用 load_images 方法,并傳入最新鏡像image的信息列表 infoList:

  images 是鏡像的意思: 這里就會遇到一個問題:鏡像到底是什么,我們用一個斷點打印出所有加載的鏡, 從控制臺輸出的結果大概就是這樣的,我們可以看到鏡像并不是一個 Objective-C 的代碼文件,它應該是一個 target 的編譯產物。這里面有很多的動態鏈接庫,還有一些蘋果為我們提供的框架,比如 Foundation、 CoreServices 等等,都是在這個 load_images 中加載進來的,而這些 imageFilePath 都是對應的二進制文件的地址。

 +load 的應用:

 +load 可以說我們在日常開發中可以接觸到的調用時間最靠前的方法,在主函數運行之前,load 方法就會調用。

 由于它的調用不是惰性的,且其只會在程序調用期間調用一次,最最重要的是,如果在類與分類中都實現了 load 方法,它們都會被調用,不像其它的在分類中實現的方法會被覆蓋,這就使 load 方法成為了方法調劑的絕佳時機。

 但是由于 load 方法的運行時間過早,所以這里可能不是一個理想的環境,因為某些類可能需要在在其它類之前加載,但是這是我們無法保證的。不過在這個時間點,所有的 framework 都已經加載到了運行時中,所以調用 framework 中的方法都是安全的。
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);

void load_images(const char *path __unused, const struct mach_header *mh) {
    // Return without taking locks if there are no +load methods here.
    // 如果 沒有 +load 方法, 直接返回
    if (!hasLoadMethods((const headerType *)mh)) return;

    // 此時表示 mh中有 +load 方法

    // 上鎖, 不能同時多個線程執行 loadMethod, 鎖1
    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        // runtimeLock 兩個鎖, 鎖2
        // 這里 write-locks 需要兩個鎖
        mutex_locker_t lock2(runtimeLock);
        //調用 prepare_load_methods 對 load 方法的調用進行準備, 主要工作就是將Class的所有方法都加載到一個叫loadable_classes的數組中
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    // 在將鏡像加載到運行時, 對 load 方法的準備就緒之后,執行 call_load_methods,開始調用 load 方法
    call_load_methods();
}

load_images中的源碼游走以后, 我們主要看到兩個重要的步驟 -- 準備load和調用load:

  1. 當有新的鏡像被dyld加載, runtime就會去該鏡像中對所有的 class/category 進行準備操作.
  2. 準備操作是prepare_load_methods
  3. 調用操作是call_load_methods

objc中如何準備 -- prepare_load_methods解析

/**
 準備load methods
 */
void prepare_load_methods(const headerType *mhdr) {
    size_t count, i;

    // 調用 load_method 時, 必須是 runtimeLock已經上鎖
    runtimeLock.assertLocked();

    //處理mach-o中的class:
    // 通過 _getObjc2NonlazyClassList 獲取二進制文件中所有的類的列表之后,會通過 remapClass 獲取類對應的指針,然后調用 schedule_class_load 遞歸地安排當前類的父類和當前類加入到一個 loadable_list中
    classref_t *classlist =
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 內部處理以后調用 add_class_to_loadable_list方法
        schedule_class_load(remapClass(classlist[i]));
    }

    //處理mach-o中的categorys:
    // 通過 _getObjc2NonlazyCategoryList 方法獲取二進制文件中所有的category的列表, 然后遞歸處理每個單獨的category.
    // 單獨處理Category的過程如下: 首先獲取每個category的Class, 然后先調用一個關鍵的方法`realizeClass`, 這個方法能夠保證每個類已經被runtime進行了`realize`過, 這個過程很重要(后面有專門的文章來解釋整個realize的過程), 然后調用`add_category_to_loadable_list`將category方法加入loadable_list
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        // realizeClass 做的工作就是Class第一次 initiail
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        // 獲取 category中的+load方法, 然后按照一定順序將+load方法加入到一個loadabel_list中
        add_category_to_loadable_list(cat);
    }
}

通過源碼注釋, 我們可以看出準備過程會處理兩塊內容, 分別是鏡像中的class以及category.

如果處理鏡像中的class, 過程是:

  1. _getObjc2NonlazyClassList獲取二進制文件中所有的類, 放到一個鏈表中, 然后遞歸處理每個class
  2. 遍歷這個鏈表, 取出每個節點, 先調用remapClass, 然后調用schedule_class_load
  3. schedule_class_load主要是將入參的class的繼承鏈的每個+load方法都加入到loadable_classes鏈表中. 注意這里的添加+load方法到鏈表的順序是, 先父類, 然后自己.

如果處理鏡像中的category, 過程有點不一樣:

  1. _getObjc2NonlazyCategoryList方法獲取二進制中所有的category, 放到一個鏈表中, 然后遞歸處理每個category
  2. 單獨category的過程是: 首先獲取每個category對應的class, 先對class進行remapClass,調用一個關鍵的方法realizeClass(我們可以認為這個方法是Class類對象在內存中的初始化創建方法),最后調用add_category_to_loadable_list方法
  3. add_category_to_loadable_list是將category按照一定順序將+load方法加入到一個叫做loadable_categories鏈表中.

realizeClass方法我們后面專門分析, 這里我們只簡單了解一下. 我們直到Class在編譯期間, 有很多方法是我們自己在代碼里面定義的, 這些方法在編譯器編譯期間就搞定了, 當它加載到內存時候, 它的方法列表里面都是編譯期間確定的方法, 我們稱為只讀方法, 但是還有一些方法例如在category中的方法, 也是與Class有關的, 但是并沒有與這個Class關聯, 通過realizeClass來調整類在內存中的結構, 例如將category中的方法都關聯到class上去, 添加到class的方法列表中, 當然, 還有一些其他的作用, 后面再講.

objc中正式調用每個類的+load方法 -- call_load_methods解析

void call_load_methods(void) {
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads(); // 這里會調用  load 方法
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

static void call_class_loads(void) {
    int i;
    
    // Detach current loadable list.
    // 用一個便利結構體, 內部持有Class對應方法的+load IMP
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    // 遍歷所有的 loadable_classes 中的每個 loadable Class, 從中按照順序取出+load方法, 按照 loadable_list 的順序執行!!!
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        // 某個類的 +load 方法會執行!!!
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

簡單來說, 就是將loadable_classes中之前存儲的class+load調用, 然后清理, 最后調用loadable_categories緩存的category相關的+load方法.

小總結

  1. +load方法是如何被調用的?

runtime在初始化時, 會注冊一個回調, 去監聽鏡像加載, 每次有新的鏡像加載時, 就會調用注冊的load_images回調, 這個方法會將鏡像中所有類和分類的+load方法按照一定順序, 放到loadable_classesloadable_categories兩個鏈表中, 然后遍歷執行兩個鏈表中的每個節點的+load方法.

  1. +load方法的調用順序如何?
    1. 父類的+load會先調用, 然后才調用子類+load
    2. 類的+load會先調用, 然后調用分類的+load
    3. 總得來說, 會先調用super類的+load方法, 然后調用自身的+load方法, 最后調用分類重寫的+load方法.

并且結合前面文章我們知道: +load方法會先于app的啟動方法main執行, 并且它在全局只會調用一次等特性, +load方法是讓我們實現的method swizzling最佳位置!!!!

就算分類重寫了+load方法, 通過上面分析, 仍然會按照父類, 本類, 分類的順序執行+load方法. 需要注意這點比較特殊, 與其他方法的執行不一樣!!!

參考

iOS程序啟動->dyld加載->runtime初始化(初識)
你真的了解load方法么?
http://www.cocoachina.com/ios/20170716/19876.html

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

推薦閱讀更多精彩內容