刨根究底之Category

powerx_yc發表於2016-03-07

起因
我們專案中很多公用的類都封裝在framework中,以便iPhone、iPad共同呼叫。某些邏輯不一樣的東西我們會在主工程用category實現,然而category預設是不能定義屬性的。我們來看看怎麼在category中新增屬性
AssociatedObject
iOS有些基礎的朋友應該會知道,runtime裡有objc_setAssociatedObject和objc_getAssociatedObject兩個方法,可以將屬性掛到Object上:

@interface Model (Property)
@property (nonatomic, strong)NSString *name;
@end

@implementation Model (Property)
static void *kName = &kName;
- (void)setName:(NSString *)name{ objc_setAssociatedObject(self, kName, name, OBJC_ASSOCIATION_COPY_NONATOMIC);}
- (NSString *)name{ return objc_getAssociatedObject(self, kName);}
@end

重寫property的setter和getter方法,在setter的時候呼叫objc_setAssociatedObject將屬性掛到self上,在getter的時候,從self身上將屬性取出來。

這樣的實現已經是可以用了,事實上大多數為category新增屬性的程式碼都是這樣寫的。不過我還是太懶,每新增一個屬性,就要寫這麼大一堆程式碼。想想要是加上十個八個屬性,頓時整個人都覺得不好了...

class_addMethod
既然能用runtime動態將屬性掛在class上,我們也可以用runtime動態將setter和getter方法插入到class中。runtime提供了class_addMethod方法動態插入method

1229266-96619ebe75efe707.jpg
2.jpg

class_addMethod需要4個引數。class可以通過[self class]獲取,SEL可以通過property的name拼接出對應的SEL,types由於引數的型別固定,所以也是可以直接確定。但是IMP怎麼辦?
imp_implementationWithBlock
說到IMP,我們先來了解一下IMP是個什麼東西

1229266-eda1eb9770aa5bb6.jpg
3.jpg

IMP是一個函式指標,指向相應的函式實現,函式一般會有2個預設引數:id型別的self和SEL型別的_cmd。平時我們之所以能在OC函式中呼叫self,也是因為函式中有隱藏起來了的self引數
翻閱runtime的文件,我們找到了通過block轉換成IMP的API:

1229266-d7f8f499d1b7f300.jpg
4.jpg

封裝
一切都準備就緒了,那我們就來封裝一個動態新增屬性的方法吧,為了簡化流程,我們暫時先只考慮id型別的屬性。

