??Category是我們在開發(fā)中經(jīng)常用到的,它可以在我們不改變原有類的前提下來動態(tài)地給類添加方法,通過這篇文章,我們一起來了解一下Category。
下面我們列一下本文目錄,以方便了解本文的主要內(nèi)容。
- Category的介紹
- Category中的數(shù)據(jù)結(jié)構(gòu)
- Category的原理
- Category的使用
- Category和Extension的區(qū)別
Category的介紹
??Category是Objective-C 2.0之后添加的語言特性,它可以在不改變或不繼承原類的情況下,動態(tài)地給類添加方法。我們平常說的分類或者類別就是說的Category。
Category中的數(shù)據(jù)結(jié)構(gòu)
Category定義的源碼我們可以在runtime的源碼中找到,我們先來看一下Category的定義:
///runtime.h
/// An opaque type that represents a category.
typedef struct objc_category *Category;
struct objc_category {
///Category名稱
char * _Nonnull category_name OBJC2_UNAVAILABLE;
///類名
char * _Nonnull class_name OBJC2_UNAVAILABLE;
///實例方法列表
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
///類方法列表
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
///協(xié)議列表
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
}
??通過定義我們可以看到,Category是指向objc_category結(jié)構(gòu)體的指針,而objc_category結(jié)構(gòu)體中包含了當前Category的名稱(category_name)、類名稱(class_name)、實例方法列表(instance_methods)、類方法列表(class_methods)、協(xié)議列表(protocols),我們看到objc_category結(jié)構(gòu)體的定義中并沒有屬性列表,這也就是為什么我們用Category不能給類添加實例變量的原因。
Category的原理
??Category既然可以動態(tài)的給類添加方法,那么它的方法又是在什么時候添加的?把方法添加到哪里面去了呢?我們在調(diào)用Category中方法的時候又是怎樣調(diào)用的呢?下面我們通過分析Category的源碼來看一下它是怎樣運行的。
??因為我們是在runtime的源碼中找到Category的源碼的,那么我們猜想Category中的方法是不是在運行時添加的呢?下面我們看一下它的源碼:
Category在objc-runtime-new.h中的定義是category_t結(jié)構(gòu)體。在查找Category執(zhí)行時后我并不知道該如何查起,所以我就在objc-runtime-new.mm搜了一下category_t,我在2703行發(fā)現(xiàn)這樣一段代碼,并有說明:
///runtime.h
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
通過注釋我們知道在這發(fā)現(xiàn)了categories,當時我覺得這個方法應(yīng)該就是,然后我就看了一下這個方法,發(fā)現(xiàn)這正是Category中的方法添加。在這里也給大家提供一個比較笨的方法,當我們查看源碼時,只知道方法或者結(jié)構(gòu)體的定義時,可以在當前的類中搜索,由于C語言的方法定義問題(方法的聲明必須在調(diào)用之前,否則會因為沒定義方法而報錯,這里它并不像我們OC中的方法定義一樣,可以在任意地方定義,在任意地方調(diào)用,C語言的方法定義必須在調(diào)用之前定義好,否則會提示方法沒有定義而報錯),我們都可以很容易的在當前文件中找到,而且源碼中的注釋寫的也很明白。下面我們分析一下這段代碼:
///runtime.h
// 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);
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);
}
}
}
}
首先我們看到的是一個for循環(huán),而for循環(huán)的單次表達式、表達式和末尾循環(huán)體是一個EACH_HEADER宏,我們找到該宏的定義:
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
這里hCount和hList都是調(diào)用_read_images該方法時傳過來的參數(shù),hi是名稱為header_info結(jié)構(gòu)體,通過定義我們大約可以猜測出來,這是在遍歷類的頭文件,而hi中就是類的頭文件的信息。然后我們通過_getObjc2CategoryList方法獲取到hi的category_t列表(里面包含了當前類的所有category_t)和長度。
category_t **catlist =
_getObjc2CategoryList(hi, &count);
拿到列表和長度后下面又是通過for循環(huán)來進行遍歷,獲取到每一個category_t,并且根據(jù)category_t的cls指針來獲取到對應(yīng)的類:
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
獲取到category_t和對應(yīng)的類后,通過addUnattachedCategoryForClass方法將Category和類先關(guān)聯(lián)起來(這里只是關(guān)聯(lián)起來,并沒有做其他的操作),下面通過remethodizeClass方法來整理相應(yīng)的類:
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
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)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
在remethodizeClass方法中,我們看見通過unattachedCategoriesForClass方法將剛才關(guān)聯(lián)的類的Category獲取到,獲取到category_list類型的列表后(category_list類型的列表包含了當前類所有的Category),執(zhí)行了attachCategories方法整理類的方法、屬性和協(xié)議列表:
// 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)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
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->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);
}
執(zhí)行attachCategories首先執(zhí)行下列代碼重新分配分類的內(nèi)存:
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
分配完內(nèi)存地址后就開始遍歷所有的分類,將每一個分類的方法、屬性和協(xié)議添加到對應(yīng)的mlists、proplists和protolists中。添加完成后通過data()方法來拿到類對象的class_rw_t結(jié)構(gòu)體類型的rw,之后通過調(diào)用rw中的方法列表、屬性列表和協(xié)議列表的attachLists函數(shù),將所有分類的方法、屬性和協(xié)議列表數(shù)組添加進去。
下面我們來看一下attachLists中做了什么:
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]));
}
}
addLists:調(diào)用方法時傳過來的,也就是要添加的分類的方法或?qū)傩粤斜?/h5>
array()->lists:類中原有的方法或?qū)傩粤斜?/h5>
下面我們看一下方法中的兩個函數(shù):memmove和memcpy:
這兩函數(shù)都是C語言中的函數(shù)庫,作用都是拷貝一定長度的內(nèi)存內(nèi)容,其長度由第三參數(shù)決定,他們兩個唯一的區(qū)別是:當內(nèi)存發(fā)生局部重疊時,memmove能夠保證拷貝結(jié)果的正確行,而memcpy不能保證拷貝結(jié)果是正確的(在這里這兩個方法就不多介紹了有興趣的同學可以參考一下這里,關(guān)于memmove 和 memcpy的區(qū)別)。
??通過上面兩個方法我們就把Category里面的方法放到了類中原方法的前面,所以當我們調(diào)用的時候會優(yōu)先調(diào)用Category中的方法。(注:有些人說Category里面的方法把類中原有的方法覆蓋了,其實這是錯誤的。Category中的方法并沒有覆蓋類中原有的方法,只是Category中的方法在類中原有的方法前面,當Runtime通過方法名查找的時候找到第一個方法就去執(zhí)行了它的實現(xiàn),并沒有繼續(xù)往下查找,關(guān)于方法的執(zhí)行可以參考這里)。我們可以通過以下代碼來調(diào)用類中原來的方法:
///Cat為自己創(chuàng)建的類,在類中聲明一個sleep方法,創(chuàng)建一個cat的Category,聲明并實現(xiàn)sleep方法
Class currentClass = [Cat class];
Cat *cat = [[Cat alloc] init];
[cat sleep];
if (currentClass) {
unsigned int methodCount;
Method *methodList = class_copyMethodList(currentClass, &methodCount);
IMP lastImp = NULL;
SEL lastSel = NULL;
for (NSInteger i = 0; i < methodCount; i++) {
Method method = methodList[i];
NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method))
encoding:NSUTF8StringEncoding];
if ([@"sleep" isEqualToString:methodName]) {
lastImp = method_getImplementation(method);
lastSel = method_getName(method);
}
}
typedef void (*fn)(id,SEL);
if (lastImp != NULL) {
fn f = (fn)lastImp;
f(cat,lastSel);
}
free(methodList);
}
??以上就是Category的實現(xiàn)原理。通過這些我們也就了解了一些,Category中的屬性、方法、協(xié)議是在運行時被添加到了類的屬性列表、方法列表、協(xié)議列表中,既然它里面的方法是被添加到了類的方法列表中,那么它里面方法的調(diào)用和類中方法的調(diào)用是一樣的。
Category的使用
- 添加方法
- 聲明私有方法
- 分解體積龐大的類文件
以上是Apple推薦的使用方法,當然還有一些人開發(fā)出了其他的用法,比如:模擬多繼承、把framework私有方法公開,感興趣的朋友可以去了解一下。
Category和Extension的區(qū)別
- Category中原則上只能增加方法,不能增加屬性(通過Runtime也可以實現(xiàn));Extension既可以增加方法,還可以增加實例變量(該實例變量和方法默認是私有的,只能在本類中調(diào)用)
- Category中的方法是在運行時決議的,沒有實現(xiàn)也可以運行,而Extension中的方法是在編譯器檢查的,沒有實現(xiàn)會報錯
- Category可以給任意類添加方法,而Extension的添加必須有這個類的源碼,對于一些系統(tǒng)類,如NSString類是無法添加Extension的,但是可以添加Category。
結(jié)束
??以上就是有關(guān)Category的原理和使用,Category還是基于在Runtime上實現(xiàn)的,如果沒有Runtime的支持Category就不能夠?qū)崿F(xiàn)。Category在我們開發(fā)中的使用還是很多的,大家可以通過源碼去學習研究。
??文章若有不足之處還請不吝賜教,大家互相學習。如果您覺得我的文章有用,點一下喜歡就可以了哦。