我所理解的iOS runtime 和 dyld

前言

關于iOS的runtime和dyld,網上資料一搜一大把,不過很多都是復制來復制去的。本來是不打算再整理一下關于這方面的資料了,但是想了想,別人的始終還是別人的,不自己整理一下寫點什么,總感覺沒法很好的消化吸收,讓它們能保存到自己的腦子里。所以這里就簡單記錄一下自己在看了一些網上的資料和objc的源碼后的一點點理解吧。不可否認的是,還是得親自跟了源碼后,才能對一些結論認識的更加透徹。如果有表述錯誤的地方,歡迎指正。

dyld 動態鏈接庫 the dynamic link editor

dyld是一個操作系統級別的組件,負責iOS系統中每個App啟動時的運行時環境初始化以及加載動態庫到內存等一些列操作。

dyld主要干了哪些事情?

關于dyld的詳細內容,可以參考:dyld詳解
我這里只簡單搬了一點點過來,因為我覺得,大概知道點這些內容應該夠用了。

第一步:設置運行環境。
第二步:加載共享緩存。(dyld會把一些基礎的共用lib緩存起來,這樣就不需要每個app啟動時都加載一次了)
第三步:實例化主程序。
第四步:加載插入的動態庫。
第五步:鏈接主程序。
第六步:鏈接插入的動態庫。
第七步:執行弱符號綁定
第八步:執行初始化方法。
第九步:查找入口點并返回。

Mach-O 文件

Mach-object文件格式的縮寫,用于可執行文件、動態鏈接庫、目標代碼的文件格式。
每個iOS App或者動態庫中都會有一個Mach-O格式的文件。
Mach-O 文件可以被dyld加載到內存中。
參考:Objective-C runtime機制(前傳)——Mach-O格式

Mach-O文件結構

也是簡單搬一點點過來。

  • header: 對mach-o文件的概要說明,包括文件類型,支持的cpu類型,load command總數
  • load commands:指導dyld如何加載mach-o文件的data數據
  • data:mach-o的數據區,包含代碼和數據。包含若干個segment,每個segment又包含若干個section。這些segment會根據對應的load command被dyld加載到內存中。

_objc_init

_objc_init是runtime的入口函數,它是在libSystem中被調用,libSystem是若干個系統lib的集合,也是由dyld動態加載的。
關于這個調用順序,通過一個符號斷點就可以看出來。至于怎么加這個斷點,網上很多,這里就不再重復了。

_objc_init 方法的源碼如下:
參考objc4源碼: https://opensource.apple.com/tarballs/objc4/

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

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_objc_notify_register(&map_images, load_images, unmap_image);
}

其中通過_dyld_objc_notify_register注冊了3個回調方法。

這個方法在dyld中定義。參考dyld的源碼:https://opensource.apple.com/tarballs/dyld/

//
// Note: only for use by objc runtime
// 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.  During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images.  During any later dlopen() call,
// dyld will also call the "mapped" function.  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.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);
  • _dyld_objc_notify_mapped: objc images被加載到內存時的回調
  • _dyld_objc_notify_init: objc images被初始化時的回調
  • _dyld_objc_notify_unmapped: objc images被移除內存時的回調

dyld在加載每個image(可以理解為動態庫或可執行文件)的過程中,都會在對應的時候調用objc注冊的這幾個回調函數,來讓objc完成對runtime環境的構建。只有將runtime環境都準備好了,一個iOS app才有運行的環境。

我所關注的重點就在這幾個回調上。

_dyld_objc_notify_mapped回調

對應到objc中的是map_images方法。
map_images會調用到_read_images方法,這個方法里的內容比較多,主要是讀取Mach-O文件對應的objc的section,并根據內容初始化runtime的內存結構。
通過源碼中的注釋可以看出,其大概做了以下這些事:

  • Discover classes: 從__objc_classlist section中讀取class list,并將這些class信息保存到一個map中。在這個步驟中,會檢測class名稱是否重復,如果重復了會給個提示。
  • Fix up remapped classes: 從__objc_classrefs section中讀取class ref信息,并進行remap
  • Fix up @selector references: 從__objc_selrefs section中讀取selector ref信息
  • Fix up old objc_msgSend_fixup call sites: 從__objc_msgrefs section中讀取message ref信息,Repairs an old vtable dispatch call site
  • Discover protocols: 從__objc_protolist section中讀取protocol信息
  • Fix up @protocol references:從__objc_protorefs section中讀取protocol ref信息,并進行remap
  • Realize non-lazy classes (for +load methods and static instances):從__objc_nlclslist section中讀取non-lazy classes信息,并初始化objc_class數據結構,實現meta class, super class, 設置isa指針,加載method list、property list、protocol list,附加categories
  • Discover categories:從__objc_catlist section中讀取categories信息,如果category的target class已經實現了(在上一個步驟),會重新構建 class的method list。category中的method 會被添加到class的method list的前面

我們可以在Xcode的scheme中添加一個環境變量OBJC_PRINT_IMAGE_TIMES:YES就可以在控制臺中看到上面這些步驟每一步所花費的時間。

在最后一步Discover categories中,上面已經給了結論:category中的method 會被添加到class的method list的前面。這個結論是從何得來的呢?
看下面這段源碼:

    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;
        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        ...
    }

