Method Swizzing
方法交换,在Objective-C
中使用还是比较常见的,要搞清它的本质,要首先理解方法的本质。
一、方法的本质
Objective-C中,方法是由SEL
和IMP
组成的,前者叫做方法编号,后者叫方法实现。Objective-C中调用方法,其实就是通过SEL
查找IMP
的过程。
SEL
:表示选择器,通常可以把它理解为一个字符串。运行时维护着一张全局的SEL表
,将相同字符串的方法名映射到唯一一个SEL
。 通过sel_registerName(char *name)
方法,可以查找到这张表中方法名对应的SEL
。苹果提供了一个语法糖@selector
用来方便地调用该函数。
IMP
:是一个函数指针。objc中的方法最终会被转换成纯C的函数,IMP就是为了表示这些函数的地址。
二、Method Swizzing原理
方法混淆就是利用runtime特性以及方法的组成本质实现的。比如有方法sel1
对应imp1
,sel2
对应imp2
,经过方法混淆,使得runtime在方法查找时将sel1
的查找结果变为imp2
,如下图:
三、安全实现
新建一个项目,添加两个类:Animal
、Dog
(父类为Animal
),添加一个方法-eat
。
- (void)eat{
NSLog(@"%s", __func__);
}
添加一个Dog
的Method Swizzing分类,
+ (void)load {
Method oriMethod = class_getInstanceMethod(self, @selector(eat));
Method swiMethod = class_getInstanceMethod(self, @selector(swi_eat));
method_exchangeImplementations(oriMethod, swiMethod);
}
- (void)swi_eat{
NSLog(@"%s", __func__);
[self swi_eat];
}
下面具体分析一下可能遇到的坑点,以及如何避免。
3.1 多次交互
最好写成单例,避免方法多次交换。例如手动调用+ load
方法。
viewController 执行下面的代码:
[Dog load];
Dog *dog = Dog.new;
[dog eat];
打印结果为:
-[Dog eat]
这是因为执行了两次方法交换,方法被换回去了,为了避免这种意外,最好写成单例。
3.2 子类交换父类实现的方法
- 父类
Animal
实现了-eat
方法,子类Dog
没有重写 -
Dog分类
进行方法交换
Animal
和Dog
分别调用-eat
方法,子类正常打印,但是父类确崩溃了,为什么呢?
因为Dog
交换方法时,先在本类查找-eat
方法,再往父类查找。在父类 Animal
找到方法实现,就执行了方法交换。但是新方法-swi_eat
在子类Dog
,Animal
找不到就报异常:reason: '-[Animal swi_eat]: unrecognized selector sent to instance 0x600000267590'
。
所以安全的实现,应该只交换子类的方法,不动父类方法。
+ (void)load {
//1、单例,避免多次交互
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod(self, @selector(eat));
Method swiMethod = class_getInstanceMethod(self, @selector(swi_eat));
// 3、取得新方法swiMethod的实现和方法类型签名
//把新方法实现放到旧方法名中,此刻调用-eat就是调用-swi_eat
BOOL didAddMethod = class_addMethod(self, @selector(eat), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
// 3.1、添加成功,当前类未实现,父类实现了-eat
// 此时不必做方法交换,直接将swiMethod的IMP替换为父类oriMethod的IMP即可
class_replaceMethod(self, @selector(swi_eat), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
// 3.2、正常情况,直接交换
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
-
class_addMethod
方法:取得新方法swiMethod
的实现和方法类型签名,把新方法(swiMethod)实现放到旧方法(oriMethod)名中,此刻调用-eat
就是调用-swi_eat
。 -
didAddMethod
:
2.1 添加成功,说明之前本类没有实现-eat
方法,此时不必做方法交换,直接将swiMethod的IMP替换为父类(oriMethod)的IMP即可。 那么此时调用-swi_eat
,实则是调用父类的-eat
。
2.2 添加失败,说明本类之前已经实现,直接交换即可。
class_addMethod
不会覆盖本类中已存在的实现,只会覆盖本类从父类继承的方法实现
3.3 只有方法声明,没有方法实现,却做了方法交换——会造成死循环
为什么呢:
- 因为本类没有
-eat
的实现,所以执行class_addMethod
成功,此时调用-eat
就是调用-swi_eat
。 - 此时
didAddMethod
为YES
,添加方法成功,进行方法交换-class_replaceMethod
。那么此时调用-swi_eat
,实则是调用本类的-eat
。最终造成死循环。
改变代码逻辑如下:
+ (void)load {
//1、单例,避免多次交互
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod(self, @selector(eat));
Method swiMethod = class_getInstanceMethod(self, @selector(swi_eat));
// 2、当原方法未实现,添加方法,并设置IMP为空实现
if (!oriMethod) {
class_addMethod(self, @selector(eat), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"%@ 方法未实现", self);
}));
}
// 3、取得新方法swiMethod的实现和方法类型签名
//把新方法实现放到旧方法名中,调用-eat就是调用-swi_eat
BOOL didAddMethod = class_addMethod(self, @selector(eat), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
// 3.1、添加成功,当前类未实现,父类实现了oriMethod
// 此时不必做方法交换,直接将swiMethod的IMP替换为父类的IMP即可
class_replaceMethod(self, @selector(swi_eat), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
// 3.2、正常情况,直接交换
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
- 未实现方法时添加方法,此时调用
-eat
就是调用-swi_eat
。 - 如果经过正常的方法交换,
-swi_eat
方法内部还是调用自己-eat
。 - 所以未实现方法时,用
block
修改-swi_eat
的实现,就可以断开死循环了。
四、总结
- 尽可能在+load方法中交换方法
- 最好使用单例保证只交换一次
- 自定义方法名不能产生冲突
- 对于系统方法要调用原始实现,避免对系统产生影响
- 做好注释(因为方法交换比较绕)
- 迫不得已情况下才去使用方法交换
目前有两类常用的 Method Swizzling 实现方案,诸如 RSSwizzle 和 jrswizzle 这种较为复杂且周全的一些实现方案可供参考。
block hook:
关键在于理解,将Block_layout的invoke指针,强行指向_objc_msgForward指针,从而启动消息转发机制,在消息转发最后一步,将副本和hook block取出包装成NSInvocation进行调用。
可以参考:Block hook 正确姿势、yulingtianxia/BlockHook、Block Hook+libffi
拓展:抖音技术团队Objective-C & Swift 最轻量级 Hook 方案
五、补充,类方法的hook
先上hook代码的简单实现
static void SwizzleMethod(Class cls, SEL ori, SEL rep) {
Method oriMethod = class_getInstanceMethod(cls, ori);
Method repMethod = class_getInstanceMethod(cls, rep);
BOOL flag = class_addMethod(cls, ori, method_getImplementation(repMethod), method_getTypeEncoding(repMethod));
if (flag) {
class_replaceMethod(cls, rep, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, repMethod);
}
}
@interface Foo : NSObject
+ (void)bar;
@end
@implementation Foo
+ (void)bar {
NSLog(@"[Foo bar] called!!");
}
@end
@implementation Foo (Swizzle)
+ (void)swz_bar {
NSLog(@"Before calling ----");
[self swz_bar];
NSLog(@"After calling ----");
}
@end
实例方法的hook实现:
SwizzleMethod([Foo class], @selector(bar), @selector(swz_bar));
类方法的hook实现,就是把 cls 参数变成这个类的 Meta-Class 即可:
SwizzleMethod(object_getClass([Foo class]), @selector(bar), @selector(swz_bar));
[Foo bar];
参考:如何对类方法进行 Method Swizzling
class_getInstanceMethod、class_getClassMethod、isKindOfClass