+ (void)addObjectProperty:(NSString *)name
{ 
//1. 通過class的指標和property的name,建立一個唯一的key NSString *key = [NSString stringWithFormat:@"%p_%@",self,name]; 
//2. 用block實現setter方法
 id setblock = ^(id self,id value){ objc_setAssociatedObject(self, (__bridge void *)key, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }; 
//3. 將block的實現轉化為IMP 
IMP imp = imp_implementationWithBlock(setblock);
 //4. 用name拼接出setter方法 
NSString *selString = [self setMethodNameWithProperty:name]; 
//5. 將setter方法加入到class中 
BOOL result = class_addMethod([self class], NSSelectorFromString(selString), imp, "v@:@"); 
//6. getter 
id getBlock = ^id(id self){ return objc_getAssociatedObject(self, (__bridge void*)key); };
 IMP getImp = imp_implementationWithBlock(getBlock); 
result = class_addMethod([self class], NSSelectorFromString(name), getImp, "@@:");}

通過class的指標和property的name,建立一個唯一的key。用來在AssociatedObject的時候存取屬性。
在block中實現setter方法
通過block建立IMP
通過name將setter的方法名拼接出來,-setMethodNameWithProperty是自己寫的方法,這裡沒有貼出來
將setter方法加入到class中。其中@"v@:@":v表示空,setter的返回值為空。@表示id型別,第一個引數,也就是self為id型別。:表示SEL型別,第二個引數為method的selector。@表示id型別,第三個引數也就是setter方法真正要傳入的引數為id型別。
後面是相應的getter方法,與setter方法類似

使用
這時候,再也不用擔心有很多property,寫一堆重複的程式碼。我們只需要調一個函式就可以將在category插入屬性

@interface Model (Property)
@property (nonatomic, strong)NSString *name;
@property (nonatomic, strong)NSURL *URL;
@property (nonatomic, strong)NSDate *date;
@end

@implementation Model (Property)
+ (void)load{ [self addObjectProperty:@"name"]; 
[self addObjectProperty:@"URL"];
 [self addObjectProperty:@"date"];}
@end

+load方法在程式執行之前會呼叫,不用擔心在用的時候,property還未插入進去。所有的category的+load方法系統都會自動呼叫。也不用擔心+load方法在category中被覆蓋。
開始考慮過在+initialize中使用,不過由於+initialize一個class只會呼叫一次,多個category的時候會有覆蓋。所以+load中使用是最好的選擇

你以為這樣就ok了麼?過幾天有使用者反饋說App啟動的時候有點卡啊,因為+load方法是在app啟動的時候呼叫的,裡面執行的程式碼越多,App啟動越慢,(說得有點誇張,實際這點程式碼影響不了什麼)。我們知道除了+load之外,還有一個+initialize方法,+initialize會在第一次使用這個類的時候呼叫,我們完全可以在+initialize中新增屬性。然而+initialize有個最大的問題就是,他跟普通方法一樣,當有多個category實現的時候,會發生覆蓋,系統只會呼叫一個Category中的+initialize,那該怎麼辦呢?
消除Category同名方法覆蓋
sunnyxx大神在objc category的祕密裡介紹過,category的同名方法覆蓋並不是真的其他同名方法就消失了,而是因為系統呼叫方法的時候根據方法名在method_list中查詢方法,找到第一個名字匹配的方法之後就不繼續往下找了。所以每次呼叫的都是method_list中最前面的同名方法。實際其他同名方法還在method_list中so...我們可以根據selector查詢到所有的同名method,然後呼叫:

static inline void __invoke_all_method(id self, SEL selecotr)
{ 
//1. 根據self,獲取
class Class class = object_getClass(self); 
//2. 獲取方法列表 
uint count; 
Method *methodList = class_copyMethodList(class, &count);
 //3. 遍歷方法列表 
for (int i = 0; i < count; i++)
 { Method method = methodList[i]; 
//4. 根據SEL查詢方法 
if (!sel_isEqual(selecotr, method_getName(method))) 
{ continue; }
 //5. 獲取方法的實現
 IMP implement = method_getImplementation(method); 
//6. 直接呼叫方法的實現
 ((void(*)(id,SEL))implement)(self, selecotr); }}
+ (void)invokeAllClassMethodWithSelector:(SEL)selector
{ 
  __invoke_all_method(self, selector);
}

根據剛剛介紹的原理,我們封裝了一個通過selector呼叫所有同名method的方法。

根據self,獲取class,如果self是例項方法的self,這裡獲取的是普通的class,如果self是類方法的self,這裡獲取的是metaClass。例項方法存放在普通class中,類方法存放在metaClass中。瞭解更多請看iOS開發RunTime之函式呼叫

通過class_copyMethodList獲取class的方法列表。如果class傳的是metaClass,獲取的是類方法的方法列表,如果class是普通class,獲取的是例項方法的方法列表。
遍歷methodList
根據SEL查詢method
獲取IMP
直接呼叫IMP

在系統的+initialize中,我們用invokeAllClassMethodWithSelector呼叫自定義的+categoryInitialize。這時候,在category的+categoryInitialize中新增屬性,就不怕Category覆蓋了

@implementation Model
+ (void)initialize{
 [self invokeAllClassMethodWithSelector:@selector(categoryInitialize)];}
@end

@implementation Model (Property1)
+ (void)categoryInitialize{ [self addBasicProperty:@"point" encodingType:@encode(CGPoint)];
 [self addBasicProperty:@"myRect" encodingType:@encode(CGRect)];}
@end

@implementation Model (Property2)
+ (void)categoryInitialize{ [self addBasicProperty:@"f" encodingType:@encode(float)]; 
[self addBasicProperty:@"a" encodingType:@encode(int)];}
@end

Extension
文章主要為了說明思路,很多程式碼沒貼出來。也沒考慮介面設計和不是id型別的問題。如果想在專案中使用這個方法。大家可以去我的github上下載完整的程式碼。LcCategoryProperty

相關文章