Category 的一些事
?
來(lái)源:伯樂(lè)在線(xiàn) - Tsui YuenHong
鏈接:http://ios.jobbole.com/90422/
點(diǎn)擊 → 申請(qǐng)加入伯樂(lè)在線(xiàn)專(zhuān)欄作者
?
新增實(shí)踐部分:偏方 Hook 進(jìn)某些方法來(lái)添加功能
?
Category – 簡(jiǎn)介
?
Category(類(lèi)別)是 Objective-C 2.0 添加的新特性(十年前的新特性 ?)。其作用可以擴(kuò)展已有的類(lèi), 而不必通過(guò)子類(lèi)化已有類(lèi),甚至也不必知道已有類(lèi)的源碼,還有就是分散代碼,使已有類(lèi)的體積大大減少,也利于分工合作。
?
在蘋(píng)果開(kāi)源項(xiàng)目中,我們可以下載相關(guān)的源碼來(lái)查看 category 的資料。
?
在 AFNetworking 和 SDWebImage 中也大量用到 category 來(lái)擴(kuò)展已有類(lèi)和分散代碼。
?
關(guān)于 category 的定義可以在 objc-runtime-new.h 中找到。由其定義可以看出 category 可以正常實(shí)現(xiàn)功能有:添加實(shí)例方法、類(lèi)方法、協(xié)議、實(shí)例屬性。( 在后面的實(shí)踐中,發(fā)現(xiàn)類(lèi)屬性也是可以添加的 )
?
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;
?
????method_list_t *methodsForMeta(bool isMeta) {
????????if (isMeta) return classMethods;
????????else return instanceMethods;
????}
?
????property_list_t *propertiesForMeta(bool isMeta) {
????????if (isMeta) return nil; // classProperties;
????????else return instanceProperties;
????}
};
?
隨便說(shuō)一句,本文并不主要注重 category 的實(shí)現(xiàn)細(xì)節(jié)和工作原理。關(guān)于細(xì)節(jié)的方面可以看相關(guān)文章 深入理解Objective-C:Category(上) ?深入理解Objective-C:Category(下) 和 結(jié)合 category 工作原理分析 OC2.0 中的 runtime 。
?
?
Category – 能做什么
?
首先,我們先來(lái)創(chuàng)建一個(gè) Person 類(lèi)以及 Person 類(lèi)的 category,可以看得出 category 的文件名就是 已有類(lèi)名+自定義名。
?
// Person.h
@interface Person : NSObject
?
@property (nonatomic, copy) NSString *name;
?
+ (void)run;
- (void)talk;
?
@end
?
// Person.m
@implementation Person
?
// 原實(shí)例方法
- (void)talk{
????NSLog(@"\n我是原實(shí)例方法\n我是%@",self.name);
}
?
// 原類(lèi)方法
+ (void)run{
????NSLog(@"\n我是原類(lèi)方法\n我是跑得很快的的香港記者");
}
?
@end
?
// Person+OtherSkills.h
@interface Person (OtherSkills){
????//?? instance variables may not be placed in categories
????//int i;
????//NSString *str;
}
?
// 添加實(shí)例屬性
@property (nonatomic, copy) NSString *otherName;
// 添加類(lèi)屬性
@property (class, nonatomic, copy) NSString *clsStr;
?
// 重寫(xiě)已有類(lèi)方法
+ (void)run;
- (void)talk;
?
// 為已有類(lèi)添加方法
- (void)logInstProp;
+ (void)logClsProp;
?
// Person+OtherSkills.m
static NSString *_clsStr = nil;
static NSString *_otherName = nil;
?
@implementation Person (OtherSkills)
?
@dynamic otherName;
?
// 重寫(xiě)類(lèi)方法
+ (void)run{
????// 警告?? Category is implementing a method which will also be implemented by its primary class
????NSLog(@"\n我是重寫(xiě)方法\n我是跑得很快的的香港記者");
}
?
// 重寫(xiě)實(shí)例方法
- (void)talk{
????// 警告?? Category is implementing a method which will also be implemented by its primary class
????NSLog(@"\n我是重寫(xiě)方法\n我是會(huì)談笑風(fēng)生的%@",self.otherName);
}
?
// 輸出實(shí)例屬性
- (void)logInstProp{
????NSLog(@"\n輸出實(shí)例屬性\n我是會(huì)談笑風(fēng)生的%@",self.otherName);
}
?
// 輸出類(lèi)屬性
+ (void)logClsProp{
????NSLog(@"\n輸出類(lèi)屬性\n我是會(huì)談笑風(fēng)生的%@",self.clsStr);
}
?
+ (NSString *)clsStr{
????return _clsStr;
}
?
+ (void)setClsStr:(NSString *)clsStr{
????_clsStr = clsStr;
}
?
- (NSString *)otherName{
????return _otherName;
}
?
- (void)setOtherName:(NSString *)otherName{
????_otherName = otherName;
}
?
創(chuàng)建完代碼之后,下面我們來(lái)看看 category 到底能干什么。
?
順便一提,我是在網(wǎng)上看到很多文章說(shuō) category 不能添加屬性,這是說(shuō)法是不對(duì)的,如 Person+OtherSkills.h 中就添加了一個(gè) otherName 的屬性。正確的說(shuō)法應(yīng)該是 category 不能添加實(shí)例變量,否則編譯器會(huì)報(bào)錯(cuò) instance variables may not be placed in categories。正常情況下,因?yàn)?category 不能添加實(shí)例變量,也會(huì)導(dǎo)致屬性的 setter & getter 方法不能正常工作。( 當(dāng)然,可以利用 Runtime 為 category 動(dòng)態(tài)關(guān)聯(lián)屬性,最后會(huì)介紹兩種使 category 屬性正常工作的方法)
?
category 可以為已有類(lèi)添加實(shí)例屬性。
?
如 Person+OtherSkills.h 中就添加了一個(gè) otherName 的屬性。可以出來(lái)能正常工作。
?
// 運(yùn)行代碼
Person *p1 = [[Person alloc] init];
?
// 實(shí)例屬性
p1.otherName = @"小花";
[p1 logInstProp];
?
p1.otherName = @"小明";
[p1 logInstProp];
?
// 輸出結(jié)果
2016-09-11 09:45:09.935 category[37281:1509791]
輸出實(shí)例屬性
我是會(huì)談笑風(fēng)生的小花
2016-09-11 09:45:09.936 category[37281:1509791]
輸出實(shí)例屬性
我是會(huì)談笑風(fēng)生的小明
?
category 可以為已有類(lèi)添加類(lèi)屬性。
?
雖然,category_t 中是沒(méi)有定義 clssProperties,但是根據(jù)實(shí)際操作卻顯示 category 的確可以為已有類(lèi)添加類(lèi)屬性并且成功執(zhí)行。
?
// 運(yùn)行代碼
Person.clsStr = @"小東";
[Person logClsProp];
?
// 輸出結(jié)果
2016-09-11 09:45:09.936 category[37281:1509791]
輸出類(lèi)屬性
我是會(huì)談笑風(fēng)生的小東
?
category 可以為已有類(lèi)添加實(shí)例方法和類(lèi)方法。
?
在上面的兩個(gè)例子中已經(jīng)體現(xiàn)了 category 可以為已有類(lèi)添加實(shí)例方法和類(lèi)方法。這里將討論加入 category 重寫(xiě)了已有類(lèi)的方法會(huì)怎么樣,在創(chuàng)建的代碼中我們已經(jīng)重寫(xiě)了 run 和 talk 方法,那這時(shí)我們來(lái)調(diào)用看看。
?
// 運(yùn)行代碼
// 調(diào)用類(lèi)方法
[Person run];
// 調(diào)用實(shí)例方法????
Person *p1 = [[Person alloc] init];
[p1 talk];
?
// 輸出結(jié)果
2016-09-11 11:22:05.817 category[37733:1562534]
我是重寫(xiě)方法
我是跑得很快的的香港記者
2016-09-11 11:22:05.817 category[37733:1562534]
我是重寫(xiě)方法
我是會(huì)談笑風(fēng)生的(null)
?
可以看得出來(lái),這時(shí)候無(wú)論是已有類(lèi)中的類(lèi)方法和實(shí)例方法都可以被 category 替換到其中的重寫(xiě)方法,即使我現(xiàn)在是沒(méi)有導(dǎo)入 Person+OtherSkills.h 。這就帶來(lái)一個(gè)很?chē)?yán)重的問(wèn)題,如果在 category 中不小心重寫(xiě)了已有類(lèi)的方法將導(dǎo)致原方法無(wú)法正常執(zhí)行。所以使用 category 添加方法時(shí)候請(qǐng)注意是否和已有類(lèi)重名了,正如 《 Effective Objective-C 2.0 》 中的第 25 條所建議的:
?
在給第三方類(lèi)添加 category 時(shí)添加方法時(shí)記得加上你的專(zhuān)有前綴
?
然而,因?yàn)?category 重寫(xiě)方法是并不是替換掉原方法,而是往已有類(lèi)中繼續(xù)添加方法,所以還是有機(jī)會(huì)去調(diào)用到原方法。這里利用 class_copyMethodList 獲取 Person 類(lèi)的全部類(lèi)方法和實(shí)例方法。
?
// 獲取 Person 的方法列表
unsigned int personMCount;
// 獲取實(shí)例方法
//Method *personMList = class_copyMethodList([Person class], &personMCount);
// 獲取類(lèi)方法
Method *personMList = class_copyMethodList(object_getClass([Person class]), &personMCount);
NSMutableArray *mArr = [NSMutableArray array];
?
// 這里是倒序獲取,所以 mArr 第一個(gè)方法對(duì)應(yīng)的是 Person 類(lèi)中最后一個(gè)方法
for (int i = personMCount - 1; i >= 0; i--) {
?
?? SEL sel = NULL;
?? IMP imp = NULL;
?
?? Method method = personMList[i];
?? NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method))
???????????????????????????????????????????? encoding:NSUTF8StringEncoding];
?? [mArr addObject:methodName];
?
?? if ([@"run" isEqualToString:methodName]) {
?????? imp = method_getImplementation(method);
?????? sel = method_getName(method);
?????? ((void (*)(id, SEL))imp)(p1, sel); // 這里的 sel 有什么用呢 ?!
?????? //break;
?? }
}
?
free(personMList);
?
其中輸出的類(lèi)方法和實(shí)例方法分別如下,顯示原方法的確可以被調(diào)用。
不過(guò)我這里有個(gè)疑問(wèn),使用 imp 時(shí)第二個(gè)參數(shù) sel 到底有什么用呢?
?
2016-09-11 11:52:44.795 category[37893:1582677]
我是原類(lèi)方法
我是跑得很快的的香港記者
2016-09-11 11:52:44.796 category[37893:1582677]
我是重寫(xiě)方法
我是跑得很快的的香港記者
2016-09-11 11:52:44.796 category[37893:1582677] (
? ? run, // 原方法
? ? run, // 重寫(xiě)方法
? ? "setClsStr:",
? ? logClsProp,
? ? clsStr
)
?
2016-09-11 11:54:14.545 category[37927:1584029]
我是原實(shí)例方法
我是(null)
2016-09-11 11:54:14.545 category[37927:1584029]
我是重寫(xiě)方法
我是會(huì)談笑風(fēng)生的(null)
2016-09-11 11:54:14.545 category[37927:1584029] (
? ? "setName:",
? ? name,
? ? ".cxx_destruct",
? ? "setOtherName:",
? ? logInstProp,
? ? tanxiaofengsheng,
? ? otherName,
? ? talk, //原方法
? ? talk ?//重寫(xiě)方法
?
?
category 可以為已有類(lèi)添加協(xié)議。
?
這里先添加一個(gè)新的 category,負(fù)責(zé)處理他談笑風(fēng)生的行為,和寫(xiě)個(gè)協(xié)議讓他上電視。
?
// Person+Delegate.h
#import "Person.h"
?
// 添加協(xié)議
@protocol PersonDelegate
?
- (void)showInTV;
?
@end
?
@interface Person (Delegate)
?
// 添加 delegate
@property (nonatomic, weak) id delegate;
?
- (void)tanxiaofengsheng;
?
@end
?
// Person+Delegate.m
#import "Person+Delegate.h"
#import
?
@implementation Person (Delegate)
?
- (id)delegate{
????return objc_getAssociatedObject(self, @selector(delegate));
}
?
- (void)setDelegate:(id)delegate{
????objc_setAssociatedObject(self, @selector(delegate), delegate, OBJC_ASSOCIATION_ASSIGN);
}
?
- (void)tanxiaofengsheng{
????for (int i = 0 ; i
?
在相應(yīng)的代理里面添加 showInTV 的方法
?
// 運(yùn)行代碼
Person *p1 = [[Person alloc] init];
p1.delegate = self;
?
// 開(kāi)始談笑風(fēng)生了
[p1 tanxiaofengsheng];
?
// ShowInTV 方法的實(shí)現(xiàn)
- (void)showInTV{
????UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 150, 150)];
????imageView.image = [UIImage imageNamed:@"naive.jpg"];
????[self.view addSubview:imageView];
}
?
這樣就利用 category 為已有類(lèi)添加了協(xié)議。
?
關(guān)于 category 的基本應(yīng)用就介紹到這里了。下面就來(lái)分享一下 category 的實(shí)踐中的使用。
?
?
Category – 實(shí)踐
?
偏方:Hook 進(jìn)某些方法來(lái)添加功能
?
一般來(lái)說(shuō),為原方法添加功能都是利用 Runtime 來(lái) Method Swizzling。不過(guò)這里也有個(gè)奇淫技巧來(lái)實(shí)現(xiàn)同樣的功能,例如我要在所有 VC 的 - (void)viewDidLoad 里面打印一個(gè)句話(huà),就可以用 category 重寫(xiě)已有類(lèi)的方法,因?yàn)?category 重寫(xiě)方法不是通過(guò)替換原方法來(lái)實(shí)現(xiàn)的,而是在原方法列表又增添一個(gè)新的同名方法,這就創(chuàng)造了機(jī)會(huì)給我們重新調(diào)用原方法了。
?
// 待 Hook 類(lèi)
// ViewController.m
// 待替換方法 無(wú)參
- (void)viewDidLoad {
????[super viewDidLoad];
????[self testForHook:@"Hello World"];
????NSLog(@"執(zhí)行原方法");
}
?
// 待替換方法 有參
- (void)testForHook:(NSString *)str1{
????NSLog(@"%@",str1);
}
?
// category 實(shí)現(xiàn)方法
// ViewController+HookOriginMethod.m
// category 重寫(xiě)原方法
- (void)viewDidLoad {
????NSLog(@"HOOK SUCCESS! \n--%@-- DidLoad !",[self class]);
????IMP imp = [self getOriginMethod:@"viewDidLoad"];
????((void (*)(id, SEL))imp)(self, @selector(viewDidLoad));
}
?
// category 重寫(xiě)原方法
- (void)testForHook:(NSString *)str1{
????NSLog(@"HOOK SUCCESS \n--%s-- 執(zhí)行",_cmd);
????IMP imp = [self getOriginMethod:@"testForHook:"];
????((void (*)(id, SEL, ...))imp)(self, @selector(testForHook:), str1);
}
?
// 獲取原方法的 IMP
- (IMP)getOriginMethod:(NSString *)originMethod{
????// 獲取 Person 的方法列表
????unsigned int methodCount;
????// 獲取實(shí)例方法
????Method *VCMethodList = class_copyMethodList([self class], &methodCount);
?
????IMP imp = NULL;
?
????// 這里是倒序獲取,所以 mArr 第一個(gè)方法對(duì)應(yīng)的是 Person 類(lèi)中最后一個(gè)方法
????for (int i = methodCount - 1; i >= 0; i--) {
?
????????Method method = VCMethodList[i];
????????NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method))
??????????????????????????????????????????????????encoding:NSUTF8StringEncoding];
?
????????if ([originMethod isEqualToString:methodName]) {
????????????imp = method_getImplementation(method);
????????????break;
????????}
????}
?
????free(VCMethodList);
????return imp;
}
?
// 執(zhí)行代碼
// ViewController.m
- (void)viewDidLoad {
????[super viewDidLoad];
????[self testForHook:@"Hello World"];
????NSLog(@"執(zhí)行原方法");
}
?
// 輸出結(jié)果
2016-09-12 23:00:15.887 category[63655:2375379] HOOK SUCCESS!?
--ViewController-- DidLoad !
2016-09-12 23:00:15.888 category[63655:2375379] HOOK SUCCESS?
--testForHook:-- 執(zhí)行
2016-09-12 23:00:15.889 category[63655:2375379] Hello World
2016-09-12 23:00:15.889 category[63655:2375379] 執(zhí)行原方法
?
查看輸出結(jié)果,可以看得出來(lái)我們的 Hook 掉 viewDidLoad 來(lái)實(shí)現(xiàn)打印成功了。
?
?
UIButton 實(shí)現(xiàn)點(diǎn)擊事件可以“傳參”。
?
一般創(chuàng)建UIButton的時(shí)候都會(huì)使用 addTarget ...這個(gè)方法來(lái)為button添加點(diǎn)擊事件,不過(guò)這個(gè)方法有個(gè)不好的地方就是無(wú)法傳自己想要的參數(shù)。例如下面代碼中聲明了str,我的意圖是點(diǎn)擊button就使控制臺(tái)或者屏幕顯示str的內(nèi)容。如果按照這樣來(lái)寫(xiě)的我想到的解決辦法就是將str設(shè)置為屬性或者成員變量,不過(guò)這樣都是比較麻煩而且不直觀的(代碼分散)。
?
NSString *str = @"hi";
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 250, 150, 100)];
button.backgroundColor = [UIColor redColor];
[button addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchDown];
[self.view addSubview:button];
?
// 點(diǎn)擊事件
- (void)click:(UIButton *)button{
????...????
}
?
我想到較好的解決辦法應(yīng)該在創(chuàng)建button,就為它設(shè)置具體的點(diǎn)擊響應(yīng)事件。實(shí)現(xiàn)方法就是為 UIButton 添加 block 屬性或者添加可傳入 block 的方法。具體代碼如下:
?
// UIButton+Category.h
#import
?
typedef void(^ActionHandlerBlock)(void);
?
@interface UIButton (Category)
?
// 點(diǎn)擊響應(yīng)的 block
@property (nonatomic, copy) ActionHandlerBlock actionHandlerBlock;
?
// 設(shè)置 UIButton 的點(diǎn)擊事件
- (void)kk_addActionHandler: (ActionHandlerBlock )actionHandlerBlock ForControlEvents:(UIControlEvents )controlEvents;
?
@end
?
// UIButton+Category.m
#import "UIButton+Category.h"
#import
?
static const void *kk_actionHandlerBlock = &kk_actionHandlerBlock;
?
@implementation UIButton (Category)
?
- (void)kk_addActionHandler:(ActionHandlerBlock)actionHandler ForControlEvents:(UIControlEvents)controlEvents{
?
????// 關(guān)聯(lián) actionHandler
????objc_setAssociatedObject(self, kk_actionHandlerBlock, actionHandler, OBJC_ASSOCIATION_COPY_NONATOMIC);
?
????// 設(shè)置點(diǎn)擊事件
????[self addTarget:self action:@selector(handleAction) forControlEvents:controlEvents];
}
?
// 處理點(diǎn)擊事件
- (void)handleAction{
?
????ActionHandlerBlock actionHandlerBlock = objc_getAssociatedObject(self, kk_actionHandlerBlock);
?
????if (actionHandlerBlock) {
????????actionHandlerBlock();
????}
}
?
- (ActionHandlerBlock)actionHandlerBlock{
????return objc_getAssociatedObject(self, @selector(actionHandlerBlock));
}
?
- (void)setActionHandlerBlock:(ActionHandlerBlock)actionHandlerBlock{
????objc_setAssociatedObject(self, @selector(actionHandlerBlock), actionHandlerBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
?
@end
?
那現(xiàn)在我們來(lái)看看調(diào)用的結(jié)果,例如我現(xiàn)在想要的點(diǎn)擊事件是 button 顏色隨機(jī)變換。
?
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 250, 150, 100)];
button.backgroundColor = [UIColor redColor];
[self.view addSubview:button];
?
// 1. 通過(guò)實(shí)例方法傳入 block 來(lái)修改??
UIButton *button2 = [[UIButton alloc] initWithFrame:CGRectMake(100, 400, 150, 100)];
button2.backgroundColor = [UIColor redColor];
[button2 kk_addActionHandler:^{
?? button.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0];
} ForControlEvents:UIControlEventTouchDown];
[self.view addSubview:button2];
?
// 2. 通過(guò)修改 block 屬性來(lái)修改
UIButton *button3 = [[UIButton alloc] initWithFrame:CGRectMake(100, 550, 150, 100)];
button3.backgroundColor = [UIColor redColor];
button3.actionHandlerBlock = ^{
?? button.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0];
};
[button3 addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button3];
?
?
// 響應(yīng)事件
- (void)click:(UIButton *)button{
????if (button.actionHandlerBlock) {
????????button.actionHandlerBlock();
????}
}
?
顯然,方法1和方法2在這個(gè)例子中實(shí)現(xiàn)的效果是相同的。不過(guò),在不同場(chǎng)合這兩個(gè)方法適用的范圍也不同。
?
直接調(diào)用實(shí)例方法傳入 block 會(huì)使代碼更加簡(jiǎn)潔和集中,但不適合 block 需要傳值的情景。
相反,設(shè)置 block 屬性要在 @selector() 中的方法中調(diào)用 block,比較麻煩,不過(guò)在需要的情況下可以傳入合適的參數(shù)。
?
p.s. 以后會(huì)繼續(xù)補(bǔ)充實(shí)踐部分。
?
最后說(shuō)一下,兩種使 category 屬性正常工作的方法:
?
因?yàn)?category 不能創(chuàng)建實(shí)例變量,那就直接使用靜態(tài)變量,如最開(kāi)始為 ohterName 和clsStr 屬性設(shè)置 setter & getter的做法。
使用objc_setAssociatedObject,其中 key 的選擇有以下幾種,個(gè)人比較喜歡第四種。
-
static char *key1; // SDWebImage & AFNetworking 中的做法,比較簡(jiǎn)單,而且 &key1 肯定唯一。key 取 &key1
-
static const char * const key2 = "key2"; // 網(wǎng)上看到的做法,指針不可變,指向內(nèi)容不可變,但是這種情況必須在賦值確保 key2 指向內(nèi)容的值是唯一。key 取 key2。
-
static const void *key3 = &key3; // 最取巧的方法,指向自己是為了不創(chuàng)建額外空間,而 const 修飾可以確保無(wú)法修改 key3 指向的內(nèi)容。key 取 key3。
-
key 取 @selector(屬性名),最方便,輸入有提示,只要你確保屬性名添加上合適的前綴就不會(huì)出問(wèn)題。
?
?
總結(jié)
以上是生活随笔為你收集整理的Category 的一些事的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 碰到日期题就怕的我来写一道水题吧
- 下一篇: php post处理,PHP处理GETP