Objective-C Method Swizzling

Method swizzling 用于改变一个已经存在的 selector 的实现。因为 method swizzling 可以通过交换 selector 来改变函数指针的引用。

Method swizzling 原理

在 OC 语言中,SEL 表示一个方法的名字,定义为如下结构体指针:

1
2
3
4
5
6
typedef struct objc_selector    *SEL;

struct objc_selector {
char *name; OBJC2_UNAVAILABLE;
char *types; OBJC2_UNAVAILABLE;
};

IMP 是一个函数指针类型,指向函数的实现代码,定义如下:

1
typedef id (*IMP)(id, SEL, ...);

这个被指向的函数的参数包含一个接收消息的 id 对象、 SEL、以及不定个数的参数,并返回一个 id 类型的对象,我们可以像在C语言里面一样使用这个函数指针。

SEL 与 IMP 的关系非常类似于 HashTable 中 key 与 value 的关系。OC 中不支持函数重载的原因就是因为一个类的方法列表中不能存在两个相同的 SEL 。但是不同的类中可以拥有相同的 SEL,不同类的实例对象执行相同的 SEL 时,会在各自的方法列表中去根据 SEL 去寻找自己对应的IMP。这使得OC可以支持函数重写。

method swizzling 技术使两个 SEL 所对应的 IMP 相互交换,也就是交换了 SEL1 和 SEL2 的函数实现,如下图所示:


method swizzling 涉及到的 runtime 接口主要有以下几个:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//获取一个类中的实例方法 Method
/**
* Returns a specified instance method for a given class.
*
* @param cls The class you want to inspect.
* @param name The selector of the method you want to retrieve.
*
* @return The method that corresponds to the implementation of the selector specified by
* \e name for the class specified by \e cls, or \c NULL if the specified class or its
* superclasses do not contain an instance method with the specified selector.
*
* @note This function searches superclasses for implementations, whereas \c class_copyMethodList does not.
*/
OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);


//通过 Method 获取IMP
/**
* Returns the implementation of a method.
*
* @param m The method to inspect.
*
* @return A function pointer of type IMP.
*/
OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m)


//通过 Class 和 SEL 获取IMP
/**
* Returns the function pointer that would be called if a
* particular message were sent to an instance of a class.
*
* @param cls The class you want to inspect.
* @param name A selector.
*
* @return The function pointer that would be called if "[object name]" were called
* with an instance of the class, or "NULL" if "cls" is "Nil".
*
* @note "class_getMethodImplementation" may be faster than "method_getImplementation(class_getInstanceMethod(cls, name))".
*
* @note The function pointer returned may be a function internal to the runtime instead of
* an actual method implementation. For example, if instances of the class do not respond to
* the selector, the function pointer returned will be part of the runtime's message forwarding machinery.
*/
OBJC_EXPORT IMP _Nullable
class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name);


//获取一个类的类方法 Method
/**
* Returns a pointer to the data structure describing a given class method for a given class.
*
* @param cls A pointer to a class definition. Pass the class that contains the method you want to retrieve.
* @param name A pointer of type \c SEL. Pass the selector of the method you want to retrieve.
*
* @return A pointer to the \c Method data structure that corresponds to the implementation of the
* selector specified by aSelector for the class specified by aClass, or NULL if the specified
* class or its superclasses do not contain an instance method with the specified selector.
*
* @note Note that this function searches superclasses for implementations,
* whereas \c class_copyMethodList does not.
*/
OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name);


//给一个类添加一个新方法和该方法的实现
/**
* Adds a new method to a class with a given name and implementation.
*
* @param cls The class to which to add a method.
* @param name A selector that specifies the name of the method being added.
* @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
* @param types An array of characters that describe the types of the arguments to the method.
*
* @return YES if the method was added successfully, otherwise NO
* (for example, the class already contains a method implementation with that name).
*
* @note class_addMethod will add an override of a superclass's implementation,
* but will not replace an existing implementation in this class.
* To change an existing implementation, use method_setImplementation.
*/
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types);


//替换一个类中某个方法的实现
/**
* Replaces the implementation of a method for a given class.
*
* @param cls The class you want to modify.
* @param name A selector that identifies the method whose implementation you want to replace.
* @param imp The new implementation for the method identified by name for the class identified by cls.
* @param types An array of characters that describe the types of the arguments to the method.
* Since the function must take at least two arguments—self and _cmd, the second and third characters
* must be “@:” (the first character is the return type).
*
* @return The previous implementation of the method identified by \e name for the class identified by \e cls.
*
* @note This function behaves in two different ways:
* - If the method identified by "name" does not yet exist, it is added as if "class_addMethod" were called.
* The type encoding specified by "types" is used as given.
*
* - If the method identified by "name" does exist, its "IMP" is replaced as if "method_setImplementation" were called.
* The type encoding specified by "types" is ignored.
*/
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types);


