需求
已知类MyClass具有私有方法methodB;要求如何在不改变MyClass源码的基础上扩展methodB方法,使其在执行methodB方法的时候具有新增功能myAction。
看着可能不太明白,这里举个例子:
IQKeyboardManager
应该都不陌生,现在要求在点击Done
按钮的同时执行自定义事件myAction
。
分析源码发现''Done"按钮对应的方法- (void)doneAction:(IQBarButtonItem*)barButton
在IQKeyboardManager.m中,对于这个私有方法貌似只能通过修改IQKeyboardManager.m源码来进行扩展了,但奈何项目是用CocoaPods进行管理的,如果直接修改三方库源码也就意味着IQKeyboardManager
需要从CocoaPods管理中移除,这对于有强迫症的人来说自然是不能忍的😣😣
怎么办
这时就轮到Runtime上场了,借助Runtime的Method Swizzling(方法替换)以及Associated Object(关联对象),可以完美的实现以上需求,同时还可以对相关类进行扩展。
假设类MyClass
如下:
MyClass.h
文件
@interface MyClass : NSObject
- (void)methodA;
- (void)test;
@end
MyClass.m
文件
@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
要在不改变源码的基础上对- methodA
以及- methodB
方法进行扩展,那么可以新建类别MyClass+MyCategory
,并在initialize
方法中将- methodA
与- methodB
方法替换为自定义方法,其中initialize
会在第一次初始化这个类之前被调用,如果始终没用到则不会调用,有点类似于懒加载。
另外在MyMethodA
与MyMethodB
中调用了自身,那是不是会引起死循环呢?答案是不会的,因为使用method_exchangeImplementations进行了方法替换,在调用MyMethodA
时实现的是methodA
方法。
#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"
,再次运行程序,可以看到
到这一步,我们已经可以对MyClass类的方法进行自定义扩展,你可以在自定义方法
- MyMethodA
、 - MyMethodB
中通过NSNotification或直接调用其他类的方法来触发具体的需求方法了。但这样做的话会使得MyClass
类与其它类的耦合性太强,而且也不方便调试。在这里我们应该设计相关接口,以供外部调用实现它们自定义的各种需求方法。那么继续往下看:
#import "MyClass.h"
@interface MyClass (MyCategory)
- (void)addTarget:(id)target action:(SEL)action parameter:(id)parameter;
- (void)removeTargetAction;
@end
给MyClass+MyCategory
类别设计两个方法,用来给第三方传递目标方法以及移除目标方法。
#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, ¶meterKey, parameter, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)parameter {
return objc_getAssociatedObject(self, ¶meterKey);
}
- (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
内部扩展了相应属性,并在恰当的时候触发传递进来的自定义方法。
再次运行:
可以看到,在运行MyClass
的- methodA
与- methodB
方法时,正确触发了自定义方法,而且我们还可以给自定义方法传递参数。需求搞定!!
写在最后
以上便是Runtime在实际开发中的一些运用,其实还可以更加灵活,对于那些就算是无法看到源代码的三方库,也可以运用runtime拿到其方法列表,然后通过分析,在对应的地方替换或注入方法,以达到你想要的效果。比如前段时间很火的微信自动抢红包,就是运用了此种技术,有兴趣的可以研究下,这里就不做讨论了。