這里的重點是memmove和memcpy函數。

// 由src所指內存區域復制count個字節到dest所指內存區域
void    *memmove(void *__dst, const void *__src, size_t __len);
// 拷貝src所指的內存內容前n個字節到dst所指的內存地址上
void    *memcpy(void *__dst, const void *__src, size_t __n);

由此可見,objc先將class原有的method list向后移了addedCount位,然后將addedList添加到了前面空出的位置上。addedList就是在categories中定義的方法。
所以,結論就是:category中定義的方法會被添加method list的前面,從而造成一種category中的方法會“覆蓋”class原有的方法的假象。“覆蓋”是假的,但是目的卻真實的達到了

_dyld_objc_notify_init回調

對應到objc中的是load_images方法。

/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

可以看到,+ load方法正是在這里被調用的。

prepare_load_methods做了2件事情:

  • 先遍歷所有non lazy classes,依次調用schedule_class_load方法,檢測每個class是否存在+load 方法,如果存在,將class和對應的IMP添加到loadable_classes這樣一個list里等待調用。
/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed 
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    // 如果已經處理過,則不再處理,保證每個在class中定義的+load方法只被添加一次
    if (cls->data()->flags & RW_LOADED) return; 
    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); //標識已經處理過
}

從這段代碼可以看到,在檢測每個class是否存在+load方法時,會優先檢測class的superclass是否存在+load方法,如果存在,會先將super class的+load 方法添加到list里,這樣就保證了super class的+load方法會先與class中的被調用。

/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    ...
    // 這里是對loadable_classes進行擴容
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    // 保存class和+load方法的IMP到list里
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}
  • 再遍歷所有categories,檢測每個category中是否存在+load方法,如果存在,將其添加到loadable_categories這樣一個list里等待調用
/***********************************************************************
* add_category_to_loadable_list
* Category cat's parent class exists and the category has been attached
* to its class. Schedule this category for +load after its parent class
* becomes connected and has its own +load method called.
**********************************************************************/
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;
    ...
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

對比這2段代碼可以看到,保存class的+load list和category的+load list是不一樣的,并且這2個list都是以數組的形式存儲,所以當一個class的多個category中都實現了+load方法時,這些+load方法都會被依次調用,調用順序同category的編譯順序是一樣的。

call_load_methods: 依次調用loadable_classes和loadable_categories中保存的class的+load方法。從這里可以看到,class本身的+load方法會先于category的+load方法調用。等所有classes的+load方法都調用完了,才會去調用category中定義的+load方法。

_dyld_objc_notify_unmapped 回調

對應到objc中是unmap_image方法。主要是做一些移除操作。

關于+load方法的幾點總結

  • +load方法是在main函數之前被調用的,當它被調用時,其他class可能還沒加載完,運行時環境并不完整。
  • +load方法是線程安全的,它里面用了鎖。
  • +load方法的調用機制是通過方法地址直接調用,在prepare_load_methods中保存了每個class對應的+load方法的方法地址。這個方式是區別于+initialize的,+initialize是通過objc_msgSend的消息機制調用的。
  • 每個class、category的+load方法都會被調用,且只會調用一次。
  • 同一個class如果在多個category中都定義了+load方法,那這些+load方法都會被調用。
  • 在+load方法中無需也不應該調用super load,因為每個+load方法都是由runtime來調用的。
  • 調用順序:
    1. super class先于sub class
    2. class先于category
    3. 編譯順序在前的先于在后的

編譯順序指的是Compile sources中出現的順序。

這里再補充幾點關于+initialize的總結吧,人們往往很喜歡把這2個進行對比。

關于 +initialize 的幾點總結

  • +initialize方法發生在runtime第一次向一個class通過objc_msgSend發送消息時,如果某個class從未被用到過,那它的+initialize就不會被調用。這跟+load方法不同,+load方法由runtime來調用,不管class有沒有被使用,都會調用。
  • +initialize方法的調用機制是objc_msgSend,這跟+load有本質區別,+load方法是runtime直接查找到方法實現的內存地址,直接通過內存地址調用。
  • 如果父類和子類都實現了+initialize方法,則父類的+initialize方法會先于子類的調用
  • 如果子類未實現+initialize方法,而父類實現了,子類會把父類的+initialize方法繼承過來調用一次,而在這之前父類的+initialize已經被調用了一次,所以父類的+initialize方法會被調用2次。
  • 如果category和class中都實現了+initialize方法,則runtime會調用category中的+initialize方法,而不會調用class中的。這是因為category中的方法會被添加到class的method list的前面。
  • 如果有多個category中都實現了+initialize方法,則處在編譯順序最后面的那個category的+initialize方法會被調用。

總結

在_objc_init中,runtime通過_dyld_objc_notify_register方法注冊了3個回調,當某個image(二進制文件)被dyld加載到內存中的過程中,就會執行對應的回調方法,因此這3個回調可能會被調用多次。
等到dyld完成了所有的runtime環境準備后,就會調用main函數,讓app啟動起來。

參考

https://www.dllhook.com/post/238.html
https://blog.csdn.net/u013378438/article/details/80353267
https://blog.csdn.net/u013378438/article/details/86614815

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374