Method Swizzling

本文翻译自http://nshipster.com/method-swizzling/

If you could blow up the world with the flick of a switch Would you do it? If you could make everybody poor just so you could be rich Would you do it? If you could watch everybody work while you just lay on your back Would you do it? If you could take all the love without giving any back Would you do it? And so we cannot know ourselves or what we’d really do… With all your power … What would you do? —The Flaming Lips, “The Yeah Yeah Yeah Song (With All Your Power)”

—上面是一首歌.想听的话自行百度…(译者注)

上一周讲的关于 关联的对象的文章,我们开始了探索Objective-C 运行时的黑魔法.这一周,我们进一步来探索,来讨论大概是最具有争议的运行时hack技术:method swizzling

Method swizzling是替换现有selector实现的过程..这是一个可以让Objective-C在运行时替换实际调用的方法成为可能.替换的selectors会映射成为class调度表中的优先函数.

例如,我们想追踪在一个iOS APP中用户打开每个页面的次数.

我们可以在每个view controller里面自己实现viewDidAppear:方法 里面并添加追踪代码.但是这样会有大量的重复代码. 使用继承也是一种方式,但是这将需要继承UIViewController,UITableViewController,UINavigationController,还有每一个其他的viewcontroller.这样同样会带来大量的重复代码…

幸运的是我们还有另外一种解决途径: method swizzling (译者注:方法 交换/调整)的category. 下面代码为实现:

#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(xxx_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);
        }
    });
}

#pragma mark - Method Swizzling

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

@end

现在,当UIViewController实例 或者它的子类实例调用viewWillAppear:,一个log语句就会被打印出来.

注入行为到这个view contorller的生命周期,响应事件,视图绘制,基础网络栈对于如何使用method swizzling都是很好的例子.其他还是有很多适合swizzling技术使用的场合,并且使iOS开发者讲变得越来越经验丰富了.

+load vs. +initialize

Swizzling 应该总是完成+load

有两个方法是 Objective-C的每个类在运行时都会自动调用的. +load是在类初始化加载完毕时发送.当+initialize被调用仅仅在程序调用类或者此类实例的第一个方法之前.两个方法都是可选的,不过这两个方法仅仅只有在实现后才会被执行

因为 method swizzling 影响全局状态,所以减少竞争条件的可能性是非常重要的.+load 会保证在类初始化完毕之后加载,提供了一点点改变系统行为的一致性. 相比之下,+initialize没有在实际执行的时候提供保证,它可能从不会被调用,如果这个类从来都不是由app直接发送消息.

dispatch_once

Swizzling应该总是完成一个dispatch_once

再次,因为swizzling会改变全局状态,所以我们应该打起十二万的精神来预防. 原子性就是这样的一个预防,保证代码只正确的执行一次,即使在不同的线程中. Grand Central Dispatch中的dispatch_once提供了这两种令人满意的行为.所以成为了initializing singletons(初始化单例)的swizzling标准惯例

Selectors, Methods, & Implementations (选择器,方法和实现)

在Objective-C中,选择器,方法和实现涉及到运行时的特别方面,虽然在正常的交谈中,这两个属于经常可以互换的过程一般就是指的消息发送.

下面是苹果的Objective-C运行时参考中对于每一个的描述 > Selector (typedef struct objc_selector *SEL): Selectors are used to represent the name of a method at runtime. A method selector is a C string that has been registered (or “mapped”) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded .

>Method (typedef struct objc_method *Method): An opaque type that represents a method in a class definition. > Implementation (typedef id (*IMP)(id, SEL, ...)): This data type is a pointer to the start of the function that implements the method. This function uses standard C calling conventions as implemented for the current CPU architecture. The first argument is a pointer to self (that is, the memory for the particular instance of this class, or, for a class method, a pointer to the metaclass). The second argument is the method selector. The method arguments follow. 最好的方法来理解这些概念之间的关系就是如下: 一个类维护调度表来解决在运行时发送的消息; 表中的每一个条目是一个方法,这些方法的一个特别的名字就是选择器,这个选择器是实现一个基本C函数的指针. 调整一个方法就是去改变一个类的调度表,以解决现有的消息选择器给不同的实现,而混淆原始的方法实现一个新的选择。 ###调用 _cmd 下面的代码看上去像是无限循环
- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}
不必惊讶,这个大可放心,他不会出现无限循环.在swizzling的过程中,`xxx_viewWillAppear:`已经被指向了UIViewController原来的实现方法:`viewWillAppear:`.一般好的程序员看到一个方法在实现内调用self的方法就会浮现一个红色警报,但是在这个情况是有意义的,如果我还能记得这到底是怎么回事的话.然后,如果我们在这个方法内调用了`viewWillAppear:`方法的话,就会产生一个无限循环.因为这个实现在运行时已经被替换成了`viewWillAppear:`方法. >Remember to prefix your swizzled method name, the same way you might any other contentious category method. >记住你swizzled方法名字的前缀.以同样方式你可能有任意其他的争议/冲突 的category方法. ###Considerations 注意事项 Swizzling被广泛认为是一种巫术技术,容易发生不可预知的行为和不可预见的后果。虽然它不是最安全的事情,但是当采取以下预防措施时,method swizzling是相当安全的. #####总是调用一个方法原生的实现(除非你有一个非常好的理由不这么做): API为输入和输出提供了一个约束,但是实现在他们中间是一个黑盒子.调整一个方法并且没有被调用这个原生的方法可能会导致私有状态被破坏.一直到你的程序休息. #####避免冲突: category方法命名一个好的前缀,要充分的确信没有哪个捣蛋鬼能在你的代码库或者工程中起相同的功能. #####理解发生了什么: 简单的复制粘贴swizzling代码没有理解它是如何工作的不仅仅是危险的, 而且还浪费了一个学习Objective-C运行时的大好机会.通读[Objective-C Runtime Reference](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html#//apple_ref/c/func/method_getImplementation)并浏览`<objc/runtime.h>`来了解到底发生了什么,为什么会这样执行之类的. #####谨慎行事 原文的一段话类似乔布斯的 保持饥饿,保持愚蠢...