Objective-C Runtime (三):Method Swizzling(方法替换)
Method Swizzling
是一种改变改变一个'selector'的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。 实现图解如下图:
Method Swizzling
本质上是将
selectorC
的方法实现
IMPc
与
selectorN
的方法实现
IMPn
交换了,当我们调用
selectorC
,也就是给对象发送
selectorC
消息时,所查找到的对应的方法实现就是
IMPn
而不是
IMPc
了。
那Method Swizzling
在什么情况下可以用到了? 例如:我们接到一个需求:对 App 的用户行为进行追踪和分析。简单来说,就是,就是当用户进入某个界面或者点击某个按钮时,记录这个事件。
最粗暴的方式,就是在每个 viewDidAppear
里添加记录事件的代码。这种方式缺点是很明显的,它破坏了代码的干净整洁。因为记录事件
的代码本身不属于原有代码的主要逻辑。随着项目扩大、代码增加,我们的原有代码里会到处分布着记录事件
的代码。这时,要找到一段事件记录的代码会变得困难,也很容易忘记添加事件记录的代码。
我们可能会想到使用继承或类别,在重写的方法里添加事件记录的代码。但这样也会带来新的问题:
- 我们无法控制别人如何去实例化我们的子类;
- 对于类别,我们没办法调用到原来的方法实现。大多时候,我们重写一个方法只是为了添加一些代码,而不是完全取代它;
- 如果有两个类别都实现了相同的方法,运行时没法保证哪一个类别的方法会给调用;
- 每个 ViewController 里的 ButtonClick 方法命名不可能都一样。
我了解决以上的问题,我们可以使用Method Swizzling
,如以下代码所示:
#import "UIViewController+Tracking.h"
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(track_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)track_viewWillAppear:(BOOL)animated {
[self track_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end
复制代码
从上面代码可以看出,我们通过method swizzling
修改了UIViewController
的@selector(viewWillAppear:)
对应的函数指针,使其实现指向了我们自定义的track_viewWillAppear:
的实现。这样,当UIViewController及其子类的对象调用viewWillAppear
时,都会打印一条日志信息。
上面代码需要解释的问题: class_addMethod
:要先尝试添加原 selector
是为了做一层保护,因为如果这个类没有实现 originalSelector
,但其父类实现了,那 class_getInstanceMethod
会返回父类的方法。这样 method_exchangeImplementations
替换的是父类的那个方法,这当然不是你想要的。所以我们先尝试添加 orginalSelector
,如果已经存在,再用 method_exchangeImplementations
把原方法的实现跟新的方法实现给交换掉。
注意事项 Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。滥用可能会造成很多问题,如果遵从以下几点预防措施的话,还是比较安全的:
- Swizzling应该总是在+load中执行;
- Swizzling应该总是在dispatch_once中执行;
- 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分;
- 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
参考: