前言
關于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來調用的。
- 調用順序:
- super class先于sub class
- class先于category
- 編譯順序在前的先于在后的
編譯順序指的是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