一、Method Swizzling是什么
Method Swizzling
是改变一个selector
的实际实现的技术。
在Objective-C
中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector
的名字。利用Objective-C
的动态特性,可以实现在运行时偷换selector
对应的方法实现,达到给方法挂钩的目的;
之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是
Method Swizzling
,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling
的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。
二、Method Swizzling原理
每个类都有一个方法列表,存放着selector
的名字和方法实现的映射关系。IMP
有点类似函数指针,指向具体的Method
实现。
- 我们可以利用
method_exchangeImplementations
来交换2个方法中的IMP; - 我们可以利用
class_replaceMethod
来修改类; - 我们可以利用
method_setImplementation
来直接设置某个方法的IMP;
……
归根结底,都是偷换了selector
的IMP
,如下图所示:
三、Method Swizzling 实践
例如,我们想跟踪在程序中每一个view controller展示
给用户的次数:当然,我们可以在每个view controller
的viewDidAppear
中添加跟踪代码;但是这太过麻烦,需要在每个view controller
中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController
, UITableViewController, UINavigationController
及其它UIKit
中view controller的
子类,这同样会产生许多重复的代码。
#import "UIViewController+Tracking.h"
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(aClass,
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
上面的代码通过添加一个Tracking
类别到UIViewController
类中,将UIViewController
类的viewWillAppear:
方法和Tracking
类别中xxx_viewWillAppear:
方法的实现相互调换。Swizzling
应该在+load
方法中实现,因为+load
是在一个类最开始加载时调用。dispatch_once
是GCD
中的一个方法,它保证了代码块只执行一次,并让其为一个原子操作,线程安全是很重要的。
如果类中不存在要替换的方法,那就先用class_addMethod
和class_replaceMethod
函数添加和替换两个方法的实现;如果类中已经有了想要替换的方法,那么就调用method_exchangeImplementations
函数交换了两个方法的 IMP,这是苹果提供给我们用于实现 Method Swizzling
的便捷方法。
在这里,我们通过method swizzling
修改了UIViewController
的@selector(viewWillAppear:)
对应的函数指针,使其实现指向了我们自定义的xxx_viewWillAppear
的实现。这样,当UIViewController
及其子类的对象调用viewWillAppear
时,都会打印一条日志信息。
viewWillAppear: <ViewController: 0x7fe101e351d0>
四、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
一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中。
七、调用_cmd
我们回过头来看看前面新的方法的实现代码:
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}
咋看上去是会导致无限循环的。但令人惊奇的是,并没有出现这种情况。在swizzling
的过程中,方法中的[self xxx_viewWillAppear:animated]
已经被重新指定到UIViewController
类的-viewWillAppear:
中。在这种情况下,不会产生无限循环。不过如果我们调用的是[self viewWillAppear:animated]
,则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:
了。
八、注意事项
Swizzling
通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:
- 1、总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
- 2、避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
- 3、明白是怎么回事:简单地拷贝粘贴
swizzle
代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C
运行时的机会。阅读Objective-C Runtime Reference
和查看<objc/runtime.h>
头文件以了解事件是如何发生的。 - 4、小心操作:无论我们对
Foundation
,UIKit
或其它内建框架执行Swizzle
操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。