iOS中的黑魔法:Method Swizzling

什么是Method Swizzling

Objective-C对象在收到消息之后会经过消息发送系统来进行处理,该系统会查出消息对应的方法并执行其代码。那么对于给定@selector名称相对应的方法是否可以在运行期可以动态改变呢?如果能善用这个特性,则可发挥出巨大优势,因为我们可以不需要源码也不需要通过继承子类来覆写对应的方法就能改变这个类本身的功能。

Objective-C中确实提供了这样的操作,这就是我们这里会介绍到的Method Swizzling。在Objective-C中每个类都有一个方法列表,类的方法列表会把@selector映射到相关的方法实现之上,使得消息发送系统能够根据这个找到应该调用的方法。

Method Swizzling 的原理

Method Swizzling 是一把双刃剑,使用得当可以让我们非常轻松地实现复杂的功能,而如果一旦误用,它也很可能会给我们的程序带来毁灭性的伤害。但是我们大可不必惊慌,在了解了它的实现原理后,我们就可以“信手拈来”了。

我们先来了解下 Objective-C 中方法 Method 的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct method_t *Method;
struct method_t {
SEL name;
const char *types;
IMP imp;

struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};

本质上,它就是 struct method_t 类型的指针,所以我们重点看下结构体 method_t 的定义。在结构体 method_t 中定义了三个成员变量和一个成员函数

name表示的是方法的名称,用于唯一标识某个方法,比如 @selector(viewWillAppear:) ;

types表示的是方法的返回值和参数类型

imp是一个函数指针,指向方法的实现;

SortBySELAddress顾名思义,是一个根据 name 的地址对方法进行排序的函数。

由此,我们也可以发现 Objective-C 中的方法名是不包括参数类型的,也就是说下面两个方法在 runtime 看来就是同一个方法:

1
2
- (void)viewWillAppear:(BOOL)animated;
- (void)viewWillAppear:(NSString *)string;

而下面两个方法却是可以共存的

1
2
- (void)viewWillAppear:(BOOL)animated;
+ (void)viewWillAppear:(BOOL)animated;

因为实例方法和类方法是分别保存在类对象和元类对象中的

原则上,方法的名称 name 和方法的实现 imp 是一一对应的,而 Method Swizzling 的原理就是动态地改变它们的对应关系,以达到替换方法实现的目的。

Method Swizzling 实践

首先来看看Method Swizzling相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获取通过SEL获取一个方法
class_getInstanceMethod

//获取一个方法的实现
method_getImplementation

//获取一个OC实现的编码类型
method_getTypeEncoding

//給方法添加实现
class_addMethod

//用一个方法的实现替换另一个方法的实现
class_replaceMethod

//交换两个方法的实现
method_exchangeImplementations

下面来看一个例子

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
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//case1: 替换实例方法
Class selfClass = [self class];
//case2: 替换类方法
Class selfClass = object_getClass([self class]);

//源方法的SEL和Method
SEL oriSEL = @selector(viewWillAppear:);
Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);

//交换方法的SEL和Method
SEL cusSEL = @selector(customViewWillApper:);
Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);

//先尝试給源方法添加实现,这里是为了避免源方法没有实现的情况
BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
if (addSucc) {
//添加成功:将源方法的实现替换到交换方法的实现
class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else {
//添加失败:说明源方法已经有实现,直接将两个方法的实现交换即可
method_exchangeImplementations(oriMethod, cusMethod);
}
});
}

要注意的地方

  1. 在 +load 方法中实现 Method Swizzling 的逻辑.
    +load 和 +initialize 是 Objective-C runtime 会自动调用的两个类方法。但是它们被调用的时机却是有差别的,+load 方法是在类被加载的时候调用的,而 +initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 +initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize 方法是永远不会被调用的。此外 +load 方法还有一个非常重要的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。换句话说在 Objective-C runtime 自动调用 +load 方法时,分类中的 +load 方法并不会对主类中的 +load 方法造成覆盖。综上所述,+load 方法是实现 Method Swizzling 逻辑的最佳“场所”。

  2. 方法交换应该保证唯一性和原子性
    唯一性:应该尽可能在+load方法中实现,这样可以保证方法一定会调用且不会出现异常。
    原子性:使用dispatch_once来执行方法交换,这样可以保证只运行一次。

  3. 方法名必须不能产生冲突

  4. 尽量少用方法交换
    虽然方法交换可以让我们高效地解决问题,但是如果处理不好,可能会导致一些莫名其妙的bug

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
@implementation UIImage (Hook)

+ (void)load {

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

Class selfClass = object_getClass([self class]);

SEL oriSEL = @selector(imageNamed:);
Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);

SEL cusSEL = @selector(myImageNamed:);
Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);

BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
if (addSucc) {
class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else {
method_exchangeImplementations(oriMethod, cusMethod);
}

});
}

+ (UIImage *)myImageNamed:(NSString *)name {

NSString * newName = [NSString stringWithFormat:@"%@%@", @"new_", name];
return [self myImageNamed:newName];
}

@end

在+load方法中将imageNamed和myImageNamed的实现交换了,现在的状态变成了
imageNamed -> myImageNamed(IMP)
myImageNamed -> imageNamed(IMP)

  • 数据统计
    例如需要跟踪记录APP中按钮的点击次数和频率等数据
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
@implementation UIButton (Hook)

+ (void)load {

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

Class selfClass = [self class];

SEL oriSEL = @selector(sendAction:to:forEvent:);
Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);

SEL cusSEL = @selector(mySendAction:to:forEvent:);
Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);

BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
if (addSucc) {
class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else {
method_exchangeImplementations(oriMethod, cusMethod);
}

});
}

- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[CountTool addClickCount];
[self mySendAction:action to:target forEvent:event];
}

@end

Method Swizzling方法封装

如果使用的地方比较多的话,封装起来使用还是比较方便的

1
2
3
4
5
6
7
8
9
10
11
12
+ (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel {

Method origMethod = class_getInstanceMethod(class, origSel);
Method swizMethod = class_getInstanceMethod(class, swizSel);

BOOL didAddMethod = class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod));
if (didAddMethod) {
class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
} else {
method_exchangeImplementations(origMethod, swizMethod);
}
}

Method Swizzling 是一种黑魔法,我们在使用它时需要加倍小心。

参考

iOS runtime实战应用:Method Swizzling

Objective-C Method Swizzling 的最佳实践