Runtime应用实践——hook黑魔法以及扩展类

需求

已知类MyClass具有私有方法methodB;要求如何在不改变MyClass源码的基础上扩展methodB方法,使其在执行methodB方法的时候具有新增功能myAction。

看着可能不太明白,这里举个例子:
IQKeyboardManager应该都不陌生,现在要求在点击Done按钮的同时执行自定义事件myAction
分析源码发现’’Done”按钮对应的方法- (void)doneAction:(IQBarButtonItem*)barButton在IQKeyboardManager.m中,对于这个私有方法貌似只能通过修改IQKeyboardManager.m源码来进行扩展了,但奈何项目是用CocoaPods进行管理的,如果直接修改三方库源码也就意味着IQKeyboardManager需要从CocoaPods管理中移除,这对于有强迫症的人来说自然是不能忍的😣😣

IQKeyboardManager.m.png

怎么办

这时就轮到Runtime上场了,借助Runtime的Method Swizzling(方法替换)以及Associated Object(关联对象),可以完美的实现以上需求,同时还可以对相关类进行扩展。
假设类MyClass如下:

MyClass.h文件

1
2
3
4
5
@interface MyClass : NSObject
- (void)methodA;

- (void)test;
@end

MyClass.m文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface MyClass ()
- (void)methodB;
@end

@implementation MyClass
- (void)methodA {
NSLog(@"MyClass methodA");
}
- (void)methodB {
NSLog(@"MyClass methodB");
}
- (void)test {
[self methodB];
}
@end

MyClass 类的共有方法:-methodA以及-test ,而调用test其实会执行私有方法-methodB

MyClass.png

要在不改变源码的基础上对- methodA以及- methodB方法进行扩展,那么可以新建类别MyClass+MyCategory,并在initialize 方法中将- methodA- methodB方法替换为自定义方法,其中initialize 会在第一次初始化这个类之前被调用,如果始终没用到则不会调用,有点类似于懒加载。
另外在MyMethodA MyMethodB 中调用了自身,那是不是会引起死循环呢?答案是不会的,因为使用method_exchangeImplementations进行了方法替换,在调用MyMethodA 时实现的是methodA 方法。

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
#import "MyClass+MyCategory.h"
#import <objc/runtime.h>

@implementation MyClass (MyCategory)

+ (void)initialize {

Method method1 = class_getInstanceMethod([self class], @selector(methodA));
Method method2 = class_getInstanceMethod([self class], @selector(MyMethodA));
method_exchangeImplementations(method1, method2);

Method method3 = class_getInstanceMethod([self class], NSSelectorFromString(@"methodB"));
Method method4 = class_getInstanceMethod([self class], @selector(MyMethodB));
method_exchangeImplementations(method3, method4);
}

- (void)MyMethodA {
[self MyMethodA];
NSLog(@"MyClass (MyCategory) methodA");
}

- (void)MyMethodB {
[self MyMethodB];
NSLog(@"MyClass (MyCategory) methodB");
}

@end

#import "MyClass+MyCategory.h" ,再次运行程序,可以看到

MyClass2.png
到这一步,我们已经可以对MyClass类的方法进行自定义扩展,你可以在自定义方法- MyMethodA- MyMethodB 中通过NSNotification或直接调用其他类的方法来触发具体的需求方法了。但这样做的话会使得MyClass类与其它类的耦合性太强,而且也不方便调试。在这里我们应该设计相关接口,以供外部调用实现它们自定义的各种需求方法。
那么继续往下看:

1
2
3
4
5
6
7
#import "MyClass.h"

@interface MyClass (MyCategory)

- (void)addTarget:(id)target action:(SEL)action parameter:(id)parameter;
- (void)removeTargetAction;
@end

MyClass+MyCategory类别设计两个方法,用来给第三方传递目标方法以及移除目标方法。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#import "MyClass+MyCategory.h"
#import <objc/runtime.h>

@interface MyClass ()

@property (nonatomic, strong) id target;
@property (nonatomic, strong) id parameter;
@property (nonatomic, copy) NSString *actionName;
@end

@implementation MyClass (MyCategory)

+ (void)initialize {

Method method1 = class_getInstanceMethod([self class], @selector(methodA));
Method method2 = class_getInstanceMethod([self class], @selector(MyMethodA));
method_exchangeImplementations(method1, method2);

Method method3 = class_getInstanceMethod([self class], NSSelectorFromString(@"methodB"));
Method method4 = class_getInstanceMethod([self class], @selector(MyMethodB));
method_exchangeImplementations(method3, method4);
}

static char targetKey;
- (void)setTarget:(id)target{
objc_setAssociatedObject(self, &targetKey, target, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)target{
return objc_getAssociatedObject(self, &targetKey);
}

static char actionNameKey;
- (void)setActionName:(NSString *)actionName {
objc_setAssociatedObject(self, &actionNameKey, actionName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)actionName{
return objc_getAssociatedObject(self, &actionNameKey);
}

static char parameterKey;
- (void)setParameter:(id)parameter {
objc_setAssociatedObject(self, &parameterKey, parameter, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)parameter {
return objc_getAssociatedObject(self, &parameterKey);
}

- (void)MyMethodA {
[self MyMethodA];
if (self.target && self.actionName && self.actionName.length > 0) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:NSSelectorFromString(self.actionName) withObject:self.parameter];
#pragma clang diagnostic pop
}
}

- (void)MyMethodB {
[self MyMethodB];
if (self.target && self.actionName && self.actionName.length > 0) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:NSSelectorFromString(self.actionName) withObject:self.parameter];
#pragma clang diagnostic pop
}
}

- (void)addTarget:(id)target action:(SEL)action parameter:(id)parameter {
self.target = target;
self.actionName = NSStringFromSelector(action);
self.parameter = parameter;
}

- (void)removeTargetAction {
self.target = nil;
self.actionName = nil;
self.parameter = nil;;
}

@end

在.m文件的实现中,使用Associated Object,MyClass 内部扩展了相应属性,并在恰当的时候触发传递进来的自定义方法。
再次运行:

MyClass3.png

可以看到,在运行MyClass - methodA- methodB方法时,正确触发了自定义方法,而且我们还可以给自定义方法传递参数。需求搞定!!

写在最后

以上便是 Runtime 在实际开发中的一些运用,其实还可以更加灵活,对于那些就算是无法看到源代码的三方库,也可以运用runtime拿到其方法列表,然后通过分析,在对应的地方替换或注入方法,以达到你想要的效果。比如前段时间很火的微信自动抢红包,就是运用了此种技术,有兴趣的可以研究下,这里就不做讨论了。