本文为大地瓜原创,欢迎知识共享,转载请注明出处。
虽然你不注明出处我也没什么精力和你计较。
作者微信号:christgreenlaw
本文的原文是Method Swizzling。本文只对其进行翻译。
方法欺骗是一个对已经存在的selector的实现进行更改的过程。由于OC的方法请求(method invocation)可以在运行时更改,这一技术是借由更改类分发表(class's dispatch table,也就是selector和函数的映射表)中selector和底层函数的映射关系而实现的。
比如说,我们想让一个iOS app中每一个展现出来的view controller都能追踪自己被展示了多少次:
每个vc都可以在自己的viewDidAppear:
的实现中添加跟踪代码,但是这会产生无数的重复代码。继承也是一种实现方案,但这需要继承UiViewController
,UINavigationController
,以及所有其他的vc类,这种做法也会有代码重复。
幸运的是,另一种方法是:在分类(category)中进行方法欺骗(method swizzling)。以下是实现方式:
#import
@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),容易产生不可预测的行为,以及不可预见的结果。虽然它并不是百分百安全,但是如果你能够注意以下问题的话,方法欺诈还是很安全的:
- 永远要触发方法的原始实现。(除非你真的有充分的理由不这样做):API提供了输入和输出的约束(contract,本意为合同),但是内部的实现是黑箱。欺诈一个方法然后又不调用原始的实现也许会造成底层私有状态崩溃,甚至会引起你应用程序的错误。
- 避免冲突:给分类方法加前缀(prefix category methods),
然后一定要确保你的代码中任何其他地方都没有在你这个功能上搞事情。(and make damn well sure that nothing else in your code base(or any of your dependencies) are monkeying around with the same piece of functionality as you are) - 明白当前到底在干什么:单纯地复制粘贴欺诈代码而不理解院里的话是很危险的,并且也浪费了学习OC runtime的机会。读一下Objective-C Runtime Reference然后搜索
以理解事情到底是怎么运行的。永远要努力理解,而不是只是胡想。(Always endeavor to replace magic thinking with understanding.) - 小心行事:不管你对欺诈Foundation、UIKit还是其他内置的framework有多大的信心,你要知道下一个版本可能代码就会崩溃。你要做好准备,进一步努力以保证玩火的时候别引火上身。
JRSwizzle 是一个牛逼的欺诈库,支持cocoapods。