MethodSwizzling
背景
有一天,项目里要替换整个项目里一个方法的所有的实现,老大说:“如果是你你怎么办?这就像个面试题”,我在那里回答"继承"、methodSignature、refactor、一堆答案没有一个命中。老大给了我一个methodSwizzing的连接,于是就有了这个文章
问题
一个常见的场景就是,当我们想对大量同样的方法执行某个相同的操作。例如我们想在每个ViewController的viewDidLoad方法中添加某行代码,当然我们可以通过继承、让UIViewController、UINavigationController等都实现这个方法。这样,它们的子类也就会执行这个方法。但是,这样会导致我们需要在各种ViewController中添加代码,而且如果有的子类并没有调用父类的viewDidLoad,那就覆盖不到这个子类。此外,在视图控制器的生命周期,响应事件,绘制视图等场景中,都会由这个需求,这个时候method swizzling 就能够为开发带来很好的作用。
示例代码
最常见的hook代码
+ (void)swizzMethod:(SEL)origSel altMethod:(SEL)altSel {
Method origMethod =class_getInstanceMethod(self,origSel);
Method altMethod = class_getInstanceMethod(self, altSel);
BOOL didAddMethod = class_addMethod([self class],origSel, method_getImplementation(altMethod),method_getTypeEncoding(altMethod));
if (didAddMethod) {
class_replaceMethod([self class], altSel,method_getImplementation(origMet),method_getTypeEncoding(origMet))
} else {
method_exchangeImplementations(class_getInstanceMethod(self, origSel_), class_getInstanceMethod(self, altSel_));
}
}
代码解释
我看到这个代码的时候,除了认得字母外,一无所知😑所以我首先查了下里面的每行代码做了什么。
首先代码中使用到了class_getInstanceMethod(self, origSel)这个方法用来返回一个class中指定的实例方法,如果class或者它的superclass不存在对应的实例方法,就会返回NULL
然后代码中使用了class_addMethod这个方法用来给一个class添加一个指定名称和实现的方法: BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
a) 方法一共有四个参数:class是要添加方法的class,name是方法的name,imp是方法的实现,实现里至少需要两个参数self和_cmd。 types是描述参数类型的字符串构成的一个数组
b) 方法会重写superclass's 实现,但是不会替换这个类里已有的实现
c) 一个Objective-C 方法就是一个至少有两个参数的c方法。例如对于一个给定的方法:
i. void myMethodIMP(id self, SEL _cmd){ // implementation ....}
d) 我们可以像下面的方法一样动态的将它以resolveThisMethodDynamicall名字添加到class中
i. class_addMethod([self class], @selector(resolveThisMethodDynamically), (IMP) myMethodIMP, "v@:");
知道了以上两个基础,就可以看到。实际上methodSwizzling是先添加了名称是被替换方法的一个指定的替换后的方法
然后方法调用了一个class_replaceMethod函数,函数共四个参数,第一个参数是要修改的方法所在的类,第二个参数是要替换实现的函数名称,之后分别传入方法实现和方法参数的字符串数组。同时方法会返回被替换函数的之前实现。
a) 如果函数名称指定的方法之前不存在,函数就会像method_add一样为类添加一个方法。
b) 如果函数名称指定的方法存在,函数就像method_setImplementation替换函数的实现
知道了这点就可以看到,方法替换函数第二步在成功添加了被替换函数同名的替换方法后,会将替换函数名对应的函数实现替换为之前的函数实现。如果添加函数不成功,也就是类之前就存在一个同名的函数实现时会调用method_exchangeImplementations函数,函数接收两个 Method 类型参数。然后交换它们的实现。所以,methodSwizzling实际上相当于将添加后的函数实现和原先的函数实现,也就是IMP指针进行了替换。
上述的代码是写在类本身的,当然我们也可以在category中对某个类进行方法替换,那么需要在load方法中调用上面swizzMethod:(SEL)origSel altMethod:(SEL)altSel相似的函数。只不过其中的实现需要改成先调用两次class_addMethod将被替换函数和替换函数都加入到类中,然后,调用method_exchangeImplementations方法将两个函数的实现进行替换。
注意事项
首先看看load 和 initialize方法:swizzling 应该只在 +load 中完成。
a) 在 Objective-C 的运行时中,每个类有两个方法都会自动调用。+load 是在一个类被初始装载时调用,也就是当代码还没有被load的时候代码从文件夹的代码加载到运行的程序中时候会调用load方法
b) +initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。
只调用一次
swizzling方法会替换所有同名的方法的实现,所以,应该确保这个方法只执行一次,不会发生多次替换的情况,这个时候就可以用dispatch_once,这个可以满足所有的需求,而且 dispatch_once也是初始化一个单例方法的标准方法,但是这种方式进行的更改仅限于method所在的类,比如在UIView的子类B中进行方法替换,那么方法替换将仅限于B及B的子类。
Selector、SEL和IMP的概念
Selector 是一个在运行时被注册(或映射)的C类型字符串。Selector由编译器产生并且在当类被加载进内存时由运行时自动进行名字和实现的映射。Method是用来表示函数定义的类型的一个结构体,IMP是一个函数指针,该方法的第一个参数指向调用方法的自身(即内存中类的实例对象,若是调用类方法,该指针则是指向元类对象 metaclass)。第二个参数是这个方法的名字 selector,该方法的真正参数紧随其后。
防止递归
使用的时候,在交换方法实现后记得要调用原生方法的实现,除非确定可以不用调用原生实现,如果不调用,可能会导致一些底层实现错误例如下面这段代码。
a) - (void) altSel {
[self altSel];
}
b) 看起来这里会发生递归调用,但是 实际上因为外层的altSel已经替换成了origSel所以实际上是origSel在调用altSel 但是如果里边换成[self origSel] 就会发生递归调用
防止父类和子类同时替换
应该在替换的方法前增加前缀,如果不在load方法中添加替换,而是在caterory中执行,因为category中的方法是在Runtime加载的时候加到类的MethodList,这就会导致如果altSel重名之后,SEL和IMP不匹配,导致hook的结果不对
被hook的方法应该是类自身的方法,如果把继承的IMP copy到自身上面会存在问题,父类的方法应该在调用的时候使用,不应该swizzling的时候copy到子类,如果父类也hock了方法。子类也hook了方法,父类hook的方法可能由于compileSourc中子类category顺序在父类category之前,而导致只调用子类的hook方法,或者两者都不调用,举个形象的例子, UIView有一个方法,实现叫做A, UIView将方法的实现替换成了B,这个时候,UIView的子类又去替换这个A原先方法名的实现,那么它替换的将是B。也就是UIView的子类就没有A的实现了。