一、先搞懂什么是方法交换,以及为啥要做
很多做iOS开发的朋友,大概率都听过“方法交换”这个词,但可能一想到要写代码,就怕踩坑——比如改了代码后APP莫名其妙闪退,或者换了个页面就出问题。其实方法交换没那么玄乎,本质就是给原来的方法“换个名字”,再把新写的方法“顶替”上去,等需要的时候再调原来的方法。举个最常见的例子:你想统计所有页面的点击事件,要是一个个改每个页面的代码,那工作量能让人崩溃,用方法交换就能一次性搞定。
先给大家放个最基础的错误示例,看看新手最容易踩的坑,这里用Objective-C作为统一技术栈,所有代码都按规范写。
// 错误示例:直接交换方法,没考虑线程和继承
#import <objc/runtime.h>
@implementation UIButton (Swizzle)
+ (void)load {
// 直接交换按钮的点击方法,没加锁,也没判断子类
Method originalMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzle_sendAction:to:forEvent:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)swizzle_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
// 统计点击事件
NSLog(@"按钮被点击了:%@", self);
// 调用原来的方法
[self swizzle_sendAction:action to:target forEvent:event];
}
@end
这个代码看起来没毛病,但实际跑起来会出大问题:比如多个线程同时执行load方法交换,可能导致交换一半就被打断;要是有自定义的UIButton子类,子类的方法也会被强制交换,完全没了自己的特性。
二、方法交换的两大核心坑:线程安全和子类继承
2.1 线程安全的坑:多线程同时改,方法“乱套”
为啥会有线程安全问题?因为Objective-C的方法交换是基于Runtime的,Runtime本身不是线程安全的——也就是说,多个线程同时修改方法的实现,就像两个人同时改同一份文档,你改完一半他接着改,最后出来的内容完全不对。
比如上面的错误示例,要是APP在启动时,有两个线程同时触发load方法,就可能出现交换失败:比如A线程刚拿到原来的方法,B线程就把新方法的实现给改了,最后A线程交换的就是错的方法,导致APP闪退。
那怎么解决线程安全?最直接的办法就是加锁——只要保证同一时间只有一个线程在执行方法交换的代码就行。常用的锁有两种:一种是dispatch_once,这个是专门用来保证代码只执行一次的,自带线程安全;另一种是互斥锁,比如@synchronized,不过dispatch_once更适合这种一次性的操作。
2.2 子类继承的坑:父类改了,子类也跟着变
很多新手做方法交换的时候,都是直接在父类的分类里写代码,比如给UIButton加分类,交换sendAction方法。但问题来了:UIButton有很多子类,比如自定义的MyButton,要是父类的方法被交换了,子类的方法也会被影响——因为子类会继承父类的方法,除非子类自己重写了这个方法。
比如你给UIButton加了点击统计的方法交换,那你自己写的MyButton的点击事件也会被统计,要是你不想统计MyButton的点击,就只能在MyButton里再交换一次,这样不仅麻烦,还容易出错。那怎么避免子类被影响?核心就是:交换方法的时候,只交换当前类的方法,不交换父类的。
三、安全实现的完整范式:一步步教你写对
3.1 第一步:加锁保证线程安全,用dispatch_once最靠谱
dispatch_once是GCD里的方法,它的作用是保证传入的代码块在整个APP生命周期里只执行一次,而且自带线程安全,不管多少个线程同时调用,都只会执行一次。把方法交换的代码放在dispatch_once的代码块里,就能解决线程安全的问题。
3.2 第二步:判断当前类是否重写了方法,避免影响子类
要想不影响子类,就得先判断当前类有没有重写要交换的方法——要是当前类没重写,那这个方法其实是从父类继承来的,要是交换了,就会影响父类和所有子类。那怎么判断?可以用class_getInstanceMethod方法,拿到当前类的方法实现,再拿到父类的方法实现,要是两者不一样,就说明当前类重写了这个方法,就可以交换;要是一样,就说明当前类没重写,就不能交换。
这里给大家解释一下Runtime里的几个常用方法,方便大家理解:
- class_getInstanceMethod:拿到某个类的实例方法,要是当前类没实现这个方法,就会返回父类的方法;
- method_getImplementation:拿到方法的实现,也就是方法具体的代码;
- class_addMethod:给某个类添加方法,要是这个类已经有这个方法了,就会返回NO;
- method_exchangeImplementations:交换两个方法的实现。
3.3 第三步:完整的示例代码,注释写得清清楚楚
下面给大家放一个安全的方法交换示例,技术栈是Objective-C,所有的坑都避开了,大家可以直接照着改:
// 安全示例:方法交换的安全实现,解决线程安全和子类继承问题
#import <objc/runtime.h>
#import <UIKit/UIKit.h>
@implementation UIButton (SafeSwizzle)
// 第一步:用load方法触发交换,load方法会在类加载的时候执行
+ (void)load {
// 第二步:用dispatch_once保证线程安全,代码只执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 要交换的方法:原来的方法是sendAction:to:forEvent:,新方法是自定义的方法
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizzledSelector = @selector(safe_swizzle_sendAction:to:forEvent:);
// 第三步:获取当前类的方法
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 第四步:判断当前类是否重写了originalSelector方法
// 先获取父类的originalSelector方法,要是当前类的方法和父类的一样,说明没重写
Class superClass = class_getSuperclass(class);
Method superOriginalMethod = class_getInstanceMethod(superClass, originalSelector);
BOOL hasOverride = (originalMethod != superOriginalMethod);
// 第五步:要是当前类没重写,就先给当前类添加originalSelector方法,避免影响父类
if (!hasOverride) {
// 给当前类添加originalSelector方法,实现是父类的实现
BOOL addSuccess = class_addMethod(class, originalSelector, method_getImplementation(superOriginalMethod), method_getTypeEncoding(superOriginalMethod));
// 添加成功后,重新获取当前类的originalSelector方法
if (addSuccess) {
originalMethod = class_getInstanceMethod(class, originalSelector);
}
}
// 第六步:判断swizzledSelector方法是否存在,要是不存在就添加
BOOL addSwizzleSuccess = class_addMethod(class, swizzledSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (addSwizzleSuccess) {
// 添加成功后,重新获取swizzledSelector方法
swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
}
// 第七步:交换两个方法的实现
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
// 自定义的交换方法,用来替换原来的sendAction方法
- (void)safe_swizzle_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
// 这里可以写自己的逻辑,比如统计点击事件
NSLog(@"按钮被点击了,当前类:%@,父类:%@", [self class], class_getSuperclass([self class]));
// 调用原来的方法,因为交换了方法的实现,所以这里调用的其实是原来的sendAction方法
[self safe_swizzle_sendAction:action to:target forEvent:event];
}
@end
这个代码的逻辑很清晰:首先用dispatch_once保证线程安全,然后判断当前类有没有重写要交换的方法,要是没重写,就先给当前类添加一个和父类一样的方法,再交换,这样就不会影响父类和其他子类了。
四、方法交换的应用场景、优缺点和注意事项
4.1 应用场景
方法交换的应用场景很多,最常见的有这几个:
- 埋点统计:比如统计所有页面的点击事件、页面停留时间,不用一个个改每个页面的代码;
- 全局拦截:比如拦截所有的网络请求,给请求加统一的参数,或者拦截所有的跳转,判断用户是否登录;
- 修复bug:比如某个系统方法有bug,没法改源码,就用方法交换替换成自己的方法,修复bug;
- 调试排查:比如排查某个方法为什么没执行,就用方法交换加日志,看方法有没有被调用。
4.2 技术优缺点
优点很明显:
- 无侵入性:不用改原来的代码,不用继承原来的类,直接用分类就能实现;
- 效率高:一次交换,全局生效,不用改多个地方的代码;
- 灵活:可以在运行时动态修改方法的实现,不用重新编译。
缺点也很突出:
- 容易踩坑:比如线程安全、子类继承、方法冲突等,新手很容易写错;
- 难排查:要是交换方法出了问题,很难排查是哪里错了,因为是动态修改的;
- 兼容性:不同版本的系统,Runtime的实现可能不一样,交换方法可能会出问题;
- 难维护:要是多个地方交换了同一个方法,很容易混乱,不好维护。
4.3 注意事项
用方法交换的时候,一定要注意这几点:
- 尽量用dispatch_once保证线程安全,不要用@synchronized,因为dispatch_once更高效,更适合一次性的操作;
- 尽量在分类里写交换方法,不要在原来的类里写,避免影响原来的代码;
- 尽量只交换自己写的类的方法,不要交换系统类的方法,避免影响系统的功能;
- 尽量不要交换太多的方法,不然会导致代码混乱,难维护;
- 尽量在测试环境多测,避免上线后出问题。
五、文章总结
方法交换是Objective-C里一个很强大的技术,但也是一个很容易踩坑的技术,很多新手用的时候,要么没考虑线程安全,要么没考虑子类继承,导致APP出问题。其实只要掌握了安全实现的范式,就能避开大部分的坑:首先用dispatch_once保证线程安全,然后判断当前类有没有重写要交换的方法,要是没重写,就先给当前类添加方法,再交换,这样就能保证方法交换的安全。
最后给大家提个醒:方法交换虽然强大,但也不要滥用,能不用就不用,要是能用其他方法实现,就尽量不用方法交换——毕竟方法交换是动态修改的,容易出问题,难排查,难维护。
Comments