阅读 1239

Objective-C Runtime (三):Method Swizzling(方法替换)

Objective-C Runtime (三):Method Swizzling(方法替换)

Method Swizzling是一种改变改变一个'selector'的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。 实现图解如下图:

从上图中,我们可以看到,使用Method Swizzling本质上是将selectorC的方法实现IMPcselectorN的方法实现IMPn交换了,当我们调用selectorC,也就是给对象发送selectorC消息时,所查找到的对应的方法实现就是IMPn而不是IMPc了。

Method Swizzling在什么情况下可以用到了? 例如:我们接到一个需求:对 App 的用户行为进行追踪和分析。简单来说,就是,就是当用户进入某个界面或者点击某个按钮时,记录这个事件。

最粗暴的方式,就是在每个 viewDidAppear 里添加记录事件的代码。这种方式缺点是很明显的,它破坏了代码的干净整洁。因为记录事件的代码本身不属于原有代码的主要逻辑。随着项目扩大、代码增加,我们的原有代码里会到处分布着记录事件的代码。这时,要找到一段事件记录的代码会变得困难,也很容易忘记添加事件记录的代码。

我们可能会想到使用继承或类别,在重写的方法里添加事件记录的代码。但这样也会带来新的问题:

  1. 我们无法控制别人如何去实例化我们的子类;
  2. 对于类别,我们没办法调用到原来的方法实现。大多时候,我们重写一个方法只是为了添加一些代码,而不是完全取代它;
  3. 如果有两个类别都实现了相同的方法,运行时没法保证哪一个类别的方法会给调用;
  4. 每个 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通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。滥用可能会造成很多问题,如果遵从以下几点预防措施的话,还是比较安全的:

  1. Swizzling应该总是在+load中执行;
  2. Swizzling应该总是在dispatch_once中执行;
  3. 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分;
  4. 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。

参考:

  1. nshipster.com/method-swiz…
文章分类
iOS