Method Swizzling

文章目录
  1. 1. Method swizzling 的作用
  2. 2. 应该在哪些地方使用 method swizzling
    1. 2.1. swizzling 应该只在 +load 中
    2. 2.2. dispatch_once
    3. 2.3. Selectors, Methods, & Implementations
    4. 2.4. 调用 _cmd
  3. 3. 涉及 runtime 方法
  4. 4. 思考
  5. 5. swift 中如何 swizzle

Method swizzling 的作用

他是一种 交换指针指向 的技术
Method swizzling 用于改变一个已经存在的 selector 的实现。
在运行时通过改变 selector 在类的消息分发列表中的映射从而改变原有方法。

例如:在 app 中追踪每一个视图控制器被用户呈现了几次:
通过在 viewDidAppear: 方法中添加追踪代码来实现,但会有大量重复代码。
继承是另一种可行的方式,但是这要求所有被继承的视图控制器如 UIViewController, UITableViewController, UINavigationController 都在 viewDidAppear:实现追踪代码,这同样会造成很多重复代码。这里有另外一种可行的方式:从 category 实现 method swizzling 。下面是实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#import <objc/runtime.h>
@implementation UIViewController (Tracking)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 1. 得到方法名称 selector
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
// 2. 得到方法类型(方法签名)
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);
// 3. 给 originalSelector 添加新method的实现和参数类,如果originSel在 cls 中没有实现体,那么会添加各一个实现体,并return YES,如果已经有实现体 return NO
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// 4. 替换
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]; // viewWillAppear
NSLog(@"viewWillAppear: %@", self);
}
@end

class_addMethod 。要先尝试添加 originSel 是为了做一层保护,因为如果这个类没有实现 originSel ,但其父类实现了,那 class_getInstanceMethod 会返回父类的方法。这样 method_exchangeImplementations 替换的是父类的那个方法。所以先尝试添加 originSel,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。

过程变化图
swizzle

应该在哪些地方使用 method swizzling

swizzling 应该只在 +load 中

在 Objective-C 的运行时中,每个类有两个方法都会自动调用。

  • +load 是在一个类被初始装载时调用,[只会调用一次]
  • +initialize 是在应用第一次调用该类的类方法或实例方法前调用的。[可能会被调用多次]

两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。

dispatch_once

swizzling 应该只在 dispatch_once 中完成。

  1. swizzling 改变了全局的状态
  2. 确保代码只执行一次。Grand Central Dispatch 的 dispatch_once 提供原子性

Selectors, Methods, & Implementations

在 Objective-C 的运行时中,selectors, methods, implementations 指代了不同概念,然而我们通常会说在消息发送过程中,这三个概念是可以相互转换的。 下面是苹果 Objective-C Runtime Reference中的描述:

  • Selector(typedef struct objc_selector *SEL):在运行时 Selectors 用来代表一个方法的名字。Selector 是一个在运行时被注册(或映射)的C类型字符串。Selector由编译器产生并且在当类被加载进内存时由运行时自动进行名字和实现的映射。
  • Method(typedef struct objc_method *Method):方法是一个不透明的用来代表一个方法的定义的类型。
1
2
3
4
5
6
// Method 结构
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
};
  • Implementation(typedef id (*IMP)(id, SEL,...)):这个数据类型指向一个方法的实现的最开始的地方。该方法为当前CPU架构使用标准的C方法调用来实现。该方法的第一个参数指向调用方法的自身(即内存中类的实例对象,若是调用类方法,该指针则是指向元类对象 metaclass )。第二个参数是这个方法的名字 selector,该方法的真正参数紧随其后。

理解 selector, method, implementation 这三个概念之间关系的最好方式是:在运行时,类(Class)维护了一个消息分发列表来解决消息的正确发送。每一个消息列表的入口是一个方法(Method),这个方法映射了一对键值对,其中键值是这个方法的名字 selector(SEL),值是指向这个方法实现的函数指针 implementation(IMP)。 Method swizzling 修改了类的消息分发列表使得已经存在的 selector 映射了另一个实现 implementation,同时重命名了原生方法的实现为一个新的 selector。

调用 _cmd

下面代码在正常情况下会出现循环:

1
2
3
4
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}

然而在交换了方法实现后就不会出现循环了。好的程序员应该对这里出现的方法的递归调用有所警觉,这里我们应该理清在 method swizzling 后方法的实现究竟变成了什么。在交换了方法的实现后,xxx_viewWillAppear:方法的实现已经被替换为了 UIViewController -viewWillAppear:的原生实现,所以这里并不是在递归调用。由于 xxx_viewWillAppear: 这个方法的实现已经被替换为了 viewWillAppear: 的实现,所以,当我们在这个方法中再调用 viewWillAppear: 时便会造成递归循环。

记住给需要转换的所有方法加个前缀以区别原生方法。

涉及 runtime 方法

  1. 通过 SEL 获取一个方法 Method
    Method class_getInstanceMethod(Class cls, SEL name);

  2. 通过 Method 获取该方法的实现 IMP
    IMP method_getImplementation(Method m);

  3. 返回一个字符串,描述了方法的参数和返回类型
    const char * method_getTypeEncoding(Method m);

  4. 通过 SEL 以及 IMP 给一个类添加新的方法 Method,其中 types 就是 method_getTypeEncoding 的返回值。
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

    1. 当 cls 中有 name 方法实现,添加method return NO
    2. 当 cls 中没有 name 方法,则添加方法实现 return YES
  5. 通过给定的 SEL 替换同一个类中的方法的实现 IMP,其中 SEL 是想要替换的 selector 名,IMP 是替换后的实现。
    IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);

  6. 交换两个方法的实现 IMP
    void method_exchangeImplementations(Method m1, Method m2);

思考

很多人认为交换方法实现会带来无法预料的结果。然而采取了以下预防措施后, method swizzling 会变得很可靠:

  • 在交换方法实现后记得要调用原生方法的实现(除非你非常确定可以不用调用原生方法的实现):APIs 提供了输入输出的规则,而在输入输出中间的方法实现就是一个看不见的黑盒。交换了方法实现并且一些回调方法不会调用原生方法的实现这可能会造成底层实现的崩溃。
  • 避免冲突:为分类的方法加前缀,一定要确保调用了原生方法的所有地方不会因为你交换了方法的实现而出现意想不到的结果。
  • 理解实现原理:只是简单的拷贝粘贴交换方法实现的代码而不去理解实现原理不仅会让 App 很脆弱,并且浪费了学习 Objective-C 运行时的机会。阅读 Objective-C Runtime Reference 并且浏览 <obje/runtime.h> 能够让你更好理解实现原理。
  • 持续的预防:不管你对你理解 swlzzling 框架,UIKit 或者其他内嵌框架有多自信,一定要记住所有东西在下一个发行版本都可能变得不再好使。做好准备,在使用这个黑魔法中走得更远,不要让程序反而出现不可思议的行为。

swift 中如何 swizzle

目前 swift 的版本已经不允许使用 loadinitialize 方法
也没有了 dispatch_once 方法,那么如何确保 swizzle 只执行一次呢?

引用 Method Swizzling