class_replaceMethod与method_exchangeImplementations区别
方法交换在开发中还是挺常见的,比如hook调viewDidLoad方法,想在每个viewDidLoad里面打印出当前类名,可以写个jm_ viewDidLoad方法,在用runtime交换俩方法的实现(也叫IMP)。
看不少开源库都用到方法交换,基本有俩种实现方式:
第一种实现:
void swizzleInstanceMethod(Class cls, SEL origSelector, SEL newSelector){
Method originalMethod = class_getInstanceMethod(cls, origSelector);
Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
IMP previousIMP = class_replaceMethod(cls, origSelector, method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
class_replaceMethod(cls, newSelector, previousIMP,method_getTypeEncoding(originalMethod));
}
第二种实现:
void swizzleInstanceMethod(Class cls, SEL origSelector, SEL newSelector){
Method originalMethod = class_getInstanceMethod(cls, origSelector);
Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
在某些情况下,这俩种确实都能达到交换IMP的效果,但是其中又有一丝区别。
看看class_replaceMethod的文档解释:
方法:IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char * types)
- Replaces the implementation of a method for a given class.
- @param cls The class you want to modify.
- @param name A selector that identifies the method whose implementation you want to replace.
- @param imp The new implementation for the method identified by name for the class identified by cls.
- @param types An array of characters that describe the types of the arguments to the method.
- Since the function must take at least two arguments-self and _cmd, the second and third characters
- must be “@:” (the first character is the return type).
- @return The previous implementation of the method identified by name for the class identified by \e cls.
- @note This function behaves in two different ways:
- If the method identified by name does not yet exist, it is added as if class_addMethod were called.
- If the method identified by name does exist, its IMP is replaced as if method_setImplementation were called.
重点是最后note的描述:当name描述的方法不存在的时候,将调用class_addMethod方法;当这个方法存在,将调用method_setImplementation方法。
再看看method_exchangeImplementations的文档解释:
方法:void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
- @param m1 Method to exchange with second method.
- @param m2 Method to exchange with first method.
- @note This is an atomic version of the following:
- IMP imp1 = method_getImplementation(m1);
- IMP imp2 = method_getImplementation(m2);
- method_setImplementation(m1, imp2);
- method_setImplementation(m2, imp1);
可以看出这个方法完全是个二愣子,直接交换IMP,啥都不管。
俩中方法都用到了class_getInstanceMethod(cls, selector)方法,这个方法有个特点:如果这个类中没有实现selector这个方法,它返回的是它某父类的 Method 对象(沿着继承链找到为止)。
重点来了:
如果这个类没实现这个方法,但是它父类实现了,直接拿这方法来交换,真没问题么??
先说结果:第二种实现有问题,第一种实现没问题。
看代码上代码:
@interface NSObject(test)
@end
@implementation NSObject(test)
- (void)oneSwizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector
{ Class cls = [self class];
Method originalMethod = class_getInstanceMethod(cls, origSelector);
Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
IMP previousIMP = class_replaceMethod(cls,
origSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
class_replaceMethod(cls,
newSelector,
previousIMP,
method_getTypeEncoding(originalMethod));
}
- (void)twoSwizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector
{
Class cls = [self class];
Method originalMethod = class_getInstanceMethod(cls, origSelector);
Method swizzledMethod = class_getInstanceMethod(cls, newSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end
@interface Base : NSObject
@end
@implementation Base
- (void)print:(NSString*)msg
{
NSLog(@"print-->obj %@ print say:%@", NSStringFromClass(self.class), msg);
}
- (void)hookPrint:(NSString*)msg {
NSLog(@"hookPrin-->obj %@ print say:%@", NSStringFromClass(self.class), msg);
}
@end
//A只实现了print方法,没有实现hookPrint方法。
@interface A : Base
@end
@implementation A
- (void)print:(NSString*)msg {
NSLog(@"A obj print say:%@", msg);
}
@end
//B啥都没实现。
@interface B : Base
@end
@implementation B
@end
现在我们测试第一种实现:分别交换A、B的print和hookPrint的方法实现。
[A oneSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
[B oneSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
//测试代码是这样的:
A* a = [A new]; [a print:@"hello1"];
B* b = [B new]; [b print:@"hello2"];
//结果:
2019-07-02 14:55:19.294580+0800 xctest[3533:667117] hookPrin-->obj A print say:hello1
2019-07-02 14:55:19.294871+0800 xctest[3533:667117] hookPrin-->obj B print say:hello2
说明A、B确实交换了方法实现。
现在我们测试第二种实现:分别交换A、B的print和hookPrint的方法实现。
[A twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
[B twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)];
//测试代码是这样的:
A* a = [A new]; [a print:@"hello1"];
B* b = [B new]; [b print:@"hello2"];
//结果:
2019-07-02 14:58:48.692403+0800 xctest[3585:681651] hookPrin-->obj A print say:hello1
2019-07-02 14:58:48.692697+0800 xctest[3585:681651] A obj print say:hello2
嗯?有没不对劲?B的交换方法失败了。不是期望中hookPrin-->obj B print say:hello2。
why?分析下:
第一种方法
,用class_replaceMethod()实现的。由于A中不存在hookPrint方法,class_replaceMethod会调用class_addMethod方法,而class_addMethod会把Base的hookPrint实现添加到当前类,print的实现最终会和hookPrint的实现交换。B中俩方法都不存在,也会添加俩方法,最终交换俩方法的实现。最终打印的时候,会调用Base的hookPrint方法。
如果调用[a hookPrint:@"hello_k"];那么最终实现的应该是A类中print的实现。结果是:A obj print say:hello_k。
第二种方法
,用method_exchangeImplementations实现。由于A没有实现hookPrint方法,在调用
[A twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)]的时候,将A的print实现与Base的hookPrint实现交换了。
接着调用 [B twoSwizzleInstanceMethod:@selector(print:) withMethod:@selector(hookPrint:)]的时候,由于B类啥都没实现,它只能将Base的print实现与base的hookPrint交换了。最终,调用[b print:@"hello2"]的时候,调用的是代码中A类的print方法的实现。
这里加不加class_addMethod的判断,结果都一样。可以自己试试。
测试代码地址:github链接
注意:整篇这是没考虑交换方法不存在的情况下考量的。
结论:当我们写的类没有继承的关系的时候,俩种方法都没什么问题。当有继承关系又不确定方法实现没实现,最好用class_replaceMethod方法。当啥都不确定的时候,老老实实地用class_replaceMethod 吧,安全无痛苦。