1. 关于Method swizzling的两种写法。
简单实现:
void simple_swizzle(Class class, SEL original, SEL swizzle) {
Method originalMethod = class_getInstanceMethod(class, original);
Method swizzleMethod = class_getInstanceMethod(class, swizzle);
method_exchangeImplementations(originalMethod, swizzleMethod);
}
需要注意的是,class_getInstanceMethod
如果没在当前类中查找到对应方法那么就会在整个继承体系中查找。如果明确知道originalMethod是当前类实现的方法,这种方式可以正常工作。当originalMethod是父类的方法时,swizzle后父类调用此方法会因为找不到子类的实现然后报错。
如果originalMethod==nil
说明没有找到这个方法,会导致swizzle失败。
最佳实践:
void best_swizzle(Class class, SEL original, SEL swizzle) {
Method originalMethod = class_getInstanceMethod(class, original);
Method swizzleMethod = class_getInstanceMethod(class, swizzle);
// 尝试向子类添加original方法,对应的方法实现为swizzle后的实现。
// didAddMethod == NO时,说明当前类存在originalMethod,处理方式和simple_swizzle一样。
// didAddMethod == YES时,说明当前类没有实现originalMethod,已成功添加
BOOL didAddMethod = class_addMethod(class, original, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
// 这里将swizzle方法的实现replace成父类的实现
class_replaceMethod(class, swizzle, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzleMethod);
}
// 这里处理 originalMethod==nil 的情况,说明之前在didAddMethod == YES的分支里repalce失败。在这里直接将swizzleMethod的实现用一个block替换
if (!originalMethod) {
method_setImplementation(swizzleMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"original method not implemented");
}));
}
}
一般情况下,如果明确需要swizzle的方法是当前类的方法,那么直接用第一种方式即可,这种方式建议用在一些通用的,需要满足高度容错度的场景,比如作为SDK的一部分发布出去的时候。
2. 关于多次swizzle后所有swizzle的方法是否都会被执行的问题。
一直有一个困惑,对同一个方法多次methods swizzing之后到底会发生什么,执行的顺序又是怎么样的。趁着周末时间来测试验证一下。
创建Coder类,提供一个叫做coding的方法。
@interface Coder : NSObject
- (void)coding;
@end
@implementation Coder
- (void)coding {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
}
@end
创建用于swizzle的category,在category的load方法中连续swizzle5次。
@interface Coder (language)
@end
@implementation Coder (language)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
coder_bestSwizzle(self, @selector(coding), @selector(coding_objc));
coder_bestSwizzle(self, @selector(coding), @selector(coding_kotlin));
coder_bestSwizzle(self, @selector(coding), @selector(coding_cpp));
coder_bestSwizzle(self, @selector(coding), @selector(coding_java));
coder_bestSwizzle(self, @selector(coding), @selector(coding_swift));
});
}
- (void)coding_objc {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
[self coding_objc];
}
- (void)coding_kotlin {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
[self coding_kotlin];
}
- (void)coding_cpp {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
[self coding_cpp];
}
- (void)coding_java {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
[self coding_java];
}
- (void)coding_swift {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
[self coding_swift];
}
@end
新建coder对象,调用coding方法
- (void)coderTest {
Coder *coder = Coder.new;
[coder coding];
}
打印如下:
2018-03-17 20:17:30.093302+0800 MyOCDemoProject[36165:5131009]
-[Coder(objc) coding_swift]
2018-03-17 20:17:30.093449+0800 MyOCDemoProject[36165:5131009]
-[Coder(objc) coding_java]
2018-03-17 20:17:30.093568+0800 MyOCDemoProject[36165:5131009]
-[Coder(objc) coding_cpp]
2018-03-17 20:17:30.093675+0800 MyOCDemoProject[36165:5131009]
-[Coder(objc) coding_kotlin]
2018-03-17 20:17:30.093805+0800 MyOCDemoProject[36165:5131009]
-[Coder(objc) coding_objc]
2018-03-17 20:17:30.093915+0800 MyOCDemoProject[36165:5131009]
-[Coder coding]
如预期的所有方法都hook成功,并且按照FILO(first in last out)的顺序调用了一遍。
那么这个顺序为什么是这样的呢?看下面的示意图就知道了
-
开始时的SEL和IMP的对应关系
-
swizzle
coding_objc
&coding
之后的对应关系,IMP指针互换
-
swizzle
coding_kotlin
&coding
之后的对应关系
因为之前coding的IMP指针已经指向了coding_objc的实现,所以在method_exchangeImplementations
时实际上交换的是coding方法的当前IMP指针而不是原始方法实现对应的IMP指针
-
后续的swizzle动作也是如此,
method_exchangeImplementations
实际上互换的对象是前一次swizzle后coding方法对应IMP指针而不是coding方法原始实现的IMP指针。
最终SEL和IMP对应关系如下图所示,并且[coder coding]
的整个执行的路径也在图中用白线标明。
看到这里也就不难理解为什么最后会形成FILO的调用顺序了。
需要注意的是:
- 正常来说swizzle不会如本文那样集中在同一个category的load方法中。方法的实际执行顺序就要具体类加载的顺序。
- 在使用runtime替换系统的方法的实现的目的大多是为了hook系统方法方法,插入自己的实现。如果你的目的是hook,那么务必要记住在你替换的实现里,调用自己一次。否则就会导致上图执行路径中,执行到你的实现时从IMP连到SEL的线断开,系统方法得不到调用。
参考文章:
Runtime中Swizzle时你可能没注意到的问题 - 简书
Objective-C Method Swizzling 的最佳实践