您好,登錄后才能下訂單哦!
這篇文章主要介紹“iOS原理分析之怎么從源碼看load與initialize方法”,在日常操作中,相信很多人在iOS原理分析之怎么從源碼看load與initialize方法問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”iOS原理分析之怎么從源碼看load與initialize方法”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
在iOS開發中,NSObject類是萬事萬物的基類,其在Objective-C的整理類架構中非常重要,其中有兩個很有名的方法:load方法與initialize方法。
+ (void)load; + (void)initialize;
說起這兩個方法,你的第一反應一定是覺得太老套了,這兩個方法的調用時機及作用幾乎成為了iOS面試的必考題。其本身調用時機也非常簡單:
1. load方法在pre-main階段被調用,每個類都會調用且只會調用一次。
2. initialize方法在類或子類第一次進行方法調用前會調用。
上面的兩點說明本身是正確的,但是除此之外,還有許多問題值得我們深究,例如:
1. 子類與父類的load方法的調用順序是怎樣的?
2. 類與分類的load方法調用順序是怎樣的?
3. 子類未實現load方法,會調用父類的么?
4. 當有多個分類都實現了load方法時,會怎么樣?
5. 每個類的load方法的調用順序是怎樣的?
6. 父類與子類的initialize的方法調用順序是怎樣的?
7. 子類實現initialize方法后,還會調用父類的initialize方法么?
8. 多個分類都實現了initialize方法后,會怎么樣?
9. ...
如上所提到的問題,你現在都能給出明確的答案么?其實,load與initialize方法本身還有許多非常有意思的特點,本篇博客,我們將結合Objective-C源碼,對這兩個方法的實現原理做深入的分析,相信,如果你對load與initialize還不夠了解,不能完全明白上面所提出的問題,那么本篇博客將會使其收獲滿滿。無論在以后的面試中,還是工作中使用到load和initialize方法時,都可能幫助你從源碼上理解其執行原理。
在開始分析之前,我們首先可以先創建一個測試工程,對load方法的執行時機先做一個簡單的測試。首先,我們創建一個Xcode的命令行程序工程,在其中創建一些類、子類和分類,方便我們測試,目錄結構如下圖所示:
其中,MyObjectOne和MyObjectTwo都是繼承自NSObject的類,MySubObjectOne是MyObjectOne的子類,MySubObjectTwo是MyObjectTwo的子類,同時我們還創建了3個分類,在類中實現load方法,并做打印處理,如下:
+ (void)load { NSLog(@"load:%@", [self className]); }
同樣,類似的也在分類中做實現:
+ (void)load { NSLog(@"load-category:%@", [self className]); }
最后我們在main函數中添加一個Log:
int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"Main"); } return 0; }
運行工程,打印結果如下:
2021-02-18 14:33:46.773294+0800 KCObjc[21400:23090040] load:MyObjectOne 2021-02-18 14:33:46.773867+0800 KCObjc[21400:23090040] load:MySubObjectOne 2021-02-18 14:33:46.773959+0800 KCObjc[21400:23090040] load:MyObjectTwo 2021-02-18 14:33:46.774008+0800 KCObjc[21400:23090040] load:MySubObjectTwo 2021-02-18 14:33:46.774052+0800 KCObjc[21400:23090040] load-category:MyObjectTwo 2021-02-18 14:33:46.774090+0800 KCObjc[21400:23090040] load-category:MyObjectOne 2021-02-18 14:33:46.774127+0800 KCObjc[21400:23090040] load-category:MyObjectOne 2021-02-18 14:33:46.774231+0800 KCObjc[21400:23090040] Main
從打印結果可以看出,load方法在main方法開始之前被調用,執行順序上來說,先調用類的load方法,再調用分類的load方法,從父子類的關系上看來,先調用父類的load方法,再調用子類的load方法。
下面,我們就從源碼上來分析下,系統如此調用load方法,是源自于什么樣的奧妙。
要深入的研究load方法,我們首先需要從Objective-C的初始化函數說起:
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(); runtime_init(); exception_init(); cache_init(); _imp_implementationWithBlock_init(); // 其他的我們都不需要關注,只需要關注這行代碼 _dyld_objc_notify_register(&map_images, load_images, unmap_image); #if __OBJC2__ didCallDyldNotifyRegister = true; #endif }
_objc_init函數定義在objc-os.mm文件中,這個函數用來做Objective-C程序的初始化,由引導程序進行調用,其調用實際會非常的早,并且是操作系統引導程序復雜調用驅動,對開發者無感。在_objc_init函數中,會進行環境的初始化,runtime的初始化以及緩存的初始化等等操作,其中很重要的一步操作是執行_dyld_objc_notify_register函數,這個函數會調用load_images函數來進行鏡像的加載。
load方法的調用,其實就是類加載過程中的一步,首先,我們先來看一個load_images函數的實現:
void load_images(const char *path __unused, const struct mach_header *mh) { if (!didInitialAttachCategories && didCallDyldNotifyRegister) { didInitialAttachCategories = true; loadAllCategories(); } // 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方法調用相關的核心如下:
void load_images(const char *path __unused, const struct mach_header *mh) { // 鏡像中沒有load方法,直接返回 if (!hasLoadMethods((const headerType *)mh)) return; { // 準備load方法 prepare_load_methods((const headerType *)mh); } // 進行load方法的調用 call_load_methods(); }
最核心的部分在于load方法的準備與laod方法的調用,我們一步一步看,先來看load方法的準備(我們去掉了無關緊要的部分):
void prepare_load_methods(const headerType *mhdr) { size_t count, i; // 獲取所有類 組成列表 classref_t const *classlist = _getObjc2NonlazyClassList(mhdr, &count); for (i = 0; i < count; i++) { // 將所有類的load方法進行整理 schedule_class_load(remapClass(classlist[i])); } // 獲取所有的分類 組成列表 category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count); for (i = 0; i < count; i++) { category_t *cat = categorylist[i]; // 將分類的load方法進行整理 add_category_to_loadable_list(cat); } }
看到這里,我們基本就有頭緒了,load方法的調用順序,基本可以確定是由整理過程所決定的,并且我們可以發現,類的load方法整理與分類的load方法整理是互相獨立的,因此也可以推斷其調用的時機也是獨立的。首先我們先來看類的load方法整理函數schedule_class_load(去掉無關代碼后):
static void schedule_class_load(Class cls) { // 類不存在或者已經加載過load,則return if (!cls) return; if (cls->data()->flags & RW_LOADED) return; // 保證加載順序,遞歸進行父類加載 schedule_class_load(cls->superclass); // 將當前類的load方法加載進load方法列表中 add_class_to_loadable_list(cls); // 將當前類設置為已經加載過laod cls->setInfo(RW_LOADED); }
可以看到,schedule_class_load函數中使用了遞歸的方式演著繼承鏈逐層向上,保證在加載load方法時,先加載父類,再加載子類。add_class_to_loadable_list是核心的load方法整理函數,如下(去掉了無關代碼):
void add_class_to_loadable_list(Class cls) { IMP method; // 讀取類中的load方法 method = cls->getLoadMethod(); if (!method) return; // 類中沒有實現load方法,直接返回 // 構建存儲列表及擴容邏輯 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)); } // 向列表中添加 loadable_class 結構體,這個結構體中存儲了類與對應的laod方法 loadable_classes[loadable_classes_used].cls = cls; loadable_classes[loadable_classes_used].method = method; // 標記列表index的指針移動 loadable_classes_used++; }
loadable_clas結構體的定義如下:
struct loadable_class { Class cls; // may be nil IMP method; };
getLoadMetho函數的實現主要是從類中獲取到load方法的實現,如下:
IMP objc_class::getLoadMethod() { // 獲取方法列表 const method_list_t *mlist; mlist = ISA()->data()->ro()->baseMethods(); if (mlist) { // 遍歷,找到load方法返回 for (const auto& meth : *mlist) { const char *name = sel_cname(meth.name); if (0 == strcmp(name, "load")) { return meth.imp; } } } return nil; }
現在,關于類的load方法的準備邏輯已經非常清晰了,最終會按照先父類后子類的順序將所有類的load方法添加進名為loadable_classes的列表中,loadable_classes這個名字你要注意一下,后面我們還會遇到它。
我們再來看分類的laod方法準備過程,其與我們上面介紹的類非常相似,add_category_to_loadable_list函數簡化后如下:
void add_category_to_loadable_list(Category cat) { IMP method; // 獲取當前分類的load方法 method = _category_getLoadMethod(cat); 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)); } // 將分類與load方法進行存儲 loadable_categories[loadable_categories_used].cat = cat; loadable_categories[loadable_categories_used].method = method; loadable_categories_used++; }
可以看到,最終分類的load方法是存儲在了loadable_categories列表中。
準備好了load方法,我們再來分析下load方法的執行過程,call_load_methods函數的核心實現如下:
void call_load_methods(void) { bool more_categories; do { // 先對 loadable_classes 進行遍歷,loadable_classes_used這個字段可以理解為列表的元素個數 while (loadable_classes_used > 0) { call_class_loads(); } // 再對類別進行遍歷調用 more_categories = call_category_loads(); } while (loadable_classes_used > 0 || more_categories); }
call_class_loads函數實現簡化后如下:
static void call_class_loads(void) { int i; // loadable_classes列表 struct loadable_class *classes = loadable_classes; // 需要執行load方法個數 int used = loadable_classes_used; // 清理數據 loadable_classes = nil; loadable_classes_allocated = 0; loadable_classes_used = 0; // 循環進行執行 循環的循序是從前到后 for (i = 0; i < used; i++) { // 獲取類 Class cls = classes[i].cls; // 獲取對應load方法 load_method_t load_method = (load_method_t)classes[i].method; if (!cls) continue; // 執行load方法 (*load_method)(cls, @selector(load)); } }
call_category_loads函數的實現要復雜一些,簡化后如下:
static bool call_category_loads(void) { int i, shift; bool new_categories_added = NO; // 獲取loadable_categories分類load方法列表 struct loadable_category *cats = loadable_categories; int used = loadable_categories_used; int allocated = loadable_categories_allocated; loadable_categories = nil; loadable_categories_allocated = 0; loadable_categories_used = 0; // 從前往后遍歷進行load方法的調用 for (i = 0; i < used; i++) { Category cat = cats[i].cat; load_method_t load_method = (load_method_t)cats[i].method; Class cls; if (!cat) continue; cls = _category_getClass(cat); if (cls && cls->isLoadable()) { (*load_method)(cls, @selector(load)); cats[i].cat = nil; } } return new_categories_added; }
現在,我相信你已經對load方法為何類先調用,分類后調用,并且為何父類先調用,子類后調用。但是還有一點,我們不甚明了,即類之間或分類之間的調用順序是怎么確定的,從源碼中可以看到,類列表是通過_getObjc2NonlazyClassList函數獲取的,同樣分類的列表是通過_getObjc2NonlazyCategoryList函數獲取的。這兩個函數獲取到的類或分類的順序實際上是與類源文件的編譯順序有關的,如下圖所示:
可以看到,打印的load方法的執行順序與源代碼的編譯順序是一直的。
我們可以采用和分析load方法時一樣的策略來對initialize方法的執行情況,進行測試,首先將測試工程中所有類中添加initialize方法的實現。此時如果直接運行工程,你會發現控制臺沒有任何輸出,這是由于只有第一次調用類的方法時,才會執行initialize方法,在main函數中編寫如下測試代碼:
int main(int argc, const char * argv[]) { @autoreleasepool { [MySubObjectOne new]; [MyObjectOne new]; [MyObjectTwo new]; NSLog(@"------------"); [MySubObjectOne new]; [MyObjectOne new]; [MyObjectTwo new]; } return 0; }
運行代碼控制臺打印效果如下:
2021-02-18 21:29:55.761897+0800 KCObjc[43834:23521232] initialize-cateOne:MyObjectOne 2021-02-18 21:29:55.762526+0800 KCObjc[43834:23521232] initialize:MySubObjectOne 2021-02-18 21:29:55.762622+0800 KCObjc[43834:23521232] initialize-cate:MyObjectTwo 2021-02-18 21:29:55.762665+0800 KCObjc[43834:23521232] ------------
可以看到,打印數據都出現在分割線前,說明一旦一個類的initialize方法被調用后,后續再向這個類發送消息,也不會在調用initialize方法,還有一點需要注意,需要注意,如果對子類發送消息,父類的initialize會先調用,再調用子類的initialize,同時,分類中如果實現了initialize方法則會覆蓋類本身的,并且分類的加載順序靠后的會覆蓋之前的。下面我們就通過源碼來分析下initialize方法的這種調用特點。
首先,在調用類的類方法時,會執行runtime中的class_getClassMethod方法來尋找實現函數,這個方法在源碼中的實現如下:
Method class_getClassMethod(Class cls, SEL sel) { if (!cls || !sel) return nil; return class_getInstanceMethod(cls->getMeta(), sel); }
通過源碼可以看到,調用一個類的類方法,實際上是調用其元類的示例方法,getMeta函數用來獲取類的元類,關于類和元類的相關組織原理,我們這里先不擴展。我們需要關注的是class_getInstanceMethod這個函數,這個函數的實現也非常簡單,如下:
Method class_getInstanceMethod(Class cls, SEL sel) { if (!cls || !sel) return nil; // 做查詢方法列表,嘗試方法解析相關工作 lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER); // 從類對象中獲取方法 return _class_getMethod(cls, sel); }
在class_getInstanceMethod方法的實現中,_class_getMethod是最終獲取要調用的方法的函數,在這之前,lookUpImpOrForward函數會做一些前置操作,其中就有initialize函數的調用邏輯,我們去掉無關的邏輯,lookUpImpOrForward中核心的實現如下:
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) { IMP imp = nil; // 核心在于!cls->isInitialized() 如果當前類未初始化過,會執行initializeAndLeaveLocked函數 if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) { cls = initializeAndLeaveLocked(cls, inst, runtimeLock); } return imp; }
initializeAndLeaveLocked會直接調用initializeAndMaybeRelock函數,如下:
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock) { return initializeAndMaybeRelock(cls, obj, lock, true); }
initializeAndMaybeRelock函數中會做類的初始化邏輯,這個過程是線程安全的,其核心相關代碼如下:
static Class initializeAndMaybeRelock(Class cls, id inst, mutex_t& lock, bool leaveLocked) { // 如果已經初始化過,直接返回 if (cls->isInitialized()) { return cls; } // 找到當前類的非元類 Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst); // 進行初始化操作 initializeNonMetaClass(nonmeta); return cls; }
initializeNonMetaClass函數會采用遞歸的方式沿著繼承鏈向上查詢,找到所有未初始化過的父類進行初始化,核心實現簡化如下:
void initializeNonMetaClass(Class cls) { Class supercls; // 標記是否需要初始化 bool reallyInitialize = NO; // 父類如果存在,并且沒有初始化過,則遞歸進行父類的初始化 supercls = cls->superclass; if (supercls && !supercls->isInitialized()) { initializeNonMetaClass(supercls); } SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs; { // 如果當前不是正在初始化,并且當前類沒有初始化過 if (!cls->isInitialized() && !cls->isInitializing()) { // 設置初始化標志,此類標記為初始化過 cls->setInitializing(); // 標記需要進行初始化 reallyInitialize = YES; } } // 是否需要進行初始化 if (reallyInitialize) { @try { // 調用初始化函數 callInitialize(cls); } @catch (...) { @throw; } return; } }
callInitialize函數最終會調用objc_msgSend函數來向類發送initialize初始化消息,如下:
void callInitialize(Class cls) { ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize)); asm(""); }
需要注意,initialize方法與load方法最大的區別在于其最終是通過objc_msgSend來實現的,每個類如果未初始化過,都會通過objc_msgSend來向類發送一次initialize消息,因此,如果子類沒有對initialize實現,按照objc_msgSend的消息機制,其是會沿著繼承鏈一路向上找到父類的實現進行調用的,所有initialize方法并不是只會被調用一次,假如父類中實現了這個方法,并且它有多個未實現此方法的子類,則當每個子類第一次接受消息時,都會調用一遍父類的initialize方法,這點非常重要,在實際開發中一定要牢記。
到此,關于“iOS原理分析之怎么從源碼看load與initialize方法”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。