相信你们对于Method Swizzling并不陌生,在平时的开发中多多少少都有些使用,这也是Runtime的开发应用中比较普遍的用法。可是它确确实实是个黑魔法,一有不慎,就是一座天坑。本章就来研究一下它,让他变成白魔法编程
Method Swizzling
(方法交换),顾名思义,就是将两个方法的实现交换,即由原来的A-AImp
、B-BImp
对应关系变成了A-BImp
、B-AImp
。设计模式
每一个类都维护一个方法Method
列表,Method
则包含SEL
和其对应IMP
的信息,方法交换作的事情就是把SEL
和IMP
的对应关系断开,并和新的IMP
生成对应关系。数组
Method Swizzing
是发生在运行时的,主要用于在运行时将两个Method进行交换,咱们能够将Method Swizzling
代码写到任何地方,可是只有在这段Method Swilzzling
代码执行完毕以后互换才起做用。bash
Method Swizzling
是OC动态性的最好诠释,深刻地去学习并理解其特性,将有助于咱们在业务量不断增大的同时还能保持代码的低耦合度,下降维护的工做量和难度。服务器
能够用图来更好的解释一下app
交换前: 框架
//获取经过SEL获取一个方法
class_getInstanceMethod
复制代码
//获取一个方法的实现
method_getImplementation
复制代码
//获取一个OC实现的编码类型
method_getTypeEncoding
复制代码
//給方法添加实现
class_addMethod
复制代码
//用一个方法的实现替换另外一个方法的实现
class_replaceMethod
复制代码
//交换两个方法的实现
method_exchangeImplementations
复制代码
+load
方法中实现,这样能够保证方法必定会调用且不会出现异常。dispatch_once
来执行方法交换,这样能够保证只运行一次。第一个坑点比较简单,就是咱们在load
中交换完方法后,不作处理的话,若是再去调用load
,方法IMP
会又被交换回来,致使交换不成功。函数
解决的方法也比较简单,在上述的注意事项1中已经说过,使用单例模式来交换方法,保证方法的交换只执行一次post
咱们能够经过一个例子来实现,建立父类LGPerson
,子类LGStudent
和分类LGStudent+LG
来进行方法的交换。学习
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LGPerson
- (void)personInstanceMethod{
NSLog(@"person对象方法:%s",__func__);
}
@end
**************************************************************
@interface LGStudent : LGPerson
@end
@implementation LGStudent
@end
**************************************************************
复制代码
首先进行普通的方法交换,并在VC里面正常调用,根据打印结果,能够发现咱们在子类交换了父类的方法后,没有产生崩溃,而且子类的分类中交换的方法也正常执行了
@implementation LGStudent (LG)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
- (void)lg_studentInstanceMethod{
[self lg_studentInstanceMethod];
NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
}
@end
*******************************调用*******************************
- (void)viewDidLoad {
[super viewDidLoad];
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
}
*******************************打印结果*******************************
2020-01-20 10:45:31.809408+0800 006---Method-Swizzling坑[81429:20470219] person对象方法:-[LGPerson personInstanceMethod]
2020-01-20 10:45:31.809568+0800 006---Method-Swizzling坑[81429:20470219] LGStudent分类添加的lg对象方法:-[LGStudent(LG) lg_studentInstanceMethod]
复制代码
可是,若是咱们在调用的时候,父类自己再调用一下这个方法的话,就会出现崩溃,缘由也比较清楚,就是子类将此方法交换了,父类并无交换后方法的IMP,因此会出现找不到方法的崩溃
- (void)viewDidLoad {
[super viewDidLoad];
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
}
复制代码
一句话总结就是:若是该方法本身没有,则先给本身添加要交换的方法。以后再父类原方法IMP指向交换的方法
在交换方法的时候,先尝试添加一下原方法到类中,并将IMP指向交换的方法
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
// oriSEL personInstanceMethod
// swizzledSEL lg_studentInstanceMethod
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 尝试添加
// ✅对应关系:personInstanceMethod(sel) - lg_studentInstanceMethod(imp)
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {// 本身没有 - 交换 - 没有父类进行处理 (重写一个)
//✅lg_studentInstanceMethod (swizzledSEL) - personInstanceMethod(imp)
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{ // 本身有
method_exchangeImplementations(oriMethod, swiMethod);
}
}
复制代码
仍是使用上述例子,若是LGStudent
有一个方法- (void)helloword
只有生命,没有实现。
就算咱们使用了上述的解决方法,添加了方法,可是因为原方法找不到,为nil。因此会形成死循环调用
咱们能够经过判断原方法是否存在,并添加一个一个空实现来解决这个坑点。以后再进行判断原方法进行交换,这样就能完美解决了
具体代码以下
+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {
✅// 在oriMethod为nil时,替换后将swizzledSEL复制一个不作任何事的空实现,代码以下:
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
✅// 通常交换方法: 交换本身有的方法 -- 走下面 由于本身有意味添加方法失败
✅// 交换本身没有实现的方法:
✅// 首先第一步:会先尝试给本身添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
✅// 而后再将父类的IMP给swizzle personInstanceMethod(imp) -> swizzledSEL
//oriSEL:personInstanceMethod
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
}
复制代码
如下swizzling方法的具体封装,和上述代码中同样
在 iOS 开发中最多见的三种埋点,就是对页面进入次数、页面停留时间、点击事件的埋点。这些均可以经过Method Swizzling来实现。
下面的例子中,咱们经过交换UIViewController
中viewWillAppear
和viewWillDisappear
的方法,来实现了进入界面和退出界面的统计,并记录了相关的类名,经过映射的关系,就能够清楚的知道用户的行为了
@implementation UIViewController (logger)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
✅// 经过 @selector 得到被替换和替换方法的 SEL,做为 SMHook:hookClass:fromeSelector:toSelector 的参数传入
SEL fromSelectorAppear = @selector(viewWillAppear:);
SEL toSelectorAppear = @selector(hook_viewWillAppear:);
[SMHook hookClass:self fromSelector:fromSelectorAppear toSelector:toSelectorAppear];
SEL fromSelectorDisappear = @selector(viewWillDisappear:);
SEL toSelectorDisappear = @selector(hook_viewWillDisappear:);
[SMHook hookClass:self fromSelector:fromSelectorDisappear toSelector:toSelectorDisappear];
});
}
- (void)hook_viewWillAppear:(BOOL)animated {
✅// 先执行插入代码,再执行原 viewWillAppear 方法
[self insertToViewWillAppear];
[self hook_viewWillAppear:animated];
}
- (void)hook_viewWillDisappear:(BOOL)animated {
✅// 执行插入代码,再执行原 viewWillDisappear 方法
[self insertToViewWillDisappear];
[self hook_viewWillDisappear:animated];
}
- (void)insertToViewWillAppear {
✅// 在 ViewWillAppear 时进行日志的埋点
[[[[SMLogger create]
message:[NSString stringWithFormat:@"%@ Appear",NSStringFromClass([self class])]]
classify:ProjectClassifyOperation]
save];
}
- (void)insertToViewWillDisappear {
✅// 在 ViewWillDisappear 时进行日志的埋点
[[[[SMLogger create]
message:[NSString stringWithFormat:@"%@ Disappear",NSStringFromClass([self class])]]
classify:ProjectClassifyOperation]
save];
}
@end
复制代码
那么点击方法,咱们也能够经过运行时方法替换的方式进行无侵入埋点。
这里最主要的工做是,找到这个点击事件的方法 sendAction:to:forEvent:
,而后在 +load()
方法替换成为你定义的方法。完整代码实现以下:
和 UIViewController
生命周期埋点不一样的是,UIButton
在一个视图类中可能有多个不一样的继承类,相同 UIButton
的子类在不一样视图类的埋点也要区别开。因此,咱们须要经过 “action 选择器名 NSStringFromSelector(action)” +“视图类名 NSStringFromClass([target class])”
组合成一个惟一的标识,来进行埋点记录
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
✅// 经过 @selector 得到被替换和替换方法的 SEL,做为 SMHook:hookClass:fromeSelector:toSelector 的参数传入
SEL fromSelector = @selector(sendAction:to:forEvent:);
SEL toSelector = @selector(hook_sendAction:to:forEvent:);
[SMHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
});
}
- (void)hook_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[self insertToSendAction:action to:target forEvent:event];
[self hook_sendAction:action to:target forEvent:event];
}
- (void)insertToSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
✅// 日志记录
if ([[[event allTouches] anyObject] phase] == UITouchPhaseEnded) {
NSString *actionString = NSStringFromSelector(action);
NSString *targetName = NSStringFromClass([target class]);
[[[SMLogger create] message:[NSString stringWithFormat:@"%@ %@",targetName,actionString]] save];
}
}
复制代码
除了 UIViewController
、UIButton
控件之外,Cocoa 框架的其余控件均可以使用这种方法来进行无侵入埋点。以 Cocoa 框架中最复杂的 UITableView
控件为例,你可使用 hook setDelegate
方法来实现无侵入埋点。另外,对于 Cocoa 框架中的手势事件(Gesture Event
),咱们也能够经过 hook initWithTarget:action:
方法来实现无侵入埋点。
这个例子我相信平时在开发中,你们都用到过,由于数组越界等是最容易形成crash
的一种方式,并且通常崩溃起来比较严重,因此咱们必定要避免的
在iOS中NSNumber、NSArray、NSDictionary
等这些类都是类簇(Class Clusters),一个NSArray
的实现可能由多个类组成。因此若是想对NSArray
进行Swizzling
,必须获取到其真身进行Swizzling
,直接对NSArray进行操做是无效的。这是由于Method Swizzling对NSArray这些的类簇是不起做用的。
由于这些类簇类,实际上是一种抽象工厂的设计模式。抽象工厂内部有不少其它继承自当前类的子类,抽象工厂类会根据不一样状况,建立不一样的抽象对象来进行使用。例如咱们调用NSArray
的objectAtIndex:
方法,这个类会在方法内部判断,内部建立不一样抽象类进行操做。
因此若是咱们对NSArray
类进行Swizzling
操做其实只是对父类进行了操做,在NSArray
内部会建立其余子类来执行操做,真正执行Swizzling
操做的并非NSArray
自身,因此咱们应该对其“真身”进行操做。
下面列举了NSArray
和NSDictionary
本类的类名,能够经过Runtime函数取出本类:
下面是一个常见的例子
@implementation NSArray (CrashHandle)
✅// 若是下面代码不起做用,形成这个问题的缘由大多都是其调用了super load方法。在下面的load方法中,不该该调用父类的load方法。这样会致使方法交换无效
+ (void)load {
Method originMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method swizzlingMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(wy_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
✅// 为了不和系统的方法冲突,我通常都会在swizzling方法前面加前缀
- (id)wy_objectAtIndex:(NSUInteger)index {
✅// 判断下标是否越界,若是越界就进入异常拦截
if (self.count-1 < index) {
@try {
return [self cm_objectAtIndex:index];
}
@catch (NSException *exception) {
✅// 在崩溃后会打印崩溃信息。若是是线上,能够在这里将崩溃信息发送到服务器
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
}
@finally {}
} ✅// 若是没有问题,则正常进行方法调用
else {
return [self cm_objectAtIndex:index];
}
}
**************************调用******************************
- (void)viewDidLoad {
[super viewDidLoad];
// 测试代码
NSArray *array = @[@0, @1, @2, @3];
[array objectAtIndex:3];
//原本要奔溃的,可是没有,打印出了信息
[array objectAtIndex:4];
}
复制代码
以上的两个例子,只是开发中经常使用的,还有不少其余的应用,就须要根据需求来不断调整了。这些都属于AOP面向切面编程的一个实际应用,Method Swizzling也是其在iOS开发中应用的最经常使用的一种AOP思想