什麼是category?
是在不改變已存在類的情況下,對其新增方法來達到對類進行功能擴充套件的目的。
category結構
可以在 objc-runtime-new.h 檔案中看到 category_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; //例項屬性列表
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);
};
複製程式碼
category 的實現原理
在原始碼 objc-runtime-new.mm 檔案中的第 2560 行開始,可以看到關於 category 的處理邏輯:
for (EACH_HEADER) {
// 1.獲取category列表
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
// 2.遍歷
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
// 3.對應的類cls
Class cls = remapClass(cat->cls);
// 3.1 如果cls為nil
if (!cls) {
// 3.2 將category置為nil
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
bool classExists = NO;
// 4. 例項方法 或 協議 或 屬性存在
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
// 4.1 處理category中的資料(類)
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
// 5. 類方法 或 協議 或 屬性存在
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
// 5.1 處理category中的資料(元類)
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
複製程式碼
處理category中的資料又是在方法remethodizeClass()中實現,注意傳入的cls引數,如果是例項方法則傳入的是類cls,如果是類方法,傳入的是cls的isa指標,也就是元類,跟蹤該方法:
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
// 1. 是否為元類
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)" : "");
}
// 2. 修改對應的類的方法列表、協議列表、屬性列表
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
複製程式碼
修改對應的類或元類中的方法列表、協議列表、屬性列表又是在attachCategories()方法內實現的,跟蹤該方法:
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// 1.建立陣列
// 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;
// 2. 倒敘插入
while (i--) {
auto& entry = cats->list[i];
// 2.1 方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 2.2 屬性列表
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
// 2.3 協議列表
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);
}
複製程式碼
建立一個陣列,倒敘插入 新增的方法、屬性和協議、具體的插入實現在方法attachLists()中,跟蹤該方法
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
// 1.主類中有多個資料集合的時候
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
// 1.1 使用realloc() 函式將原來的空間擴充
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
// 1.2 將原來的陣列複製到後面
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
// 1.3 把新陣列複製到前面
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
//2.為空的時候,直接將指標指向新的資料集。
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
// 3. 主類中只有一個資料集合的時候
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
// 3.1 使用malloc()重新申請一塊記憶體
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
// 3.2 將原來的集合放到最後
if (oldList) array()->lists[addedCount] = oldList;
// 3.3 新陣列複製到前邊
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
複製程式碼
如上縮減,runtime 針對主類中原有列表的三種情況,有三種不同的插入處理。
小結
- 當category中方法和類中的方法同名的話,會優先呼叫category中的方法
- 這裡看起來是category的方法覆蓋了主類的方法,但實際是category的方法在方法列表的前面,所以總先呼叫category中的方法。
- 多個category中存在相同的方法時,呼叫的順序跟編譯的順序有關,最後一個編譯的,會被呼叫。
category為什麼可以新增方法,而不能新增屬性呢?
回頭仔細看下 obj_class 的結構:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
/ ************** /
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
/ ************** /
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
複製程式碼
注意看 ivars 和 methodLists 有什麼區別?
ivars是指向 objc_ivar_list 的指標,而 methodLists 是指 向objc_method_list 的指標的指標。
objc_class 結構體的大小是固定的,不能新增資料,只能修改。
所以 ivars 指向的 objc_ivar_list 是一個固定區域,只能修改成員變數的值,不能增加成員變數的個數;
雖然沒辦法擴充套件methidLists指向的區域,但是可以修改 *objc_method_list 的值來增加成員變數;
給category增加屬性 : 關聯物件(Associated Object)
雖然可以實現,但是不推薦在category中新增屬性
在 .h 檔案中新增屬性的宣告
@property (copy, nonatomic) NSString * addName;
複製程式碼
在 .m 中
#import "NSObject+addProperty.h"
#import <objc/runtime.h>
@implementation NSObject (addProperty)
static char *addNameKey = "addNameKey";
-(void)setAddName:(NSString *)addName{
/* 關聯方法:
* id object 給哪個物件的屬性賦值
* const void *key 屬性對應的key
* id value 設定屬性值為value
* objc_AssociationPolicy policy 使用的儲存策略,copy/retain/assign
*/
objc_setAssociatedObject(self, addNameKey, addName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)addName{
return objc_getAssociatedObject(self, addNameKey);
}
@end
複製程式碼
關聯物件(Associated Object)
關聯物件可以被理解為 Runtime 中的字典
objc_setAssociatedObject 相當於 setValue:forKey 進行關聯value物件
關聯物件與被關聯物件本身的儲存並沒有直接的關係,它是儲存在單獨的雜湊表中的\
objc_getAssociatedObject 用來讀取物件
objc_removeAssociatedObjects函式來移除一個關聯物件
或者使用objc_setAssociatedObject函式將key指定的關聯物件設定為nil。