结合 category 工作原理分析 OC2.0 中的 runtime
絕大多數(shù) iOS 開發(fā)者在學(xué)習(xí) runtime 時都閱讀過 runtime.h 文件中的這段代碼:
struct objc_class {Class isa OBJC_ISA_AVAILABILITY;Class super_class OBJC2_UNAVAILABLE;const char *name OBJC2_UNAVAILABLE;long version OBJC2_UNAVAILABLE;long info OBJC2_UNAVAILABLE;long instance_size OBJC2_UNAVAILABLE;struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;struct objc_method_list **methodLists OBJC2_UNAVAILABLE;struct objc_cache *cache OBJC2_UNAVAILABLE;struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; } OBJC2_UNAVAILABLE;復(fù)制代碼可以看到其中保存了類的實例變量,方法列表等信息。
不知道有多少讀者思考過 OBJC2_UNAVAILABLE 意味著什么。其實早在 2006 年,蘋果在 WWDC 大會上就發(fā)布了 Objective-C 2.0,其中的改動包括 Max OS X 平臺上的垃圾回收機(jī)制(現(xiàn)已廢棄),runtime 性能優(yōu)化等。
這意味著上述代碼,以及任何帶有 OBJC2_UNAVAILABLE 標(biāo)記的內(nèi)容,都已經(jīng)在 2006 年就永遠(yuǎn)的告別了我們,只停留在歷史的文檔中。
Category 的原理
雖然上述代碼已經(jīng)過時,但仍具備一定的參考意義,比如 methodLists 作為一個二級指針,其中每個元素都是一個數(shù)組,數(shù)組中的每個元素則是一個方法。
接下來就介紹一下 category 的工作原理,在美團(tuán)的技術(shù)博客 深入理解Objective-C:Category 中已經(jīng)有了非常詳細(xì)的解釋,然而可能由于時間問題,其中的不少內(nèi)容已經(jīng)過時,我根據(jù)目前最新的版本(objc-680) 做一些簡單的分析,為了便于閱讀,在不影響代碼邏輯的前提下有可能刪除部分無關(guān)緊要的內(nèi)容。
概述
首先 runtime 依賴于 dyld 動態(tài)加載,在 objc-os.mm 文件中可以找到入口,它的調(diào)用棧簡單整理如下:
void _objc_init(void) └──const char *map_2_images(...)└──const char *map_images_nolock(...)└──void _read_images(header_info **hList, uint32_t hCount)復(fù)制代碼以上四個方法可以理解為 runtime 的初始化過程,我們暫且不深究。在 _read_images 方法中有如下代碼:
if (cat->classMethods || cat->protocols /* || cat->classProperties */) {addUnattachedCategoryForClass(cat, cls->ISA(), hi);if (cls->ISA()->isRealized()) {remethodizeClass(cls->ISA());} }復(fù)制代碼根據(jù)注釋可見蘋果曾經(jīng)計劃利用 category 來添加屬性。在 addUnattachedCategoryForClass 方法中會找到當(dāng)前類的所有 category,然后在 remethodizeClass 真正的去做處理。不過到目前為止還沒有接觸到相關(guān)的 category 處理,我們繼續(xù)沿著調(diào)用棧向下走:
void _read_images(header_info **hList, uint32_t hCount) └──static void remethodizeClass(Class cls)└──static void attachCategories(Class cls, category_list *cats, bool flush_caches)復(fù)制代碼這里的 attachCategories 就是處理 category 的核心所在,不過在閱讀這段代碼之前,我們有必要先熟悉一下相關(guān)的數(shù)據(jù)結(jié)構(gòu)。
Category 相關(guān)的數(shù)據(jù)結(jié)構(gòu)
首先來了解一下一個 Category 是如何存儲的,在 objc-runtime-new.h 中可以看到如下定義,我只列出了其中成員變量:
struct category_t {const char *name;classref_t cls;struct method_list_t *instanceMethods;struct method_list_t *classMethods;struct protocol_list_t *protocols;struct property_list_t *instanceProperties; };復(fù)制代碼可見一個 category 持有了一個 method_list_t 類型的數(shù)組,method_list_t 又繼承自 entsize_list_tt,這是一種泛型容器:
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {// 成員變量和方法 };template <typename Element, typename List, uint32_t FlagMask> struct entsize_list_tt {uint32_t entsizeAndFlags;uint32_t count;Element first; };復(fù)制代碼這里的 entsize_list_tt 可以理解為一個容器,擁有自己的迭代器用于遍歷所有元素。 Element 表示元素類型,List 用于指定容器類型,最后一個參數(shù)為標(biāo)記位。
雖然這段代碼實現(xiàn)比較復(fù)雜,但仍可了解到 method_list_t 是一個存儲 method_t 類型元素的容器。method_t 結(jié)構(gòu)體的定義如下:
struct method_t {SEL name;const char *types;IMP imp; };復(fù)制代碼最后,我們還有一個結(jié)構(gòu)體 category_list 用來存儲所有的 category,它的定義如下:
struct locstamped_category_list_t {uint32_t count;locstamped_category_t list[0]; }; struct locstamped_category_t {category_t *cat;struct header_info *hi; }; typedef locstamped_category_list_t category_list;復(fù)制代碼除了標(biāo)記存儲的 category 的數(shù)量外,locstamped_category_list_t 結(jié)構(gòu)體還聲明了一個長度為零的數(shù)組,這其實是 C99 中的一種寫法,允許我們在運(yùn)行期動態(tài)的申請內(nèi)存。
以上就是相關(guān)的數(shù)據(jù)結(jié)構(gòu),只要了解到這個程度就可以繼續(xù)讀源碼了。
處理 Category
對 Category 中方法的解析并不復(fù)雜,首先來看一下 attachCategories 的簡化版代碼:
static void attachCategories(Class cls, category_list *cats, bool flush_caches) {if (!cats) return;bool isMeta = cls->isMetaClass();method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));// Count backwards through cats to get newest categories firstint mcount = 0;int i = cats->count;while (i--) {auto& entry = cats->list[i];method_list_t *mlist = entry.cat->methodsForMeta(isMeta);if (mlist) {mlists[mcount++] = mlist;}}auto rw = cls->data();prepareMethodLists(cls, mlists, mcount, NO, fromBundle);rw->methods.attachLists(mlists, mcount);free(mlists);if (flush_caches && mcount > 0) flushCaches(cls); }復(fù)制代碼首先,通過 while 循環(huán),我們遍歷所有的 category,也就是參數(shù) cats 中的 list 屬性。對于每一個 category,得到它的方法列表 mlist 并存入 mlists 中。
換句話說,我們將所有 category 中的方法拼接到了一個大的二維數(shù)組中,數(shù)組的每一個元素都是裝有一個 category 所有方法的容器。這句話比較繞,但你可以把 mlists 理解為文章開頭所說,舊版本的 objc_method_list **methodLists。
在 while 循環(huán)外,我們得到了拼接成的方法,此時需要與類原來的方法合并:
auto rw = cls->data(); rw->methods.attachLists(mlists, mcount);復(fù)制代碼這兩行代碼讀不懂是必然的,因為在 Objective-C 2.0 時代,對象的內(nèi)存布局已經(jīng)發(fā)生了一些變化。我們需要先了解對象的布局模型才能理解這段代碼。
Objective-C 2.0 對象布局模型
objc_class
相信讀到這里的大部分讀者都學(xué)習(xí)過文章開頭所說的對象布局模型,因此在這一部分,我們采用類比的方法,來看看 Objective-C 2.0 下發(fā)生了哪些改變。
首先,Class 和 id 指針的定義并沒有發(fā)生改變,他們一個指向類對應(yīng)的結(jié)構(gòu)體,一個指向?qū)ο髮?yīng)的結(jié)構(gòu)體:
// objc.h typedef struct objc_class *Class; typedef struct objc_object *id;復(fù)制代碼比較有意思的一點是,objc_class 結(jié)構(gòu)體是繼承自 objc_object 的:
struct objc_object {Class isa OBJC_ISA_AVAILABILITY; };struct objc_class : objc_object {Class superclass;cache_t cache; // formerly cache pointer and vtableclass_data_bits_t bits; // class_rw_t * plus custom rr/alloc flagsclass_rw_t *data() { return bits.data();} };復(fù)制代碼這一點也很容易理解,早在 Objective-C 1.0 時代,我們就知道一個對象的結(jié)構(gòu)體只有 isa 指針,指向它所屬的類。而類的結(jié)構(gòu)體也有 isa 指針指向它的元類。因此讓類結(jié)構(gòu)體繼承自對象結(jié)構(gòu)體就很容易理解了。
可見 Objective-C 1.0 的布局模型中,cache 和 super_class 被原封不動的移過來了,而剩下的屬性則似乎消失不見。取而代之的是一個 bits 屬性,以及 data() 方法,這個方法調(diào)用的其實是 bits 屬性的 data() 方法,并返回了一個 class_rw_t 類型的結(jié)構(gòu)體指針。
class_data_bits_t
以下是簡化版 class_data_bits_t 結(jié)構(gòu)體的定義:
struct class_data_bits_t {uintptr_t bits; public:class_rw_t* data() {return (class_rw_t *)(bits & FAST_DATA_MASK);} }復(fù)制代碼可見這個結(jié)構(gòu)體只有一個 64 位的 bits 成員,存儲了一個指向 class_rw_t 結(jié)構(gòu)體的指針和三個標(biāo)志位。它實際上由三部分組成。首先由于 Mac OS X 只使用 47 位內(nèi)存地址,所以前 17 位空余出來,提供給 retain/release 和 alloc/dealloc 方法使用,做一些優(yōu)化。其次,由于內(nèi)存對齊,指針地址的后三位都是 0,因此可以用來做標(biāo)志位:
// class is a Swift class 復(fù)制代碼如果計算一下就會發(fā)現(xiàn),FAST_DATA_MASK 這個 16 進(jìn)制常量的二進(jìn)制表示恰好后三位為0,且長度為47位: 11111111111111111111111111111111111111111111000,我們通過這個掩碼做按位與運(yùn)算即可取出正確的指針地址。
引用 Draveness 在 深入解析 ObjC 中方法的結(jié)構(gòu) 中的圖片做一個總結(jié):
class_rw_t
bits 中包含了一個指向 class_rw_t 結(jié)構(gòu)體的指針,它的定義如下:
struct class_rw_t {uint32_t flags;uint32_t version;const class_ro_t *ro;method_array_t methods;property_array_t properties;protocol_array_t protocols; }復(fù)制代碼注意到有一個名字很類似的結(jié)構(gòu)體 class_ro_t,這里的 'rw' 和 ro' 分別表示 'readwrite' 和 'readonly'。因為 class_ro_t 存儲了一些由編譯器生成的常量。
These are emitted by the compiler and are part of the ABI.
正是由于 class_ro_t 中的兩個屬性 instanceStart 和 instanceSize 的存在,保證了 Objective-C2.0 的 ABI 穩(wěn)定性。因為即使父類增加方法,子類也可以在運(yùn)行時重新計算 ivar 的偏移量,從而避免重新編譯。
關(guān)于 ABI 穩(wěn)定性的問題,本文不做贅述,讀者可以參考 Non Fragile ivars。
如果閱讀 class_ro_t 結(jié)構(gòu)體的定義就會發(fā)現(xiàn),舊版本實現(xiàn)中類結(jié)構(gòu)體中的大部分成員變量現(xiàn)在都定義在 class_ro_t 和 class_rw_t 這兩個結(jié)構(gòu)體中了。感興趣的讀者可以自行對比,本文不再贅述。
class_rw_t 結(jié)構(gòu)體中還有一個 methods 成員變量,它的類型是 method_array_t,繼承自 list_array_tt。
list_array_tt 是一個泛型結(jié)構(gòu)體,用于存儲一些元數(shù)據(jù),而它實際上是元數(shù)據(jù)的二維數(shù)組:
template <typename Element, typename List>{struct array_t {uint32_t count;List* lists[0];}; } class method_array_t : public list_array_tt<method_t, method_list_t>復(fù)制代碼其中 Element 表示元數(shù)據(jù)的類型,比如 method_t,而 List 則表示用于存儲元數(shù)據(jù)的一維數(shù)組,比如 method_list_t。
list_array_tt 有三種狀態(tài):
當(dāng)一個類剛創(chuàng)建時,它可能處于狀態(tài) 1 或 2,但如果使用 class_addMethod 或者 category 來添加方法,就會進(jìn)入狀態(tài) 3,而且一旦進(jìn)入狀態(tài) 3 就再也不可能回到其他狀態(tài),即使新增的方法后來又被移除掉。
方法合并
掌握了這些 runtime 的基礎(chǔ)只是以后就可以繼續(xù)鉆研剩下的 category 的代碼了:
auto rw = cls->data(); rw->methods.attachLists(mlists, mcount);復(fù)制代碼這是剛剛卡住的地方,現(xiàn)在來看,rw 是一個 class_rw_t 類型的結(jié)構(gòu)體指針。根據(jù) runtime 中的數(shù)據(jù)結(jié)構(gòu),它有一個 methods 結(jié)構(gòu)體成員,并從父類繼承了 attachLists 方法,用來合并 category 中的方法:
void attachLists(List* const * addedLists, uint32_t addedCount) {if (addedCount == 0) return;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])); }復(fù)制代碼這段代碼很簡單,其實就是先調(diào)用 realloc() 函數(shù)將原來的空間拓展,然后把原來的數(shù)組復(fù)制到后面,最后再把新數(shù)組復(fù)制到前面。
在實際代碼中,比上面略復(fù)雜一些。因為為了提高性能,蘋果做了一些優(yōu)化,比如當(dāng) List 處于第二種狀態(tài)(只有一個指針,指向一個元數(shù)據(jù)的集合)時,其實并不需要在原地擴(kuò)容空間,而是只要重新申請一塊內(nèi)存,并將最后一個位置留給原來的集合即可。
這樣只多花費(fèi)了很少的內(nèi)存空間,也就是原來二維數(shù)組占用的內(nèi)存空間,但是 malloc() 的性能優(yōu)勢會更加明顯,這其實是一個空間換時間的權(quán)衡問題。
需要注意的是,無論執(zhí)行哪種邏輯,參數(shù)列表中的方法都會被添加到二維數(shù)組的前面。而我們簡單的看一下 runtime 在查找方法時的邏輯:
static method_t *getMethodNoSuper_nolock(Class cls, SEL sel){for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end;++mlists) {method_t *m = search_method_list(*mlists, sel);if (m) return m;}return nil; }static method_t *search_method_list(const method_list_t *mlist, SEL sel) {for (auto& meth : *mlist) {if (meth.name == sel) return &meth;} }復(fù)制代碼可見搜索的過程是按照從前向后的順序進(jìn)行的,一旦找到了就會停止循環(huán)。因此 category 中定義的同名方法不會替換類中原有的方法,但是對原方法的調(diào)用實際上會調(diào)用 category 中的方法。
總結(jié)
讀完本文后,你應(yīng)該對以下內(nèi)容有比較深刻的理解,排名不分先后:
參考資料
總結(jié)
以上是生活随笔為你收集整理的结合 category 工作原理分析 OC2.0 中的 runtime的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 界面位置_java界面中怎样设
- 下一篇: 【Foreign】采蘑菇 [点分治]