刨根究底之Category
起因
我們專案中很多公用的類都封裝在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
class_addMethod需要4個引數。class可以通過[self class]獲取,SEL可以通過property的name拼接出對應的SEL,types由於引數的型別固定,所以也是可以直接確定。但是IMP怎麼辦?
imp_implementationWithBlock
說到IMP,我們先來了解一下IMP是個什麼東西
IMP是一個函式指標,指向相應的函式實現,函式一般會有2個預設引數:id型別的self和SEL型別的_cmd。平時我們之所以能在OC函式中呼叫self,也是因為函式中有隱藏起來了的self引數
翻閱runtime的文件,我們找到了通過block轉換成IMP的API:
封裝
一切都準備就緒了,那我們就來封裝一個動態新增屬性的方法吧,為了簡化流程,我們暫時先只考慮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
相關文章
- iOS套路面試題之CategoryiOS面試題Go
- Category探索Go
- Extension,CategoryGo
- hello categoryGo
- man categoryGo
- Category – 簡介Go
- 分類-CategoryGo
- OC Category、AssociatedObjectGoObject
- IOS category 與 extensioniOSGo
- Category_theory and FunctorGo
- 如何在SAP Spartacus category 頁面裡拿到當前的category資訊Go
- Category的本質<一>Go
- iOS底層原理-CategoryiOSGo
- Category的實現原理Go
- 談談Category和ExtensionGo
- iOS設計模式——CategoryiOS設計模式Go
- 10.2.0.4 Bug Fixes by CategoryGo
- Item category - VOV7Go
- SAP MM Purchase Order History CategoryGo
- 探秘Runtime - 深入剖析CategoryGo
- Category 的一些事Go
- iOS Extension Category Protrol 例子理解iOSGo
- SAP SD 基礎知識之計劃行類別(Schedule Line Category)Go
- ObjC中Category的原理簡析OBJGo
- Runtime原始碼 Category(分類)原始碼Go
- iOS問題整理03----CategoryiOSGo
- JUnit 註解@Category的工作原理Go
- 關於iOS Class Category的整理iOSGo
- 【iOS】category重寫方法的呼叫iOSGo
- ecshop /category.php SQL Injection VulGoPHPSQL
- 深入理解Objective-C:CategoryObjectGo
- iOS使用Category新增@property變數iOSGo變數
- Category的本質<三>關聯物件Go物件
- iOS runtime 給 Category 加屬性iOSGo
- 範疇category:組合的本質Go
- SAP CRM Product category的決定邏輯Go
- Category的本質<二>load,initialize方法Go
- 如何使用ABAP程式碼建立SAP Product CategoryGo