新增实践部分:偏方 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 和 结合 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 <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(@"谈笑风生..."); } // 谈笑风生完就要上电视了 if ([self.delegate respondsToSelector:@selector(showInTV)]) { [self.delegate showInTV]; } } @end
|
在相应的代理里面添加 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
的时候都会使用 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 <UIKit/UIKit.h> 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 <objc/runtime.h> 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(属性名)
,最方便,输入有提示,只要你确保属性名添加上合适的前缀就不会出问题。
感谢您的阅读,如有错误,敬请指出。