iOS开发记录

Method Swizzling 方法欺骗

2018-01-20  本文已影响14人  大地瓜123

本文为大地瓜原创,欢迎知识共享,转载请注明出处。
虽然你不注明出处我也没什么精力和你计较。
作者微信号:christgreenlaw


本文的原文是Method Swizzling。本文只对其进行翻译。


方法欺骗是一个对已经存在的selector的实现进行更改的过程。由于OC的方法请求(method invocation)可以在运行时更改,这一技术是借由更改类分发表(class's dispatch table,也就是selector和函数的映射表)中selector和底层函数的映射关系而实现的。

比如说,我们想让一个iOS app中每一个展现出来的view controller都能追踪自己被展示了多少次:

每个vc都可以在自己的viewDidAppear:的实现中添加跟踪代码,但是这会产生无数的重复代码。继承也是一种实现方案,但这需要继承UiViewControllerUINavigationController,以及所有其他的vc类,这种做法也会有代码重复。

幸运的是,另一种方法是:在分类(category)中进行方法欺骗(method swizzling)。以下是实现方式:

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(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);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

现在呢,任何一个UIViewController的实例、或者其子类的实例调用viewWillAppear:时,都会打印一条日志信息。

向vc的生命周期、响应事件、视图绘制、或者是Foundation networking stack中注入行为,这些做法都是方法欺骗的优秀例子。方法欺骗的适用场景非常多,OC的开发者经验越丰富,这种使用就会越多。

我们不去理会为什么、以及在哪里使用欺诈,使用欺诈的方式永远是不变的:

+load vs. +initialize

Swizzling should always be done in +load.

OC运行时为每个类都会自动触发两个方法。+load消息在class最初加载的时候发送,而+initialize仅仅在应用程序第一次调用类上的方法或者使用类实例时调用。两个方法都是optional 的,都仅是在有实现的情况下才会调用。

由于方法欺诈影响全局状态,所以将冲突的可能性最小化就显得尤为重要。+load保证会在类初始化时调用,也就为改变全局行为提供了一定的一致性。相反的是,+initialize并不保证在什么时候执行---实际上,如果那个类永远不被app直接发送消息的话,它能永远都得不到调用。

dispatch_once

Swizzling should always be done in a dispatch_once.

需要再次强调一下,由于欺诈改变全局状态,我们需要在运行时尽可能的谨慎。原子性就是需要注意的一点,原子性保证代码仅会执行一次,即使在多线程下也是这样。GCD的dispatch_once提供了我们所需要的行为,就像在initializing singletons中一样。我们在进行方法欺诈时也应该把这个当做一个标准写法。

Selectors, Methods, & Implementations

OC中,selectors、methods、implementations都是runtime的一个特定方面,尽管在一般的描述中,这些术语通常可以互换地表示消息发送的过程。(大地瓜注:平时我们说这几个术语时一般都是指的发送消息,但实际上它们是runtime中不同的几个方面

以下是这几个术语在苹果的 Objective-C Runtime Reference 中的描述:

  • Selector (typedef struct objc_selector *SEL): selectors用于在运行时表示方法的名字。一个方法的selector是一串在OC runtime注册的C字符串。类在加载时,编译器生成的selectors自动由runtime完成匹配。
  • Method (typedef struct objc_method *Method): 一个不透明的类型,用于在类定义中代表一个方法。
  • Implementation (typedef id (*IMP)(id, SEL, ...)): 这个数据类型是一个指向实现方法的函数的起始位置的指针。这个函数使用当前CPU架构实现的标准的C调用规范。第一个参数是指向自己的指针(也就是类的特定实例的内存,或者说,对于类方法来说就是指向元类的指针)。第二个参数是method selector。接下来是method arguments。

要理解这些概念之间的关系,最好的描述方式就是:一个类(Class)维护一个分发表,以解决运行时的消息发送;表中的每条记录都是一个方法(Method),记录标志了一个特定的name,也就是the selector(SEL),指向一个实现(IMP),也就是底层C函数的指针。

要欺诈一个方法,也就是要更改一个类的分发表,用以将一个现存的selector解析到一个不同的实现上,同时将原始的method实现解析到一个新的selector上。

Invoking _cmd

下面的代码好像会引起一个无限循环:

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}

令人惊讶的是,并不会。在欺诈的过程中,xxx_viewWillAppear:已被分配给UIViewController -viewWillAppear:原始实现。一般根据直觉来讲,在自身的实现中,给self调用一个方法会引起错误,但是在这种情况下,如果我们还记得到底是怎样调用的,这一切就解释的通了。然而,如果我们在这个方法中调用viewWillAppear:,反倒会真的引起无限循环了,因为这个方法的实现已经在运行时被欺诈给viewWillAppear:了。

一定要记得将你的欺诈方法加一个前缀,就像你创建任何其他有争议的分类方法一样。

思考

欺诈一般被认为是一种黑魔法(voodoo techique),容易产生不可预测的行为,以及不可预见的结果。虽然它并不是百分百安全,但是如果你能够注意以下问题的话,方法欺诈还是很安全的:


JRSwizzle 是一个牛逼的欺诈库,支持cocoapods。

上一篇 下一篇

猜你喜欢

热点阅读