Runtime之Method Swizzling

Objective-C 中的 Method Swizzling 是一项异常强大的技术,它可以允许我们动态地替换方法的实现,实现 Hook 功能,是一种比子类化更加灵活的“重写”方法的方式。

Method Swizzling 原理

在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。

这里写图片描述

我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,

我们可以利用 class_replaceMethod 来修改类,
我们可以利用 method_setImplementation 来直接设置某个方法的IMP,

归根结底,都是偷换了selector的IMP,如下图所示:

这里写图片描述

首先看一个简单的例子:

/**
 *   交换两个方法的实现,通过此操作,可为已有方法添加新功能。交互字符串的大小写功能.
 */
-(void)testOne{

    //此函数根据给定的SEL从类中取出与之相关的方法
    Method originalMethod = class_getInstanceMethod([NSString class],@selector(lowercaseString));
    Method swappedMethod =  class_getInstanceMethod([NSString class],@selector(uppercaseString));

    //此函数的两个参数表示待交换的两个方法实现
    /**
     * Exchanges the implementations of two methods.
     *
     * @param m1 Method to exchange with second method.
     * @param m2 Method to exchange with first method.
     *
     * @note This is an atomic version of the following:
     *  \code
     *  IMP imp1 = method_getImplementation(m1);
     *  IMP imp2 = method_getImplementation(m2);
     *  method_setImplementation(m1, imp2);
     *  method_setImplementation(m2, imp1);
     *  \endcode
     */
    method_exchangeImplementations(originalMethod, swappedMethod);


    /*
      从现在开始,如果在NSString实例上调用lowercaseString
      那么执行的将是uppercaseString的原有实现,反之亦然:
    */
    NSString *string = @"jacK IS a Big strong BOY!";

    NSString *lowercaseString = [string lowercaseString];
    NSLog(@"lowercaseString = %@", lowercaseString);

    // Output: lowercaseString = JACK IS A BIG STRONG BOY!

    NSString *uppercaseString = [string uppercaseString];
    NSLog(@"uppercaseString = %@", uppercaseString);

    // Output: uppercaseString = jack is a big strong boy!


    /*

     然而在实际应用中,像这样直接交换两个方法实现的,意义并不大。
     因为lowercaseString与uppercaseString这两个方法已经各自实现得很好了,
     没必要再交换了。但是,可以通过这一手段来为既有的方法实现增添新功能。
     */
}

再看一个实际的例子:

我们想跟踪在程序中每一个viewController展示给用户的次数:当然,我们可以在每个viewController的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个viewController中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。

这种情况下,我们就可以使用Method Swizzling,如在代码所示,给UIViewController添加一个分类:

#import <UIKit/UIKit.h>

@interface UIViewController (Tracking)

@end

#import "UIViewController+Tracking.h"
#import <objc/runtime.h>

@implementation UIViewController (Tracking)

/*
在这里,我们通过method swizzling修改了UIViewController的@selector(viewWillAppear:)
对应的函数指针,使其实现指向了我们自定义的SH_viewWillAppear的实现。
这样,当UIViewController及其子类的对象调用viewWillAppear时,都会打印一条日志信息。
*/

+ (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(SH_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));

       /*
         class_addMethod方法,它可以向类中动态地添加方法,用以处理给定的选择子。
         第三个参数为函数指针,指向待添加的方法。而最后一个参数则表示待添加方法的“类型编码”。
         在本例中,编码开头的字符表示方法的返回值类型,后续字符则表示其所接受的各个参数。
       */

        if (didAddMethod) {
            //替换掉方法的实现部分
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));

        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling
- (void)SH_viewWillAppear:(BOOL)animated {

    [self SH_viewWillAppear:animated];

    NSLog(@"SH_viewWillAppear: %@", self);

    //在这里进行代码统计和相应的操作!

  /*
    上面代码看上去是会导致无限循环的。但令人惊奇的是,并没有出现这种情况。
    在swizzling的过程中,方法中的[self SH_viewWillAppear:animated]已经被重新指定到UIViewController类的-viewWillAppear:中。在这种情况下,不会产生无限循环。
    不过如果我们调用的是[self viewWillAppear:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为SH_viewWillAppear:了。
  */

}

@end

上面的例子很好地展示了使用method swizzling来一个类中注入一些我们新的操作。当然,还有许多场景可以使用method swizzling。这里我们说说使用method swizzling需要注意的一些问题:

Swizzling应该总是在+load中执行

在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证—事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。

Swizzling应该总是在dispatch_once中执行

与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

选择器、方法与实现

在Objective-C中,选择器(selector)、方法(method)和实现(implementation)是运行时中一个特殊点,虽然在一般情况下,这些术语更多的是用在消息发送的过程描述中。

以下是Objective-C Runtime Reference中的对这几个术语一些描述:

Selector(typedef struct objc_selector *SEL):用于在运行时中表示一个方法的名称。一个方法选择器是一个C字符串,它是在Objective-C运行时被注册的。选择器由编译器生成,并且在类被加载时由运行时自动做映射操作。
Method(typedef struct objc_method *Method):在类定义中表示方法的类型
Implementation(typedef id (*IMP)(id, SEL, …)):这是一个指针类型,指向方法实现函数的开始位置。这个函数使用为当前CPU架构实现的标准C调用规范。每一个参数是指向对象自身的指针(self),第二个参数是方法选择器。然后是方法的实际参数。

理解这几个术语之间的关系最好的方式是:一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method),其中key是一个特定名称,即选择器(SEL),其对应一个实现(IMP),即指向底层C函数的指针。

为了swizzle一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中。

注意事项

Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:

总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
明白是怎么回事:学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看 头文件以了解事件是如何发生的。
小心操作:无论我们对Foundation, UIKit或其它内建框架执行Swizzle操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。

推荐:

http://blog.csdn.net/yiyaaixuexi/article/details/9374411
http://nshipster.com/method-swizzling/
http://blog.leichunfeng.com/blog/2015/06/14/objective-c-method-swizzling-best-practice/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值