摘要
Category 主要作用是為已有的類,添加方法、屬性、協(xié)議。
其實現(xiàn)原理,一方面,在編譯時期,會生成 category_t 及相關(guān)結(jié)構(gòu)體。
另一方面,在運行時期,會將這些方法、屬性、協(xié)議添加到類之中。
category_t 結(jié)構(gòu)
// objc4-750.1/runtime/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;
// 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);
};
比較特殊地,它并非繼承 objc_object。
而且,當(dāng)中并沒有 ivars,這也解釋了,為什么 Category 不能添加變量。
插個題外話,其中的 _classProperties 比較少見,查了下,才發(fā)現(xiàn)是為配合 Swift,Xcode8 推出的新特性。
語法如下:
@property (class) NSString *someString;
編譯時期
- 編譯時期,不同 Category,會被編譯成不同的 category_t 結(jié)構(gòu)體。
- 而 Category 中的方法、屬性、協(xié)議等列表,也會被編譯成不同的結(jié)構(gòu)體,存于 category_t 之中。
- Category 的相關(guān)信息最終會在 Mach-O 中有所記錄。
具體的,先來個簡單代碼,看看會編譯成什么。
@implementation Cat (JD)
- (void)run {
NSLog(@"run");
}
@end
轉(zhuǎn)化成 c++ 代碼
clang -x objective-c -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 Cat+JD.m
可看到 run 方法被轉(zhuǎn)換成以下靜態(tài)函數(shù)
// @implementation Cat (JD)
static void _I_Cat_JD_run(Cat * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_mv_t42fx9pj0mn9k96m8y1rwwd40000gn_T_Cat_JD_0bdbd6_mi_0);
}
// @end
創(chuàng)建了名為 _OBJC_$_CATEGORY_INSTANCE_METHODS_Cat_$_JD 的 _method_list_t。
并與上面的靜態(tài)函數(shù)關(guān)聯(lián)起來。
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Cat_$_JD __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Cat_JD_run}}
};
創(chuàng)建名為 _OBJC_$_CATEGORY_Cat_$_JD 的 _category_t (即 category_t) 結(jié)構(gòu)體。
static struct _category_t _OBJC_$_CATEGORY_Cat_$_JD __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Cat",
0, // &OBJC_CLASS_$_Cat,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Cat_$_JD,
0,
0,
0,
};
與原來的類關(guān)聯(lián)
static void OBJC_CATEGORY_SETUP_$_Cat_$_JD(void) {
_OBJC_$_CATEGORY_Cat_$_JD.cls = &OBJC_CLASS_$_Cat;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
(void *)&OBJC_CATEGORY_SETUP_$_Cat_$_JD,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Cat_$_JD,
};
源碼中有不少 section ("__DATA,__objc_const") 這樣的代碼,其實就是 Mach-O 的相關(guān)存儲位置,會在加載時用到。
運行時期
Objc 初始化時,從 Mach-O 中拿到編譯時期產(chǎn)生的 category_t 數(shù)組。
再做了以下操作:
- 將 category_t 中的實例屬性、實例方法、協(xié)議添加到 class 之中。
- 將類屬性、類方法、協(xié)議添加到 metaClass 中。
添加之后, Category 方法、屬性、協(xié)議都會在原列表的前面。
來看看具體實現(xiàn)。
_read_images()
從 ObjC 初始化方法 _objc_init() 開始,會調(diào)用 _read_images()。
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
...
// _objc_init->map_images->map_images_nolock->_read_images->realizeClass
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
在 _read_images() 之中,既給 class 添加了實例方法、協(xié)議、實例屬性,也給 metaClass 添加了類方法、協(xié)議、類屬性。
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
...
// Discover categories.
for (EACH_HEADER) {
// 獲取
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
...
// 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;
// 實例方法、協(xié)議、實例屬性 -> class
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
...
}
// 類方法、協(xié)議、類屬性 -> class
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
...
}
}
}
...
}
attachCategories()
而添加的具體邏輯,并不在 remethodizeClass() 之中,而在 attachCategories()。
其調(diào)用堆棧

attachCategories() 核心代碼如下。
可以看到,后加載到的 category 信息,會先添加。
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
...
// 逆序添加
// 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();
}
// 省略了屬性、協(xié)議 ...
}
auto rw = cls->data();
...
// remethodizeClass() 調(diào)用的,需要更新緩存; methodizeClass() 則不需要
if (flush_caches && mcount> 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
// 省略了協(xié)議 ...
}
list_array_tt::attachLists()
添加列表的邏輯在 list_array_tt::attachLists() 之中
從中可看到,會保留類原來的列表,只是將其順序往后移動了,所以調(diào)用時,會優(yōu)先調(diào)用分類方法。
// objc4-750.1/runtime/objc-runtime-new.h
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]));
}
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]));
}
}
many lists -> many lists 當(dāng)中,關(guān)鍵的 2 行代碼
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
其實也就是將 Category 里的列表,移動到原列表之前,翻譯過來,可用一張圖表示。

小結(jié)
======== 編譯時期 ========
不同 Category,會被編譯成不同的 category_t 結(jié)構(gòu)體。
包括其中的方法、屬性、協(xié)議等列表,也被編譯成結(jié)構(gòu)體,存于 category_t 之中。
所有這些信息最終會在 Mach-O 中有所記錄。
======== 運行時期 ========
Objc 初始化時,從 Mach-O 中拿到編譯時期產(chǎn)生的 category_t 數(shù)組。
再做了以下操作:
- 將 category_t 中的實例屬性、實例方法、協(xié)議添加到 class 之中。
- 將類屬性、類方法、協(xié)議添加到 metaClass 中。
添加之后, Category 方法、屬性、協(xié)議列表都會在前面。
疑問
上面有提到 Category 無法添加變量的原因是 category_t 中無 ivars。
那為什么不在 category_t 中也增加 ivars 呢?
個人見解如下:
結(jié)合 objc_class 的結(jié)構(gòu)來看,變量列表存在于 class_ro_t 中,是不可變的。
而變量其實是存在于對象內(nèi)存布局之中,Category 是給類添加?xùn)|西,不應(yīng)影響對象。
struct class_ro_t {
// 變量列表
const ivar_list_t * ivars;
}
但不知正確與否,麻煩知道的同學(xué)告知一聲,感激不盡!