新增實(shí)踐部分:偏方 Hook 進(jìn)某些方法來(lái)添加功能
Category - 簡(jiǎn)介
Category(類別)是 Objective-C 2.0
添加的新特性(十年前的新特性 ??)。其作用可以擴(kuò)展已有的類, 而不必通過(guò)子類化已有類,甚至也不必知道已有類的源碼,還有就是分散代碼,使已有類的體積大大減少,也利于分工合作。
在蘋(píng)果開(kāi)源項(xiàng)目中,我們可以下載相關(guān)的源碼來(lái)查看 category
的資料。
在 AFNetworking 和 SDWebImage 中也大量用到 category
來(lái)擴(kuò)展已有類和分散代碼。
關(guān)于 category
的定義可以在 objc-runtime-new.h
中找到。由其定義可以看出 category
可以正常實(shí)現(xiàn)功能有:添加實(shí)例方法、類方法、協(xié)議、實(shí)例屬性。( 在后面的實(shí)踐中,發(fā)現(xiàn)類屬性也是可以添加的 )
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 和 結(jié)合 category 工作原理分析 OC2.0 中的 runtime 。
Category - 能做什么
首先,我們先來(lái)創(chuàng)建一個(gè) Person
類以及 Person
類的 category,可以看得出 category 的文件名就是 已有類名+自定義名
。
// 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);
}
// 原類方法
+ (void)run{
NSLog(@"\n我是原類方法\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;
// 添加類屬性
@property (class, nonatomic, copy) NSString *clsStr;
// 重寫(xiě)已有類方法
+ (void)run;
- (void)talk;
// 為已有類添加方法
- (void)logInstProp;
+ (void)logClsProp;
// Person+OtherSkills.m
static NSString *_clsStr = nil;
static NSString *_otherName = nil;
@implementation Person (OtherSkills)
@dynamic otherName;
// 重寫(xiě)類方法
+ (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);
}
// 輸出類屬性
+ (void)logClsProp{
NSLog(@"\n輸出類屬性\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 可以為已有類添加實(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 可以為已有類添加類屬性。
雖然,category_t
中是沒(méi)有定義 clssProperties
,但是根據(jù)實(shí)際操作卻顯示 category 的確可以為已有類添加類屬性并且成功執(zhí)行。我個(gè)人覺(jué)得是部分源碼沒(méi)有更新或者隱藏了??,如果有知道原因的同學(xué)可以說(shuō)一下
// 運(yùn)行代碼
Person.clsStr = @"小東";
[Person logClsProp];
// 輸出結(jié)果
2016-09-11 09:45:09.936 category[37281:1509791]
輸出類屬性
我是會(huì)談笑風(fēng)生的小東
category 可以為已有類添加實(shí)例方法和類方法。
在上面的兩個(gè)例子中已經(jīng)體現(xiàn)了 category
可以為已有類添加實(shí)例方法和類方法。這里將討論加入 category
重寫(xiě)了已有類的方法會(huì)怎么樣,在創(chuàng)建的代碼中我們已經(jīng)重寫(xiě)了 run
和 talk
方法,那這時(shí)我們來(lái)調(diào)用看看。
// 運(yùn)行代碼
// 調(diào)用類方法
[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ú)論是已有類中的類方法和實(shí)例方法都可以被 category
替換到其中的重寫(xiě)方法,即使我現(xiàn)在是沒(méi)有導(dǎo)入 Person+OtherSkills.h
。這就帶來(lái)一個(gè)很嚴(yán)重的問(wèn)題,如果在 category 中不小心重寫(xiě)了已有類的方法將導(dǎo)致原方法無(wú)法正常執(zhí)行。所以使用 category
添加方法時(shí)候請(qǐng)注意是否和已有類重名了,正如 《 Effective Objective-C 2.0 》
中的第 25 條所建議的:
在給第三方類添加 category 時(shí)添加方法時(shí)記得加上你的專有前綴
然而,因?yàn)?category 重寫(xiě)方法是并不是替換掉原方法,而是往已有類中繼續(xù)添加方法,所以還是有機(jī)會(huì)去調(diào)用到原方法。這里利用 class_copyMethodList
獲取 Person
類的全部類方法和實(shí)例方法。
// 獲取 Person 的方法列表
unsigned int personMCount;
// 獲取實(shí)例方法
//Method *personMList = class_copyMethodList([Person class], &personMCount);
// 獲取類方法
Method *personMList = class_copyMethodList(object_getClass([Person class]), &personMCount);
NSMutableArray *mArr = [NSMutableArray array];
// 這里是倒序獲取,所以 mArr 第一個(gè)方法對(duì)應(yīng)的是 Person 類中最后一個(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);
其中輸出的類方法和實(shí)例方法分別如下,顯示原方法的確可以被調(diào)用。
不過(guò)我這里有個(gè)疑問(wèn),使用 imp 時(shí)第二個(gè)參數(shù) sel 到底有什么用呢?
2016-09-11 11:52:44.795 category[37893:1582677]
我是原類方法
我是跑得很快的的香港記者
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 可以為已有類添加協(xié)議。
這里先添加一個(gè)新的 category,負(fù)責(zé)處理他談笑風(fēng)生的行為,和寫(xiě)個(gè)協(xié)議讓他上電視。
// Person+Delegate.h
#import "Person.h"
// 添加協(xié)議
@protocol PersonDelegate <NSObject>
- (void)showInTV;
@end
@interface Person (Delegate)
// 添加 delegate
@property (nonatomic, weak) id<PersonDelegate> delegate;
- (void)tanxiaofengsheng;
@end
// Person+Delegate.m
#import "Person+Delegate.h"
#import <objc/runtime.h>
@implementation Person (Delegate)
- (id<PersonDelegate>)delegate{
return objc_getAssociatedObject(self, @selector(delegate));
}
- (void)setDelegate:(id<PersonDelegate>)delegate{
objc_setAssociatedObject(self, @selector(delegate), delegate, OBJC_ASSOCIATION_ASSIGN);
}
- (void)tanxiaofengsheng{
for (int i = 0 ; i < 10; i ++) {
NSLog(@"談笑風(fēng)生...");
}
// 談笑風(fēng)生完就要上電視了
if ([self.delegate respondsToSelector:@selector(showInTV)]) {
[self.delegate showInTV];
}
}
@end
在相應(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
為已有類添加了協(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è)句話,就可以用 category
重寫(xiě)已有類的方法,因?yàn)?category
重寫(xiě)方法不是通過(guò)替換原方法來(lái)實(shí)現(xiàn)的,而是在原方法列表又增添一個(gè)新的同名方法,這就創(chuàng)造了機(jī)會(huì)給我們重新調(diào)用原方法了。
// 待 Hook 類
// 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 類中最后一個(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 <UIKit/UIKit.h>
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 <objc/runtime.h>
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)題。
-