swizzling是一种能够改变已存在的selector的实现的方法,它通过改变selectors在类的映射表中的映射方式修改Objective-C的方法调用。
举个例子,如果我们想追踪各个viewController对用户展示了多少次,我们可以在每个viewController的viewDidAppear:中添加追踪代码,但是那样会产生大量的重复代码块。也可以通过继承的方式来实现,但是这种方式会要求继承UIViewController、UITableViewController、UINavigationController等其他任何的viewController类,这种方式也会产生重复代码。
幸运的是,我们可以在代理中添加swizzling方法来做到,而没有上述的问题:
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dospatch_onec(&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);
/*
* 当调整一个类方式的时候使用下面的实现方式:
* 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);
}
现在当任何UIViewController的实例或者子类调用viewWillAppear:都会打印出日志。
使用swizzling方法往视图控制器的生命周期、响应时间、视图绘画或者网络堆栈库里面注入行为具有很长好的效果。当然还有很多适用swizzling技术的场景。
+load vs. +initialize
Swizzling只能在+load方法中实现
在类中,只有两个方法会被运行时自动调用:
* 当类初始加载的时候会调用+load
* 当类或者对象被第一次使用时会调用+initialize
要实现Swizzling方法,+load和+initialize都是可选的,只要实现了方法,就会被执行。但是因为swizzling方式会影响全局的状态,所以尽量减少竟态条件是非常重要的。类在初始化的时候,+load是必然会被执行的,这在改变系统范围的行为的时候提供了一定的一致性。相较之下,+initalize就没有这方面的保证,它有可能永远不会被执行(只有被实例化的时候才会执行)。
dispatch_once
Swizzling应该在dispatch_once中实现
重复强调一下,因为swizzling会改变全局的状态,所以我们需要采取所有的可以采取的预防措施。原子操作就是一种预防措施,它确保了其中的代码只会被执行一次,即使在在不同的线程中。
Selectors,Methods,&Implementations
在Objective-C中,selectors,methods和implementations分别代表了runtime的几个方面,然而在消息发送的过程中,这些概念可以相互转换。
下面是Apple'sObjective-C运行时的官方描述:
Selector(typedef struct objc_selector *SEL):在runtime,Selectors代表方法的名称,selector方法是objective-C在运行时注册(或者映射)的一个C字符串,在类加载的时候,它由编辑器生成,并由runtime自动生成映射。
Method(typedef struct objc_method *Method):在类的定义中代表一个方法的未知类型。
Implementation(typedef id (*IMP)(id, SEL, ...)):这个数据类型是一个指向实现该方法的函数的开始地址,这个函数为当前的CPU架构使用标准的C语言协议。第一个参数是一个指向自身的指针(类的实例地址或者指向元类);第二个参数是selector方法。
理解这些概念的最好方式是:类维维护着一个消息分发列表,表中的入口是方法(Method),方法标记着selector的名称和IMP的指针。
swizzle方法是改变类的分发表中的已存在的selector和implementation的映射关系,使selector与implementation组成新的映射关系。
Invoking _cmd
下面代码可能会导致无限循环:
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillApear: %@", NSStringFromClass([self class]));
}
然而,并没有。在swizzling的实现过程中,xxx_viewWillAppear:已经被替换成了-viewWillAppear:,如果将[self xxx_viewWillAppear:animated]改成-viewWillAppear:则会发生死循环
记得给swizzled方法添加前缀是一个好的习惯。
总结
很多人认为Swizzling是一种不可靠的技术,容易产生不可预料的行为和后果。但是当做了如下处理后,它将变得非常可靠:
* 总是调用原生方法(除非你有更好的理由不去做):APIs提供了输入输出的协议,但是implementation却没有公开,是一个黑盒子。使用Swizzling方法后,而不是用原生的方法将会导致底层不可预知的崩溃。
* 避免冲突:分类方法使用前缀,确保其它代码库或者依赖库没有实现和你一样的功能。
* 理解原理
* 保持谨慎