窺探iOS底層實現--OC物件的分類:instance、class、meta-calss物件的isa和superclass
iOS底層原理總結 -- 利用Runtime原始碼 分析Category的底層實現 ...
前言:
本文總結了一下Category中的內部去實現部分,程式碼部分較多,新增了註釋,閱讀起來可能比較枯燥。但是請大家務必堅持讀完。會有更多的收貨。
本文中涉及到Class類物件的內部結構的知識,請參考文章 窺探iOS底層實現--OC物件的分類:instance、class、meta-calss物件的isa和superclass
思考:
- Category的實現原理?
- 為什麼Category的中的方法會優先呼叫?
- 延伸問題 - 如果多個分類中都實現了同一個方法,那麼在呼叫該方法的時候會優先呼叫哪一個方法?
- 擴充套件和分類的區別?
Category 基本實現
首先 看一下分類程式碼程式碼的實現 可選擇性跳過
///> main.h
int main(int argc, const char *argv[]){
@autoreleasepool{
Person *person = [[Person alloc] init]
[person run];
[person test];
[person eat];
}
return 0
}
///> person
@interface Person: NSObject
@end
@implementation Person
- (void)run{
Nslog(@"run")
}
@end
///> person+test
@interface Person(test)
- (void)test;
@end
@implementation Person(test)
- (void)test{
Nslog(@"test")
}
@end
///> person+Eat
@interface Person(eat)
- (void)eat;
@end
@implementation Person(eat)
- (void)eat{
Nslog(@"eat")
}
@end
複製程式碼
分類的底層結構體 編譯完畢之後
編譯完畢的時候 一開始程式執行的時候 所有分類的方法 一開始都存放在 結構體中(每一個分類都有一個新的結構體物件),
編譯完畢之後 category存放在 結構體category_t中 並沒有合併到 原始類中 每一個分類都會生成catrgory_t的結構體, 在執行時的時候才會將分類中的方法、協議、屬性等 合併到原始的類中去。
下面是原始碼觀看的過程在每一步都給出了註釋, 有點枯燥,但是看完之後會很受益。
分類程式碼 C\C++原始碼分析
利用:
xcrun -sdk iphoneos clang -arch arm64 - OC原始檔 -o 輸出的CPP檔案
複製程式碼
命令可以檢視轉化為C\C++程式碼。會有生成一個xxx.cpp的檔案就是我們想要的檔案
可以將其拖入到xcode中, 方便搜尋
接下來直接搜尋 category_t 得出如下結構體 我已經將註釋放在後面了
struct _category_t{
const char *name; ///> 分類的名字
struct _class_t *cls; ///> class
const struct _method_list_t *instance_methods; ///> 例項方法列表
const struct _method_list_t *class_methods; ///> 類方法列表
const struct _protocol_list_t *protocols; ///> 協議
const struct _prop_list_t *properties; ///> 屬性
}
複製程式碼
每建立一個類都會 根會根據如下方法建立一個category_t的結構體
static struct _category_t OBJC_$CATEGORY_PERSON_$_Test __attribute__ ((userd, section("__DATA,__objc_const")))={
///> 屬於那個類的分類
"Person",
///> class
0,
///> 物件方法列表
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$Test,
///> 類方法列表
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$Test,
///> 協議列表
0, // (const _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_PERSON_$_Test,
///> 屬性列表
0, // (const _prop_list_t *)&_OBJC_$_PROP_LIST_PERSON_$_Test,
}
複製程式碼
接下來檢視一下 Runtime的原始碼是怎麼將分類合併的,
Runtime原始碼分析
首先下載Runtimed的原始碼。 ------ 這裡用xcode開啟
-
搜尋 "catrgory_t {"
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; // Fields below this point are not always present on disk. struct property_list_t *_classProperties; method_list_t *methodsForMeta(bool isMeta) { if (isMeta) return classMethods; else return instanceMethods; } property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi); }; 複製程式碼
可以看到 Runtime中的結構和上面的category_t的結構類似。
-
Runtime的程式入口檔案為objc-os.mm 檔案,
-
我這裡直接到 有關Category的程式碼部分 在objc-runtime-new.mm檔案中 搜搜Discover categories. 的註釋程式碼
// Discover categories. for (EACH_HEADER) { /** catlist 是一個二維陣列, 每一個分類都會建立一個category_t的結構體 這裡的二維陣列放了兩個分類結構體的內容 如程式碼中的 eat和test catlist = [[],[]] */ category_t **catlist = _getObjc2CategoryList(hi, &count); bool hasClassProperties = hi->info()->hasCategoryClassProperties(); ///> 將每一個陣列中的內容遍歷 for (i = 0; i < count; i++) { ///> 獲取 單獨的category_t結構體 category_t *cat = catlist[i]; ///> 重新對映class 取出結構體的class Class cls = remapClass(cat->cls); if (!cls) { // Category's target class is missing (probably weak-linked). // Disavow any knowledge of this category. catlist[i] = nil; if (PrintConnecting) { _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with " "missing weak-linked target class", cat->name, cat); } continue; } // Process this category. // First, register the category with its target class. // Then, rebuild the class's method lists (etc) if // the class is realized. bool classExists = NO; /// 判斷結構體的內容 if (cat->instanceMethods || cat->protocols || cat->instanceProperties) { addUnattachedCategoryForClass(cat, cls, hi); if (cls->isRealized()) { /// 核心內容 : 重新組織類中的方法 remethodizeClass(cls); classExists = YES; } if (PrintConnecting) { _objc_inform("CLASS: found category -%s(%s) %s", cls->nameForLogging(), cat->name, classExists ? "on existing class" : ""); } } if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) { addUnattachedCategoryForClass(cat, cls->ISA(), hi); if (cls->ISA()->isRealized()) { /// 核心內容 : 重新組織類中的元類方法 remethodizeClass(cls->ISA()); } if (PrintConnecting) { _objc_inform("CLASS: found category +%s(%s)", cls->nameForLogging(), cat->name); } } } } 複製程式碼
以上程式碼中找到了 核心的方法: remethodizeClass 使用了兩次 , 重新組織類的方法和元類的方法
-
command+單機,進入
static void remethodizeClass(Class cls) { category_list *cats; bool isMeta; runtimeLock.assertWriting(); isMeta = cls->isMetaClass(); // Re-methodizing: check for more categories if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { if (PrintConnecting) { _objc_inform("CLASS: attaching categories to class '%s' %s", cls->nameForLogging(), isMeta ? "(meta)" : ""); } ///> 附加分類的程式碼呼叫 , 傳入了 類物件、分類。 ///> cls: [Person class] ///> cats: [category_t(test), category_t(eat)] attachCategories(cls, cats, true /*flush caches*/); free(cats); } } 複製程式碼
-
command 進入 attachCategories(cls, cats, true /flush caches/); 方法
///> cls: [Person class] ///> cats: [category_t(test), category_t(eat)] static void attachCategories(Class cls, category_list *cats, bool flush_caches){ if (!cats) return; if (PrintReplacedMethods) printReplacements(cls, cats); ///> 是否是元類物件 bool isMeta = cls->isMetaClass(); // fixme rearrange to remove these intermediate allocations ///> malloc 分配記憶體 ///> 方法陣列 二維陣列 eg:[[method_t,method_t], [method_t,method_t]] method_list_t **mlists = (method_list_t **) malloc(cats->count * sizeof(*mlists)); ///> 屬性陣列 eg:[[property_t,property_t], [property_t,property_t]] property_list_t **proplists = (property_list_t **) malloc(cats->count * sizeof(*proplists)); ///> 協議陣列 eg:[[protocol_t,protocol_t], [protocol_t,protocol_t]] protocol_list_t **protolists = (protocol_list_t **) malloc(cats->count * sizeof(*protolists)); // Count backwards through cats to get newest categories first int mcount = 0; int propcount = 0; int protocount = 0; int i = cats->count; bool fromBundle = NO; while (i--) { ///> 取出某個分類 auto& entry = cats->list[i]; ///> 取出分類中的物件方法 method_list_t *mlist = entry.cat->methodsForMeta(isMeta); ///> 將每一個分類的方法列表陣列放在 上方定義的二維陣列當中! if (mlist) { mlists[mcount++] = mlist; fromBundle |= entry.hi->isBundle(); } property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi); ///> 將每一個分類的協議列表陣列放在 上方定義的二維陣列當中! if (proplist) { proplists[propcount++] = proplist; } protocol_list_t *protolist = entry.cat->protocols; ///> 將每一個分類的屬性列表陣列放在 上方定義的二維陣列當中! if (protolist) { protolists[protocount++] = protolist; } } ///> 取出類物件中的資料 auto rw = cls->data(); prepareMethodLists(cls, mlists, mcount, NO, fromBundle); /** 核心程式碼: rw: 類物件結構體中 有一個erw的結構, 這一步驟就是將資料合併到類物件的 rw結構中去 請參照文章: 將所有的分類的物件方法 附加到類物件中去! 也就是 在執行d時的時候講 分類的資料合併到了原始的類物件中!! */ rw->methods.attachLists(mlists, mcount); free(mlists); if (flush_caches && mcount > 0) flushCaches(cls); ///> 同理屬性方法列表 rw->properties.attachLists(proplists, propcount); free(proplists); ///> 同理協議方法列表 rw->protocols.attachLists(protolists, protocount); free(protolists); } 複製程式碼
-
command 進入 rw->methods.attachLists(mlists, mcount); 方法中
/** addedLists: [[method_t, method_t],[method_t, method_t]] addedCount: 2 s二維陣列的數量 */ 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; ///> realloc 重新分配記憶體 newCont ///> 為了合併分類中的陣列 擴充原來陣列的大小 ///> 需要重新分配記憶體 setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); array()->count = newCount; /* 記憶體移動 array()->lists 原來的方法列表 addedCount 分類的陣列的count 將原來的方法列表挪動到新的位置, (array()->lists + addedCount addedCount是挪動的位數 相當有將原來的方法放到了最後! */ memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0])); /* 記憶體挪動 拷貝 array()->lists 原來的方法列表 addedLists 傳進來的分類list 上面的方法已經將 類的方法列表做到了擴充 並且類原始帶的方法列表向後挪動的 addedCount的位數 為的就是 將傳入的分類的方法列表 拷貝到array()->lists(原始方法列表)的最前面, 所以 這就是分類的資料會優先呼叫的 原因 */ memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } else if (!list && addedCount == 1) { // 0 lists -> 1 list list = addedLists[0]; } else { // 1 list -> many lists List* oldList = list; uint32_t oldCount = oldList ? 1 : 0; uint32_t newCount = oldCount + addedCount; setArray((array_t *)malloc(array_t::byteSize(newCount))); array()->count = newCount; if (oldList) array()->lists[addedCount] = oldList; memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } } 複製程式碼
總結分類的一些問題
由原始碼分析我們可以得知,
-
Category的實現原理?
原理:底層結構是結構體 categoty_t 建立好分類之後分兩個階段:
-
編譯階段:
將每一個分類都生成所對應的 category_t結構體, 結構體中存放 分類的所屬類name、class、物件方法列表、類方法列表、協議列表、屬性列表。
-
Runtime執行時階段:
將生成的分類資料合併到原始的類中去,某個類的分類資料會在合併到一個大的陣列當中(後參與編譯的分類會在陣列的前面),分類的方法列表,屬性列表,協議列表等都放在二維陣列當中,然後重新組織類中的方法,將每一個分類對應的列表的合併到原始類的列表中。(合併前會根據二維陣列的數量擴充原始類的列表,然後將分類的列表放入前面)
-
-
為什麼Category的中的方法會優先呼叫?
如上所述, 在擴充陣列的時候 會將原始類中擁有的方法列表移動到後面, 將分類的方法列表資料放在前面,所以分類的資料會優先呼叫
-
延伸問題 - 如果多個分類中都實現了同一個方法,那麼在呼叫該方法的時候會優先呼叫哪一個方法?
在多個分類中擁有相同的方法的時候, 會根據編譯的先後順序 來新增分類方法列表, 後編譯的分類方法在最前面,所以要看 Build Phases --> compile Sources中的順序。 後參加編譯的在前面。
-
擴充套件和分類的區別
擴充套件@interface 是匿名分類, 不是分類。 就是屬性新增 在編譯的時候就加入到了類中
category在runtime中才合併的。
參考:
- Runtime原始碼地址:Source Browser:OBJective-c原始碼找到objc4,下載版本號最大是最新的原始碼
- MJ老師底層相關視訊
再次感謝!!
如有錯誤之處還請各位大神指出!!