导语:Method Swizzling是Objective-C中运行时中讨论较多的内容,本文主要介绍使用Method Swizzling遇到的问题和项目中使用的Swizzling方案。
一、Method Swizzling简介
Method Swizzling的本质是在运行时交换方法实现(IMP),如hook系统方法,在原有的方法中,插入自己的业务需求。
1、Method Swizzling原理
-
Objective-C的消息机制:在 Objective-C 中调用一个方法, 实际上是在底层通过 objc_msgSend()发送一个消息。 而查找消息的唯一依据是selector的方法名。
//调用方法 [obj doSomething]; //[obj doSomething]本质上是给obj发doSomething消息 objc_msgSend(obj,@selector(doSomething))
每一个OC实例对象都保存有isa指针和实例变量,其中isa指针所属类,类维护一个运行时可接收的方法列表(MethodLists);方法列表(MethodLists)中保存selector的方法名和方法实现(IMP,指向Method实现的指针)的映射关系。在运行时,通过selecter找到匹配的IMP,从而找到的具体的实现函数。
- 开发中可以利用Objective-C的动态特性,在运行时替换selector对应的方法实现(IMP),达到给hook的目的。下图是利用Method Swizzling来替换selector对应IMP后的方法列表示意图。
2、Method Swizzling使用
Method Swizzling的本质就是偷换selector的IMP,下面就Swizzle NSObject的description方法,简单举例:
#import "NSObject+Swizzle.h"
#import <objc/runtime.h>
@implementation NSObject (Swizzle)
+ (void)load{
//调换IMP
Method originalMethod = class_getInstanceMethod([NSObject class], @selector(description));
Method myMethod = class_getInstanceMethod([NSObject class], @selector(qs_description));
method_exchangeImplementations(originalMethod, myMethod);
}
- (void)qs_description{
NSLog(@"description 被 Swizzle 了");
return [self qs_description];
}
@end
说明:调用被hook的description方法,获取内容前,会打印“description 被 Swizzle 了”这样的日志。
3、Method Swizzling存在的问题
- 不是线程安全的(Method swizzling is not atomic)
- 改变了代码本来的行为(Changes behavior of un-owned code)
- 潜在的命名冲突(Possible naming conflicts)
- 改变方法的参数(Swizzling changes the method's arguments)
- 继承问题(The order of swizzles matters)
- 难以理解 (Difficult to understand)
- 难以调试(Difficult to debug)
二、RSSwizzle:Method Swizzling的优雅方案
RSSwizzle是线程安全的Method Swizzling方案,能够帮我们解决Method Swizzling的使用问题。介绍如下:
1、不是线程安全的(Method swizzling is not atomic)
通常在 load方法中交换方法实现,如果在其他时机交换方法实现,需要考虑线程安全的问题。
RSSwizzle利用了自旋锁OSSpinLock保证线程安全。可以在任意时机交换方法实现。
2、 改变了代码本来的行为(Changes behavior of un-owned code)
这正是Swizzle的目标。但是在Swizzle方法中,我们保留*调用原始实现的好习惯,能避免绝大多数问题。我们利用Swizzle,一般是为了在原始实现基础上,添加某些自己的业务需求,并不想刻意去破坏原有实现。
RSSwizzle提供调用原来实现的宏RSSWCallOriginal,很方便。
3、潜在的命名冲突(Possible naming conflicts)#####
通常在替换的方法名前加前缀,可以很大程度上避免命名冲突冲突问题。
RSSwizzle在自定义的swizzle的静态方法完成方法替换,完全避免了命名冲突问题。
4、改变方法的参数(Swizzling changes the method's arguments)
-
参数 _cmd 被篡改,正常调用Swizzle 的方法有问题。
//调用方法 [self qs_setFrame:frame]; //发消息 objc_msgSend(self, @selector(qs_setFrame:), frame);
说明:在运行时,寻找qs_setFrame:的方法实现, _cmd参数虽然是 qs_setFrame: ,但是实际上找到的方法实现是原始的 setFrame: 实现。
RSSwizzle的自定义的swizzle的静态方法解决这个问题。
5、继承问题(The order of swizzles matters)
多个有继承关系的类的对象Swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被Swizzle的实现。
在load中Swizzle不用担心这种问题,因为load类方法会默认从父类开始调用。
6、难以理解 (Difficult to understand)
主要表现在调用原始实现,看起来像递归,有点懵。
RSSwizzle提供的宏RSSWCallOriginal让调用原始实现更容易,代码阅读性更强。
7、难以调试(Difficult to debug)
Debug时候打印出的backtrace(回溯),其中掺杂着被swizzle的方法名,看起来比较乱,所以命名清晰很重要;
RSSwizzle打印出来的命名很清晰,此外Swizzle了什么,最好有文档记录。
三、RSSwizzle的基础使用
RSSwizzle中提供了两种使用方式,一种是通过调用类方法来实现函数的替换,另一种是使用RSSwizzle定义的宏来进行函数的替换。
1、 使用类方法替换实例方法实现
/**
参数1:要被替换的函数选择器
参数2:要被替换的函数所在的类
参数3: block中返回替换后的方法,block参数中需要返回一个方法函数,这个函数为要替换成的函数,要和原函数类型相同。在类中的函数默认都会有一个名为self的id参数
参数4:此次替换用到的key
*/
[RSSwizzle swizzleInstanceMethod:@selector(touchesBegan:withEvent:) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) {
return ^(__unsafe_unretained id self,NSSet* touches,UIEvent* event){
NSLog(@"touchesBegan:withEvent:被Swizzle了");
};
} mode:RSSwizzleModeAlways key:NULL];
2、 使用宏替换实例方法实现
/*
参数1:要被替换的函数所在的类
参数2: 要被替换的函数选择器
参数3:返回值类型,
参数4:参数列表
参数5:要替换的代码块,
参数6:执行模式,
参数7:key值标识,RSSwizzleModeOncePerClass模式下使用,其他情况置为NULL
*/
RSSwizzleInstanceMethod([ViewController class], @selector(touchesEnded:withEvent:), RSSWReturnType(void), RSSWArguments(NSSet<UITouch *> *touches,UIEvent *event),RSSWReplacement({
NSLog(@"touchesEnded:withEvent被Swizzle了");
RSSWCallOriginal(touches,event);
}), RSSwizzleModeAlways, NULL);
3、 使用类方法替换类方法实现
/*
参数1:要替换的函数选择器
参数2:要替换此函数的类
参数3:block中返回替换后的方法,block参数中需要返回一个方法函数,这个函数为要替换成的函数,要和原函数类型相同。在类中的函数默认都会有一个名为self的id参数
*/
[RSSwizzle swizzleClassMethod:@selector(testClassMethod1) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) {
return ^(__unsafe_unretained id self){
NSLog(@"Class testClassMethod1 Swizzle");
};
}];
4、使用宏替换类方法实现
/*
参数1:要替换方法的类
参数2:要替换的方法选择器
参数3:方法的返回值类型
参数4:方法的参数列表
参数5:要替换的方法代码块
*/
RSSwizzleClassMethod(NSClassFromString(@"ViewController"), NSSelectorFromString(@"testClassMethod2"), RSSWReturnType(void), RSSWArguments(), RSSWReplacement({
//先执行原始方法
RSSWCallOriginal();
NSLog(@"Class testClassMethod2 Swizzle");
}));
说明:RSSwizzle还提供了Swizzle模式,使用Swizzle实例方法时候需要用到。Swizzle类方法,默认RSSwizzleModeAlways,定义如下:
typedef NS_ENUM(NSUInteger, RSSwizzleMode) {
//任何情况下 始终执行替换操作
RSSwizzleModeAlways = 0,
//相同key标识的替换操作只会被执行一次
RSSwizzleModeOncePerClass = 1,
//相同key标识的替换操作在子类父类中只会被执行一次
RSSwizzleModeOncePerClassAndSuperclasses = 2
};
四、一个使用Swizzling典型的错误案例
网络上很多博客介绍了使用Swizzling来防止重复点击UIButton,但是大部分都会有问题。
1、错误代码
一般在load中替换sendAction:to:forEvent:方法,主要代码如下:
+ (void)load {
Method before = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method after = class_getInstanceMethod(self, @selector(qs_sendAction:to:forEvent:));
method_exchangeImplementations(before, after);
}
- (void)qs_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
if ([NSDate date].timeIntervalSince1970 - self.qs_acceptEventTime < self.qs_acceptEventInterval) {
return;
}
if (self.qs_acceptEventInterval > 0) {
self.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
}
[self qs_sendAction:action to:target forEvent:event];
}
错误现象:
点击UITabBar上按钮会crash, 提示类似于:[UITabBarButton qs_acceptEventTime]: unrecognized selector sent to instance ...。
错误原因:
1)UITabBarButton是UITabBarController中各个子控制器在工具条中对应的按钮,是UITabBar的私有属性,UITabBarButton的父类是UIControl,而UIButton的父类也是UIControl,sendAction:to:forEvent:是UIControl的实例方法;
2) 在UIButton类中没有sendAction:to:forEvent:这个方法实现,通过class_getInstanceMethod() 获取的是父类的 Method 对象,使用 method_exchangeImplementations() 就把父类的原始实现(IMP)跟自己的 Swizzle 实现交换了。这就导致UIControl的其他子类,如UITabBarButton在被点击后,都调用了UIButton的Swizzle 实现,发生了严重的Crash问题。
说明:虽然在UIControl的分类的load方法交换方法实现,能解决问题,我们将Swizzling的影响扩大很多倍,不是理想的做法。下面介绍解决办法。
2、解决办法
在项目直接使用method_exchangeImplementations很危险,甚至导致Crash,在项目中不建议这么做。可采用的解决办法有两种:
方法A
原理:如果类中没有实现 Original Selector 对应的方法,那就通过class_addMethod方法为Original Selector增加Swizzle 的实现,通过class_replaceMethod修改Swizzle Selector 的 实现 为 Original 的实现;如果已经有Original Selector 对应的方法(通过class_addMethod方法添加是失败的), 这时才使用method_exchangeImplementations来直接交换。
代码如下:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizzledSelector = @selector(qs_sendAction:to:forEvent:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
说明1:class_addMethod方法可以为类添加新的方法实现(IMP),添加成功返回YES.。否则返回NO。如果选择器(select)已经有对应的方法实现(IMP), 添加也是失败的,利用这点可以检查是否有源方法实现,如果没有利用class_replaceMethod来将swizzledSelector和originalMethod对应设置好。
说明2:.class_replaceMethod用来替换类中的方法实现,会调用class_addMethod和method_setImplementation方法(直接设置某个方法的IMP)
方法B
原理:RSSwizzle完美避开了在load中使用method_exchangeImplementations交换方法的尴尬,基于Swizzle模式和class_replaceMethod完美控制了替换方法实现。
代码如下:
+ (void)load{
RSSwizzleInstanceMethod([UIButton class], @selector(sendAction:to:forEvent:), RSSWReturnType(void), RSSWArguments(SEL action,id target,UIEvent *event), RSSWReplacement({
UIButton *btn = self;
if ([NSDate date].timeIntervalSince1970 - btn.qs_acceptEventTime < btn.qs_acceptEventInterval) {
return;
}
if (btn.qs_acceptEventInterval > 0) {
btn.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
}
RSSWCallOriginal(action,target,event);
}), RSSwizzleModeAlways, NULL);
}
说明:RSSwizzleInstanceMethod宏实现方法实现的替换,代码更易阅读。
End
Demo地址
QSSwizzleKitDemo参考资料
Objective-C的hook方案(一): Method Swizzling
Objective-C Method Swizzling我是南华coder,一名北漂的初级iOS程序猿。iOS札记是我的一点学习笔记,不足之处,望批评指正。