//交换两个方法的实现
/**
* Exchanges the implementations of two methods.
*
* @param m1 Method to exchange with second method.
* @param m2 Method to exchange with first method.
*
* @note This is an atomic version of the following:
* \code
* IMP imp1 = method_getImplementation(m1);
* IMP imp2 = method_getImplementation(m2);
* method_setImplementation(m1, imp2);
* method_setImplementation(m2, imp1);
* \endcode
*/
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);

⚠️ 需要注意的是, class_getMethodImplementation(cls, name) 要比 method_getImplementation(class_getInstanceMethod(cls, name))快。

Method swizzling 应用

1. 替换一个类的实例方法

example: 页面打点,替换 NSViewController 的 viewWillAppear 方法

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
43
44
45
46
47
48
#import "NSViewController+Tracking.h"
#import <objc/runtime.h>

@implementation NSViewController (Tracking)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originSel = @selector(viewWillAppear);
SEL swizzledSel = @selector(sw_viewWillAppear);

// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(class, originSel);
// Method swizzledMethod = class_getClassMethod(class, swizzledSel);

Method originMethod = class_getInstanceMethod(class, originSel); //当前类没有实现 originSel 则获取到的是父类的实现
Method swizzledMethod = class_getInstanceMethod(class, swizzledSel);

//给类添加一个新方法和该方法的实现, 如果该类中已经存在这个方法,则return NO
BOOL didAddMethod = class_addMethod(class, originSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
//添加方法成功,此时对应关系如下:
// originSel --> swizzledMethod.imp
// swizzledSel --> swizzledMethod.imp
//下面再进行一步方法替换:
class_replaceMethod(class, swizzledSel, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
//替换后变为:
// originSel --> swizzledMethod.imp
// swizzledSel --> originMethod.imp
//这样就完成了方法替换
}else {
//添加 originMethod 方法失败,说明该类中已经存在这个方法的实现
//只需要交换这两个方法的实现即可
method_exchangeImplementations(originMethod, swizzledMethod);
}
});
}

- (void)sw_viewWillAppear {
[self sw_viewWillAppear];
NSLog(@"tracking viewWillAppear: %@ ",self);
}


@end

现在,NSViewController 或其子类的实例对象在调用 viewWillAppear: 的时候会有 log 的输出:

1
2020-05-08 21:40:39.495648+0800 Demo[85619:1059256] tracking viewWillAppear: <ViewController: 0x600002c1cd00>

在视图控制器的生命周期,响应事件,绘制视图或者 Foundation 框架的网络栈等方法中插入代码都是 method swizzling 能够为开发带来好的作用的例子。有很多的场景选择 method swizzling 会是很合适的解决方式,这显然也会让 Objective-C 开发者的技术变得越来越成熟。

在进行 method swizzling 的时候需要注意以下几点⚠️

  • swizzling 应该只在 +load 中完成。 在 Objective-C 的运行时中,每个类有两个方法都会自动调用。+load 是在一个类被初始装载时调用,+initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。
  • swizzling 应该只在 dispatch_once 中完成。
    由于 swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once 满足了所需要的需求,并且应该被当做使用 swizzling 的初始化单例方法的标准。
  • 多个有继承关系的类进行 swizzling 时,应该按照由父类到子类的顺序开始,这样才能保证子类方法拿到父类中的被 swizzled 的实现。在 +(void)load 中 swizzle 不会出错,就是因为 load 类方法会默认从父类开始调用。

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

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

2. hook 一个“黑盒”类的实例方法(拿不到该类的头文件,只知道该类的名字和要hook的函数名字)

example: hook 一个类 Person 中的 -(void)speak:eat: 方法,但是没有该类的头文件,需要创建一个类(Person_hook)作为载体,将原始方法 -(void)speak:eat: hook到该载体类的方法 -(void)hook_speak:eat: 中。

具体实现如下,新建一个类 Person_hook:

1
2
3
4
5
6
//Person_hook.h
#import <Foundation/Foundation.h>

@interface Person_hook : NSObject

@end

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
//Person_hook.m

#import "Person_hook.h"
#import <objc/runtime.h>

@implementation Person_hook
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

Class originClass = NSClassFromString(@"Person");
Class swizzClass = [self class];

SEL originSel = NSSelectorFromString(@"speak:eat:");
SEL swizzSel = NSSelectorFromString(@"hook_speak:eat:");
Method originClass_originMethod = class_getInstanceMethod(originClass, originSel);
//被hook的原始类没有实现这个实例方法,就没有hook它的必要了,直接返回
if (originClass_originMethod == nil) {
return;
}

Method swizzClass_swizzMethod = class_getInstanceMethod(swizzClass, swizzSel);
//向原始类中添加名字为 swizzSel 的当前类的方法实现
class_addMethod(originClass, swizzSel, method_getImplementation(swizzClass_swizzMethod), method_getTypeEncoding(swizzClass_swizzMethod));

