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