Method Swizzling 实现不修改任何代码替换掉某个方法的实现

Method Swizzling 和应用举例

直接从例子开始讲起。

例如 NSURL 的 URLWithString: 方法,通过它来创建 NSURL 时,如果输入的参数不合法,会导致创建出来的对象为 nil,从而导致后面的程序异常。

当然,我们需要保证输入的参数一定是不合法的。但是有的时候我们希望无论输入任何参数,都能保证正常的创建出 NSURL 对象,而不是 nil。

这个时候可以另写一个方法,在里面做好判断,然后所有之前调用 URLWithString: 的地方都改为去调用这个方法。

当然这样也可以,但是还有一种方法,它不需要改动任何现有的代码就能实现意图,这就是 Objective-C 的黑魔法 —— Method Swizzling。

实现步骤:

1. 创建 NSURL 的类别

创建一个 NSURL 的类别,文件为叫做 NSURL+Safe.h 和 NSURL+Safe.m。

2. 编写要替换成的方法

1
2
3
4
5
6
7
+ (instancetype)safe_URLWithString:(NSString *)string {
NSURL *URL = [NSURL URLWithString:string];
if (!URL) {
URL = [[NSURL alloc] init];
}
return URL;
}

在 NSURL 类别中添加上面方法,主要实现为如果通过 URLWithString: 创建得到的对象为 nil,那么则自己直接 [[NSURL alloc] init] 创建一个空的 NSURL 对象。

3. 编写 load 方法

现在介绍一下 load 会在什么时机被调用:当类被加载进内存时(实际上为 objc_class 对象)被调用,在 App 启动时会自动加载需要的所有类进内存,所有 load 的调用时间是非常早的,在 main 函数调用之前。

另外还有两个规则:

  • 当父类和子类都实现 load 函数时,父类的 load 函数会被先执行
  • 类别中的 load 不会替换原始类中的 load 方法,原始类和类别中的 load 方法都会被执行,原始类的 load 方法先被执行后,再执行类别中的 load 方法。如果有多个类别都实现了 load 方法,这些类别中的 load 的方法的执行顺序是不确定的。

下面来编写 load 方法:

由于用到了 Runtime API,需要在文件头部包含 #import <objc/runtime.h>

load 方法中主要是对 method_exchangeImplementations 方法的调用,它接受两个类型为 Method 的参数。它的作用是交换两个方法的实现的指针地址(IMP)。

可以通过 class_getClassMethodclass_getInstanceMethod 来获取类方法或者实例方法的 Method 类型对象。

load 方法的实现如下:

1
2
3
4
5
+ (void)load {
Method systemMethod = class_getClassMethod([NSURL class], @selector(URLWithString:));
Method customMethod = class_getClassMethod([NSURL class], @selector(safe_URLWithString:));
method_exchangeImplementations(systemMethod, customMethod);
}

4. 修改自定义方法中的调用

在上面的 safe_URLWithString 方法中,调用了 NSURL 的 URLWithString 方法。在添加完 load 方法后,由于 App 启动后会交换 URLWithString 和 safe_URLWithString 的实现,因此调用 URLWithString 实际上变成了调用 safe_URLWithString 自己,就会造成死循环。所以要改为:

1
2
3
4
5
6
7
+ (instancetype)safe_URLWithString:(NSString *)string {
NSURL *URL = [NSURL safe_URLWithString:string];
if (!URL) {
URL = [[NSURL alloc] init];
}
return URL;
}

这样看上去像是循环调用了,实际上是没有问题,不过不熟悉实现机制的人看到可能会比较懵逼,可以加上注释进行说明。

5. 测试

现在,可以编写一些测试代码看看是否生效。比如:

1
NSURL *URL = [NSURL URLWithString:@"aweg:\eahgiowe"];

会发现本来返回 nil 的 URLWithString 方法现在能返回一个 NSURL 对象了。

5. 在任意项目中使用

目前是指编写了两个文件 NSURL+Safe.h 和 NSURL+Safe.m 并添加到项目中,项目中其它部分完全没有修改。

如果想把这个功能应用到一个新的项目中,只要把这两个文件拷过去,添加到项目中即可,别的什么都不用做,非常方便。

总结

当任何时候想要替换项目中某个方法的实现时,都可以采用这个方法,只要编写一个类别,在里面用 Method Swizzling 交换方法实现,然后把类别添加进项目中即可。