起因
我們項目中很多公用的類都封裝在framework中富雅,以便iPhone裆熙、iPad共同調(diào)用邓夕。某些邏輯不一樣的東西我們會在主工程用category實現(xiàn)掰茶,然而category默認(rèn)是不能定義屬性的萤厅。我們來看看怎么在category中添加屬性
AssociatedObject
iOS有些基礎(chǔ)的朋友應(yīng)該會知道橄抹,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的時候調(diào)用objc_setAssociatedObject將屬性掛到self上楼誓,在getter的時候,從self身上將屬性取出來赦拘。
這樣的實現(xiàn)已經(jīng)是可以用了慌随,事實上大多數(shù)為category添加屬性的代碼都是這樣寫的。不過我還是太懶躺同,每添加一個屬性阁猜,就要寫這么大一堆代碼。想想要是加上十個八個屬性蹋艺,頓時整個人都覺得不好了...
class_addMethod
既然能用runtime動態(tài)將屬性掛在class上剃袍,我們也可以用runtime動態(tài)將setter和getter方法插入到class中。runtime提供了class_addMethod方法動態(tài)插入method
class_addMethod需要4個參數(shù)捎谨。class可以通過[self class]獲取民效,SEL可以通過property的name拼接出對應(yīng)的SEL,types由于參數(shù)的類型固定涛救,所以也是可以直接確定畏邢。但是IMP怎么辦?
imp_implementationWithBlock
說到IMP检吆,我們先來了解一下IMP是個什么東西
IMP是一個函數(shù)指針舒萎,指向相應(yīng)的函數(shù)實現(xiàn),函數(shù)一般會有2個默認(rèn)參數(shù):id類型的self和SEL類型的_cmd蹭沛。平時我們之所以能在OC函數(shù)中調(diào)用self臂寝,也是因為函數(shù)中有隱藏起來了的self參數(shù)
翻閱runtime的文檔,我們找到了通過block轉(zhuǎn)換成IMP的API:
封裝
一切都準(zhǔn)備就緒了摊灭,那我們就來封裝一個動態(tài)添加屬性的方法吧咆贬,為了簡化流程,我們暫時先只考慮id類型的屬性帚呼。
+ (void)addObjectProperty:(NSString *)name
{
//1. 通過class的指針和property的name掏缎,創(chuàng)建一個唯一的key NSString *key = [NSString stringWithFormat:@"%p_%@",self,name];
//2. 用block實現(xiàn)setter方法
id setblock = ^(id self,id value){ objc_setAssociatedObject(self, (__bridge void *)key, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); };
//3. 將block的實現(xiàn)轉(zhuǎn)化為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,創(chuàng)建一個唯一的key。用來在AssociatedObject的時候存取屬性御毅。
在block中實現(xiàn)setter方法
通過block創(chuàng)建IMP
通過name將setter的方法名拼接出來根欧,-setMethodNameWithProperty是自己寫的方法,這里沒有貼出來
將setter方法加入到class中端蛆。其中@"v@:@":v表示空凤粗,setter的返回值為空。@表示id類型今豆,第一個參數(shù)嫌拣,也就是self為id類型。:表示SEL類型呆躲,第二個參數(shù)為method的selector异逐。@表示id類型,第三個參數(shù)也就是setter方法真正要傳入的參數(shù)為id類型插掂。
后面是相應(yīng)的getter方法灰瞻,與setter方法類似
使用
這時候,再也不用擔(dān)心有很多property辅甥,寫一堆重復(fù)的代碼酝润。我們只需要調(diào)一個函數(shù)就可以將在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方法在程序運行之前會調(diào)用,不用擔(dān)心在用的時候璃弄,property還未插入進(jìn)去要销。所有的category的+load方法系統(tǒng)都會自動調(diào)用。也不用擔(dān)心+load方法在category中被覆蓋夏块。
開始考慮過在+initialize中使用疏咐,不過由于+initialize一個class只會調(diào)用一次,多個category的時候會有覆蓋脐供。所以+load中使用是最好的選擇
你以為這樣就ok了么浑塞?過幾天有用戶反饋說App啟動的時候有點卡啊,因為+load方法是在app啟動的時候調(diào)用的政己,里面執(zhí)行的代碼越多酌壕,App啟動越慢,(說得有點夸張匹颤,實際這點代碼影響不了什么)。我們知道除了+load之外托猩,還有一個+initialize方法印蓖,+initialize會在第一次使用這個類的時候調(diào)用,我們完全可以在+initialize中添加屬性京腥。然而+initialize有個最大的問題就是赦肃,他跟普通方法一樣,當(dāng)有多個category實現(xiàn)的時候,會發(fā)生覆蓋他宛,系統(tǒng)只會調(diào)用一個Category中的+initialize船侧,那該怎么辦呢?
消除Category同名方法覆蓋
sunnyxx大神在objc category的秘密里介紹過厅各,category的同名方法覆蓋并不是真的其他同名方法就消失了镜撩,而是因為系統(tǒng)調(diào)用方法的時候根據(jù)方法名在method_list中查找方法,找到第一個名字匹配的方法之后就不繼續(xù)往下找了队塘。所以每次調(diào)用的都是method_list中最前面的同名方法袁梗。實際其他同名方法還在method_list中so...我們可以根據(jù)selector查找到所有的同名method,然后調(diào)用:
static inline void __invoke_all_method(id self, SEL selecotr)
{
//1. 根據(jù)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. 根據(jù)SEL查找方法
if (!sel_isEqual(selecotr, method_getName(method)))
{ continue; }
//5. 獲取方法的實現(xiàn)
IMP implement = method_getImplementation(method);
//6. 直接調(diào)用方法的實現(xiàn)
((void(*)(id,SEL))implement)(self, selecotr); }}
+ (void)invokeAllClassMethodWithSelector:(SEL)selector
{
__invoke_all_method(self, selector);
}
根據(jù)剛剛介紹的原理遮怜,我們封裝了一個通過selector調(diào)用所有同名method的方法。
根據(jù)self鸿市,獲取class锯梁,如果self是實例方法的self,這里獲取的是普通的class焰情,如果self是類方法的self陌凳,這里獲取的是metaClass。實例方法存放在普通class中烙样,類方法存放在metaClass中冯遂。了解更多請看iOS開發(fā)RunTime之函數(shù)調(diào)用
通過class_copyMethodList獲取class的方法列表。如果class傳的是metaClass谒获,獲取的是類方法的方法列表蛤肌,如果class是普通class,獲取的是實例方法的方法列表批狱。
遍歷methodList
根據(jù)SEL查找method
獲取IMP
直接調(diào)用IMP
在系統(tǒng)的+initialize中裸准,我們用invokeAllClassMethodWithSelector調(diào)用自定義的+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
文章主要為了說明思路,很多代碼沒貼出來爪膊。也沒考慮接口設(shè)計和不是id類型的問題权悟。如果想在項目中使用這個方法。大家可以去我的github上下載完整的代碼推盛。LcCategoryProperty