方法交互的例子
在实例项目开发中,经常需要通过runtime 来给类的类方法或实例方法做交换,从而达到不用修改原类的代码就可以给原类中特定的方法做替换操作。虽然这只是runtime 的其中一个功能,但可以用来做很多事情,例如用在一些统计业务中,不用在每个类中都写一遍,而可以直接通过黑魔法来交换方法,把统计业务写在自定义的方法中,可以神不知鬼不觉得达到我们想要的结果,此处还有一个好处,就是可以将所有的统计业务代码写在同一处,方便管理。
在ViewController 中,覆盖 - viewWillAppear:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
}
此时张三创建了一个基于UIViewController 的Category,名为
UIViewController+LogCollect
@interface UIViewController (LogCollect)
- (void)logCollect_viewWillAppear:(BOOL)animated;
@end
@implementation UIViewController (LogCollect)
- (void)logCollect_viewWillAppear:(BOOL)animated {
[self logCollect_viewWillAppear:animated];
}
@end
然后在AppDelegate中提供一个交换方法的触发场景:
#import "AppDelegate.h"
#import <objc/runtime.h>
#import "UIViewController+LogCollect.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self exchangeViewWillAppear1];
return YES;
}
- (void)exchangeViewWillAppear1 {
Method originalMethod = class_getClassMethod([UIViewController class], @selector(viewWillAppear:));
Method swizzedMethod = class_getClassMethod([UIViewController class], @selector(logCollect_viewWillAppear:));
method_exchangeImplementations(originalMethod, swizzedMethod);
}
运行结果如下
logCollect_viewWillAppear
origin viewWillAppear
分析
当程序运行起来的时候,先通过AppDelegate 的代理方法—didFinishLaunchingWithOptions: 方法,表示程序已经完成启动,其中调用了交换方法,
首选 runtime 函数 class_getInstanceMethod 获取到UIViewController 系统自带的实例方法viewWillAppear: ,同理也获取到了LogCollect 提供的logCollect_viewWillAppear: 方法。最后用method_exchangeImplementations 来实现两个方法的交换。
这里着重看一下UIViewController+LogCollect 中的logCollect_viewWillAppear: 的实现
里面又调用了一次本方法,有人认为调用改方法会造成死循环,其实并非如此,因为通过方法交换,logCollect_willAppear: 中还调用了logCollect_willAppear: 方法,其实调用的已经是被替换了的原系统的willAppear: 方法实现了,所以并不会造成死循环。
在看一下完整的顺序
1.ViewController 要出现在屏幕上时,系统自动调用了willAppear: 方法,此时方法已经被替换所以实际调用的是logCollect_viewWillAppear: 的实现,
2.而logCollect_viewWillAppear: 的实现 又调用了logCollect_willAppear:方法,
3.此时logCollect_viewWillAppear:的实现是被替换成了系统的willAppear:的方法
虽然看起来很混乱,其实并不会复杂,从整体来看,相当于我们在系统方法-willAppear:调用的同时,顺带执行了一段自定义的代码,不影原来方法的使用。当然我们也可以在替换方法中不去调用原来的方法,这样就达到了一个完全替换的效果。这用在非系统方法中没有问题,但对于willAppear: 这样的系统方法,如果不去调用其实现,将很可能造成许多令人讨厌的麻烦。
黑魔法坑点
1.多个类对animation_viewWillAppear交换
以上代码其实有一个问题,我们在logCollect_viewWillAppear: 中调用了原来的实现,然而在这个时候我们又有一个开发者Animation,在Animation的代码中,同样需要在UIViewController 的willAppear: 方法中进行交换,
代码如下:
@interface UIViewController (Animation)
- (void)animation_viewWillAppear:(BOOL)animated;
@end
@implementation UIViewController (Animation)
- (void)animation_viewWillAppear:(BOOL)animated {
[self animation_viewWillAppear:animated];
}
#import "AppDelegate.h"
#import <objc/runtime.h>
#import "UIViewController+LogCollect.h"
#import "UIViewController+Animation.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self exchangeViewWillAppear1];
[self exchangeViewWillAppear2];
return YES;
}
- (void)exchangeViewWillAppear1 {
Method originalMethod = class_getClassMethod([UIViewController class], @selector(viewWillAppear:));
Method swizzedMethod = class_getClassMethod([UIViewController class], @selector(logCollect_viewwillAppear:));
method_exchangeImplementations(originalMethod, swizzedMethod);
}
- (void)exchangeViewWillAppear2 {
Method originalMethod = class_getClassMethod([UIViewController class], @selector(viewWillAppear:));
Method swizzedMethod = class_getClassMethod([UIViewController class], @selector(animation_viewWillAppear:));
method_exchangeImplementations(originalMethod, swizzedMethod);
}
打印结果
logCollect_viewWillAppear
animation_viewWillAppear
origin viewWillAppear
可以看出先打印logCollect,animation,origin
分析过程,首先logCollect来交换方法,然后animation交换方法,那么animation 交换的是原来的viewWillAppear,还是logCollect_viewWillAppear 呢???
在一开始l只有ogCollect交换方法的时候,打印顺序是logCollect->origin
,然后animation 也交换的时候logCollect->animation ->origin,可以猜测animation交换的方法应该是origin,
交换顺序
交换前的关系
首先log进行了交换后的关系
从图中可以看出
@selector(viewWillAppear)对应的变成了Implementation已经变成了logCollect_viewWillAppear,
而logCollect_viewWillAppear也换做了viewWillAppear。
在重新观察animation
对于黑魔法,笔者的建议是要慎用!之所以成为黑魔法,有一些不可思议,
黑魔法的使用场景一般是用在比较大的范围,所以一旦使用不当出现问题,则是大规模的,其中就包括此例中多个交换方法造成不同调用顺序甚至不起作用所产生的一系列问题。
2.子类没有实现父类实现
场景子类没有实现父类的那个方法,子类的分类进行方法交互
父类Animation
// .h 文件
@interface Animation : NSObject
- (void)eat;
@end
//.m 文件
#import "Animation.h"
@implementation Animation
- (void)eat {
}
@end
子类Monkey
// .h 文件
#import "Animation.h"
NS_ASSUME_NONNULL_BEGIN
@interface Monkey : Animation
@end
NS_ASSUME_NONNULL_END
// .m 文件
#import "Monkey.h"
@implementation Monkey
@end
分类Monkey+zm进行方法交互
#import "Monkey+zm.h"
#import <objc/runtime.h>
@implementation Monkey (zm)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(eat));
Method swizzedMethod = class_getInstanceMethod(self, @selector(zm_eat));
if (originalMethod != NULL && swizzedMethod != NULL)
{
method_exchangeImplementations(originalMethod, swizzedMethod);
}
});
}
调用
Monkey *m = [[Monkey alloc]init];
[m eat];
Animation *a = [[Animation alloc]init];
[a eat];
崩溃
-[Animation zm_eat]: unrecognized selector sent to instance 0x600000eb0220
解析
[m eat] 它不报错,是因为Monkey中的imp交换成了zm_eat,而Monkey+zm在LG分类中有这个方法,所以不会报错
[a eat] 它崩溃了
其本质原因:Monkey的分类Monkey+zm中进行了方法交换,将Animation中imp 交换成了Monkey中的zm_eat,然后需要去 Animation中的找eat,但是Animation中没有eat方法,即相关的imp找不到,所以就崩溃了
优化:避免父类imp找不到
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL oriSEL = @selector(eat);
SEL swizzledSEL = @selector(zm_eat);
Method originalMethod = class_getInstanceMethod(self,oriSEL);
Method swizzedMethod = class_getInstanceMethod(self,swizzledSEL);
if (originalMethod != NULL && swizzedMethod != NULL)
{
// 此时class_addMethod会添加(名字为oriSEL,实现为swizzedMethod)的方法
BOOL success = class_addMethod(self, oriSEL, method_getImplementation(swizzedMethod), method_getTypeEncoding(originalMethod));
if (success) {
// 自己没有
// 此时在将swizzedMethod的实现替换为originalMethod的实现即可。
class_replaceMethod(self,swizzledSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
// 自己有交换
method_exchangeImplementations(originalMethod, swizzedMethod);
}
}
});
}
在上面因为子类没有实现- (void)eat 方法,所以把父类的- (void)eat 方法交换成立zm_eat方法,父类在调用时找不到- (void)eat 这个方法了,所以往子类添加- (void)eat 方法,这样就不会交换父类的了。
原始如图所示
执行完class_addMethod
// 此时class_addMethod会添加(名字为oriSEL,实现为swizzedMethod)的方法
BOOL success = class_addMethod(self, oriSEL, method_getImplementation(swizzedMethod),
method_getTypeEncoding(originalMethod));
执行完class_replaceMethod
// 此时在将swizzedMethod的实现替换为originalMethod的实现即可。
class_replaceMethod(self,swizzledSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
3. 子类没有实现,父类也没有实现,下面的调用有什么问题?
父类调用eat 方法没有实现
#import "Animation.h"
@implementation Animation
//- (void)eat {
//
//}
@end
报错
递归死循环解析
原因是 栈溢出,递归死循环了
(void)zm_eat{
[self zm_eat];
}l
- (void)eat没有实现,然后在方法交换时,始终都找不到oriMethod,然后交换了寂寞,即交换失败,当我们调用- zm_eat然后导致了自己调自己,即递归死循环。
优化:避免递归死循环
如果oriMethod为空,为了避免方法交换没有意义,而被废弃,需要做一些事情
通过class_addMethod给oriSEL添加swiMethod方法
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL oriSEL = @selector(eat);
SEL swizzledSEL = @selector(zm_eat);
Method originalMethod = class_getInstanceMethod(self,oriSEL);
Method swizzedMethod = class_getInstanceMethod(self,swizzledSEL);
// 当原始方法没有时
if (!originalMethod) {
//此时class_addMethod会添加 (会添加名字为oriSEL实现为swizzedMethod)的方法
class_addMethod(self,oriSEL, method_getImplementation(swizzedMethod), method_getTypeEncoding(swizzedMethod));
method_setImplementation(swizzedMethod, imp_implementationWithBlock(^(id self){
NSLog(@"空 imp");
}));
}
// 此时class_addMethod会添加(名字为oriSEL,实现为swizzedMethod)的方法
BOOL success = class_addMethod(self, oriSEL, method_getImplementation(swizzedMethod), method_getTypeEncoding(swizzedMethod));
if (success) {
// 自己没有
// 此时在将swizzedMethod的实现替换为originalMethod的实现即可。
class_replaceMethod(self,swizzledSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
// 自己有交换
method_exchangeImplementations(originalMethod, swizzedMethod);
}
});
}
没有添加之前
添加方法名为eat,实现为zm_eat之后
// 当原始方法没有时
if (!originalMethod) {
//此时class_addMethod会添加 (会添加名字为oriSEL实现为swizzedMethod)的方法
class_addMethod(self,oriSEL, method_getImplementation(swizzedMethod), method_getTypeEncoding(swizzedMethod));
method_setImplementation(swizzedMethod, imp_implementationWithBlock(^(id self){
NSLog(@"空 imp");
}));
}
方法交换之后
method_exchangeImplementations(originalMethod, swizzedMethod);
小结
交换方法虽然特定情况下开发带来的便利,但同时也存在一些“侵入性”,可能影响其他类的代码。因此不要为了使用而使用,应尽量将影响范围控制在期望中。