新增實踐部分:偏方 Hook 進某些方法來新增功能
Category – 簡介
Category(類別)是 Objective-C 2.0
新增的新特性(十年前的新特性 ?)。其作用可以擴充套件已有的類, 而不必通過子類化已有類,甚至也不必知道已有類的原始碼,還有就是分散程式碼,使已有類的體積大大減少,也利於分工合作。
在蘋果開源專案中,我們可以下載相關的原始碼來檢視 category
的資料。
在 AFNetworking 和 SDWebImage 中也大量用到 category
來擴充套件已有類和分散程式碼。
關於 category
的定義可以在 objc-runtime-new.h
中找到。由其定義可以看出 category
可以正常實現功能有:新增例項方法、類方法、協議、例項屬性。( 在後面的實踐中,發現類屬性也是可以新增的 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 和 結合 category 工作原理分析 OC2.0 中的 runtime 。
Category – 能做什麼
首先,我們先來建立一個 Person
類以及 Person
類的 category,可以看得出 category 的檔名就是 已有類名+自定義名
。
1 2 3 4 5 6 7 8 9 |
// Person.h @interface Person : NSObject @property (nonatomic, copy) NSString *name; + (void)run; - (void)talk; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Person.m @implementation Person // 原例項方法 - (void)talk{ NSLog(@"\n我是原例項方法\n我是%@",self.name); } // 原類方法 + (void)run{ NSLog(@"\n我是原類方法\n我是跑得很快的的香港記者"); } @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// 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
的屬性。可以出來能正常工作。
1 2 3 4 5 6 7 8 9 |
// 執行程式碼 Person *p1 = [[Person alloc] init]; // 例項屬性 p1.otherName = @"小花"; [p1 logInstProp]; p1.otherName = @"小明"; [p1 logInstProp]; |
1 2 3 4 5 6 7 |
// 輸出結果 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 的確可以為已有類新增類屬性並且成功執行。我個人覺得是部分原始碼沒有更新或者隱藏了?,如果有知道原因的同學可以說一下
1 2 3 |
// 執行程式碼 Person.clsStr = @"小東"; [Person logClsProp]; |
1 2 3 4 |
// 輸出結果 2016-09-11 09:45:09.936 category[37281:1509791] 輸出類屬性 我是會談笑風生的小東 |
category 可以為已有類新增例項方法和類方法。
在上面的兩個例子中已經體現了 category
可以為已有類新增例項方法和類方法。這裡將討論加入 category
重寫了已有類的方法會怎麼樣,在建立的程式碼中我們已經重寫了 run
和 talk
方法,那這時我們來呼叫看看。
1 2 3 4 5 6 |
// 執行程式碼 // 呼叫類方法 [Person run]; // 呼叫例項方法 Person *p1 = [[Person alloc] init]; [p1 talk]; |
1 2 3 4 5 6 7 |
// 輸出結果 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
類的全部類方法和例項方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 獲取 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 到底有什麼用呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 ) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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,負責處理他談笑風生的行為,和寫個協議讓他上電視。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Person+Delegate.h #import "Person.h" // 新增協議 @protocol PersonDelegate - (void)showInTV; @end @interface Person (Delegate) // 新增 delegate @property (nonatomic, weak) id delegate; - (void)tanxiaofengsheng; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 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
的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 執行程式碼 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
重寫方法不是通過替換原方法來實現的,而是在原方法列表又增添一個新的同名方法,這就創造了機會給我們重新呼叫原方法了。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 待 Hook 類 // ViewController.m // 待替換方法 無參 - (void)viewDidLoad { [super viewDidLoad]; [self testForHook:@"Hello World"]; NSLog(@"執行原方法"); } // 待替換方法 有參 - (void)testForHook:(NSString *)str1{ NSLog(@"%@",str1); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// 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; } |
1 2 3 4 5 6 7 |
// 執行程式碼 // ViewController.m - (void)viewDidLoad { [super viewDidLoad]; [self testForHook:@"Hello World"]; NSLog(@"執行原方法"); } |
1 2 3 4 5 6 7 |
// 輸出結果 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
設定為屬性或者成員變數,不過這樣都是比較麻煩而且不直觀的(程式碼分散)。
1 2 3 4 5 6 7 8 9 10 |
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
的方法。具體程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// 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 顏色隨機變換。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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 取 &key1static const char * const key2 = "key2";
// 網上看到的做法,指標不可變,指向內容不可變,但是這種情況必須在賦值確保 key2 指向內容的值是唯一。key 取 key2。static const void *key3 = &key3;
// 最取巧的方法,指向自己是為了不建立額外空間,而 const 修飾可以確保無法修改 key3 指向的內容。key 取 key3。- key 取
@selector(屬性名)
,最方便,輸入有提示,只要你確保屬性名新增上合適的字首就不會出問題。