Runtime 之 Method Swizzling

最近翻译了一篇关于在 Swift 中使用 Method Swizzling 的文章(SwiftGG 传送门),对比 Objective-C 还是发现有很多的不同之处,写下来算是那篇英文原文的扩展,顺便写了两个小 demo 来演示(Github 传送门

Runtime 消息机制简介

要说清楚 Method Swizzling 是怎么回事,就要从 Objective-C 的 Runtime 说起。

Objective-C 中调用方法都是动态实现的,也就是说只有在程序运行到当前调用处时才确定到底执行哪个方法,并不是编译时确定的。而方法是通过发送消息(或消息转发,另说)来执行的,具体过程以语句

1
[object doIT:@"Hello World!"];

来说明:

毫无疑问,object 是对象,doIT: 是具体的方法名。而 Runtime 时上述代码会转化成:

1
objc_msgSend(object, @selector(doIT:), @"Hello World!");

顾名思义,语句的大致意思是:给 object 对象发送(objc_msgSend)一个 @selector(doIT:) 的消息,参数为字符串 @"Hello World!"

object 对象是如何找到这个 @selector(doIT:) 然后发送消息的呢?

首先,需要说明一下,所有类都继承自 NSObject,而实例对象的类类型是 Class,在 Class 的定义中有 isa(一个指向该对象的指针)、methodLists(一个存储对象方法或类方法的列表)等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif

}

objc_msgSend 通过 object 对象的 isa 指针找到对应的类,再通过 SEL 方法选择器(即 @selector(doIT),同时每个方法对应着一个不能重名的 SEL)中的方法名(doIT:)在对象的 methodLists 中找到对应的方法。

然而,方法真正的实现是由 IMP(Implementation)来执行消息的具体代码。其中,IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象 id(self 指针),调用对应的方法选择器 SEL (方法名),以及不定个数的方法参数,并返回一个 id。

简单地给这个消息发送过程画了个图,没有画其中 cache 的机制,将就着看吧。

Runtime消息发送过程

其中,一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method,图中红色),其中 key 是一个特定名称,即选择器(SEL,图中绿色),其对应一个实现(IMP,图中粉色),即指向底层C函数的指针。

OK,到了这里,一般正常的流程就走完了,对应对象的方法也执行了。

Objective-C 中的 Method Swizzling

Method Swizzling 通过改变特定 selector(方法)与实际实现(IMP)的映射,可以将一个方法的实现在 Runtime 时替换成其它的方法实现。

使用场景:

  1. 不愿意复制粘帖,而破坏代码干净整洁

  2. 不愿意为了加一点小功能,而搬出继承或类别

当然,Method Swizzling 也是一把双刃剑,它也有它的危险,可以看看 StackOverflow 上的讨论(StackOverflow 传送门)。

用 Objective-C 举个栗子

打开任何 ViewController 时,都可以实现日志记录或者统计打点的功能。我写的 Objective-C 版小 demo,真的很小(Github 传送门)。

  1. 新建工程 blabla。
  2. 新建 m 文件,输入文件名(如”Swizzling”),File Type 选择 Category,Class 选择 UIViewController。
  3. 成功创建 UIViewController+Swizzling.hUIViewController+Swizzling.m
  4. UIViewController+Swizzling.m 中输入如下代码:
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
#import "UIViewController+Swizzling.h"
#import <objc/runtime.h>

@implementation UIViewController (Swizzling)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];

SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzledViewDidAppear:);

Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

BOOL didAddedMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

NSLog(@"%@",didAddedMethod?@"YES":@"NO");

if (didAddedMethod) {
class_replaceMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
}else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}

});
}

- (void)swizzledViewDidAppear:(BOOL)animated {
[self swizzledViewDidAppear:animated];
NSLog(@"View Controller: %@ did appear animated: %@", NSStringFromClass([self class]), animated?@"YES":@"NO");
}

@end

在任何 UIViewController 中就都会执行我们在 swizzledViewDidAppear: 方法中的相关操作了。

1
2
3
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
}

Swift 中的 Method Swizzling

要在 Swift 自定义类中使用 Method Swizzling 有两个必要条件:

  1. 包含 Swizzle 方法的类需要继承自 NSObject

  2. 需要 Swizzle 的方法必须有动态属性(dynamic attribute)

注:对于 Swift 的自定义类,因为默认并没有使用 Objective-C 运行时,因此也没有动态派发的方法列表,所以如果要 Swizzle 的是 Swift 类型的方法的话,是需要将原方法和替换方法都加上 dynamic 标记,以指明它们需要使用动态派发机制。当然类也要继承自 NSObject。

再注:下面这个例子使用了 Objective-C 的动态派发,对于 NSObject 的子类(UIViewController)是可以直接使用的,并不是 Swift 中自定义的类,因此没有加 dynamic 标记也是可以的。

用 Swift 举同一个栗子

同样地,打开任何 ViewController 时,都可以实现日志记录或者统计打点的功能。我写的 Swift 版小 demo,真的真的很小(Github 传送门)。

  1. 新建工程 blabla。
  2. 新建 Swift 文件,输入文件名(如”Swizzling”)。
  3. 成功创建 Swizzling.swift
  4. Swizzling.swift 中输入如下代码:
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
import UIKit

extension UIViewController {
override public static func initialize() {

struct Static {
static var token: dispatch_once_t = 0;
}

dispatch_once(&Static.token) {
let originalSelector = Selector("viewDidAppear:")
let swizzledSelector = Selector("swizzledViewDidAppear:")

let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)

let didAddMethod:Bool = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))

if didAddMethod {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
}else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}

}
}

func swizzledViewDidAppear(animated: Bool) {
self.swizzledViewDidAppear(animated)

print("View Controller: \(self) did appear animated: \(animated)")
}

}

同样地,在任何 UIViewController 中就都会执行我们在 swizzledViewDidAppear 方法中的相关操作了。

1
2
3
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
}

Method Swizzling 中 Objective-C 与 Swift 的异同

我打算用 Markdown 中比较累人的表格来玩一下,顺便踩踩格式上的坑。

区别 Objective-C Swift
Runtime 头文件 #import <objc/runtime.h> 不需要
Swizzling 调用处 load 方法 initialize 方法
语法 Objective-C Swift

注:load 方法只在 Objective-C 里有,而且不能在 Swift 里重载,不管怎么试都会报编译错误。接下来执行 Swizzle 最好的地方就是 initialize 了,这是调用第一个方法前的地方。

在 dispatch_once 中执行

因为 Swizzling 会改变全局状态,所以我们需要在运行时采取一些预防措施。GCD 的dispatch_once 可以保证操作的原子性,确保代码只被执行一次,不管有多少个线程。

文章目录
  1. 1. Runtime 消息机制简介
  2. 2. Objective-C 中的 Method Swizzling
    1. 2.1. 使用场景:
    2. 2.2. 用 Objective-C 举个栗子
  3. 3. Swift 中的 Method Swizzling
    1. 3.1. 用 Swift 举同一个栗子
  4. 4. Method Swizzling 中 Objective-C 与 Swift 的异同
    1. 4.1. 在 dispatch_once 中执行