Method originClass_swizzMethod = class_getInstanceMethod(originClass, swizzSel);
//将原始类中的两个方法的实现进行交换
method_exchangeImplementations(originClass_originMethod, originClass_swizzMethod);
});
}


- (void)hook_speak:(NSString *)languge eat:(NSString *)food {
NSLog(@"即将调用 Person 类的 speak:eat: 方法");
[self hook_speak:languge eat:food];
NSLog(@"已经调用 Person 类的 speak:eat: 方法");
}

@end

当原始类 Person 的 speak:eat: 方法在某个地方被调用时,就会被 hook 到 Person_hook 类中的方法,从而实现 hook 原始类方法的目的。控制台会打印如下,其中第二行是原始方法的行为,第一行和第三行是我们把它hook住以后所做的事情:

1
2
3
2020-05-09 16:16:33.997741+0800 Demo[6117:1349426] 即将调用 Person 类的 speak:eat: 方法
2020-05-09 16:16:36.938236+0800 Demo[6117:1349426] Person speak: hello, eat: food
2020-05-09 16:16:36.938373+0800 Demo[6117:1349426] 已经调用 Person 类的 speak:eat: 方法

如果上述例子中被hook的实例方法变为类方法,对应的代码有两处不同:

  1. 获取 Method 的接口变为:class_getClassMethod
  2. 类方法保存在元类的 method-list 中,向一个类中动态添加类方法,就是将方法添加到元类 Meta Class 中,上述代码改动两行即可:
1
2
Class meta_class = objc_getMetaClass(class_getName(originClass));
class_addMethod(meta_class, swizzSel, method_getImplementation(swizzClass_swizzMethod), method_getTypeEncoding(swizzClass_swizzMethod));


3. 数组越界防护.

数组/字典是以类蔟方式实现的,NSArray 和 NSMutableArray 并不是数组真实的类名。在进行hook时注意需要拿到真正的类名称。因为在苹果内部实现中,其真实的类名是有可能发生变化的,因此不建议写死。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

#import "NSArray+Safe.h"
#import <objc/runtime.h>

@implementation NSArray (Safe)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray *arr = [NSArray array];
Class cls = NSClassFromString(arr.className);
NSMutableArray *muArr = [NSMutableArray array];
Class cls_mu = NSClassFromString(muArr.className);

Method origin_method = class_getInstanceMethod(cls, @selector(objectAtIndex:));
Method swizz_method = class_getInstanceMethod(cls, @selector(safeObjectAtIndex:));
method_exchangeImplementations(origin_method, swizz_method);

Method origin_method_mu = class_getInstanceMethod(cls_mu, @selector(objectAtIndex:));
Method swizz_method_mu = class_getInstanceMethod(cls_mu, @selector(safeMutableObjectAtIndex:));
method_exchangeImplementations(origin_method_mu, swizz_method_mu);

//数组下标
Method origin_method_sub = class_getInstanceMethod(cls, @selector(objectAtIndexedSubscript:));
Method swizz_method_sub = class_getInstanceMethod(cls, @selector(safeObjectAtIndexedSubscript:));
method_exchangeImplementations(origin_method_sub, swizz_method_sub);

Method origin_method_muta_sub = class_getInstanceMethod(cls_mu, @selector(objectAtIndexedSubscript:));
Method swizz_method_muta_sub = class_getInstanceMethod(cls_mu, @selector(safeMutableObjectAtIndexedSubscript:));
method_exchangeImplementations(origin_method_muta_sub, swizz_method_muta_sub);

});
}


- (id)safeObjectAtIndex:(NSUInteger)index {
if (self.count > index && self.count) {
return [self safeObjectAtIndex:index];
}
NSLog(@"safeObjectAtIndex: error , out of index");
return nil;
}

- (id)safeMutableObjectAtIndex:(NSUInteger)index {
if (self.count > index && self.count) {
return [self safeMutableObjectAtIndex:index];
}
NSLog(@"safeMutableObjectAtIndex: error , out of index");
return nil;
}



- (id)safeObjectAtIndexedSubscript:(NSUInteger)index {
if (self.count > index && self.count) {
return [self safeObjectAtIndexedSubscript:index];
}
NSLog(@"safeObjectAtIndexedSubscript: error, out of index");
return nil;
}

- (id)safeMutableObjectAtIndexedSubscript:(NSUInteger)index {
if (self.count > index && self.count) {
return [self safeMutableObjectAtIndexedSubscript:index];
}
NSLog(@"safeMutableObjectAtIndexedSubscript: error, out of index");
return nil;
}

@end

⚠️ Method swizzling 的危险性

在项目中还是要慎重使用的,使用不当带来的问题,将会让你付出很大代价去排查。Method swizzling 的危险性可以参考以下文章:

iOS 界的毒瘤:Method Swizzle

Objective-C Method Swizzling

What are the Dangers of Method Swizzling in Objective-C?

神经病院Objective-C Runtime出院第三天——如何正确使用Runtime