您的位置:首页 > 其它

Method Swizzling的各种姿势

2016-08-30 09:45 316 查看


因为Objective-C的runtime机制, Method Swizzling这个黑魔法解决了我们实际开发中诸多常规手段所无法解决的问题, 比如代码的插桩,Hook,Patch等等. 我们首先看看常规的Method Swizzling是怎样用的, NSHipster有一篇介绍基本用法的文章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

#import

@implementation UIViewController (Tracking)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&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);

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);
}

@end

简要说明一下以上代码的几个重点:

通过在Category的
+ (void)load
方法中添加Method Swizzling的代码,在类初始加载时自动被调用,load方法按照父类到子类,类自身到Category的顺序被调用.

在dispatch_once中执行Method Swizzling是一种防护措施,以保证代码块只会被执行一次并且线程安全,不过此处并不需要,因为当前Category中的load方法并不会被多次调用.

尝试先调用class_addMethod方法,以保证即便originalSelector只在父类中实现,也能达到Method Swizzling的目的.

xxx_viewWillAppear:
方法中
[self xxx_viewWillAppear:animated];
代码并不会造成死循环,因为Method Swizzling之后, 调用
xxx_viewWillAppear:
实际执行的代码已经是原来viewWillAppear中的代码了.

其实以上的代码也可以简写为以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

+ (void)load {
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);
if (!originalMethod || !swizzledMethod) {
return;
}

IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);

class_replaceMethod(class,originalSelector,swizzledIMP,swizzledType);
class_replaceMethod(class,swizzledSelector,originalIMP,originalType);
}

这是因为
class_replaceMethod
方法其实能够覆盖到
class_addMethod
method_setImplementation
两种场景, 对于第一个
class_replaceMethod
来说,
如果
viewWillAppear:
实现在父类, 则执行
class_addMethod
, 否则就执行
method_setImplementation
将原方法的IMP指定新的代码块; 而第二个
class_replaceMethod
完成的工作便只是将新方法的IMP指向原来的代码.

除了以上的场景之外,其它场景下我们如何使用Method Swizzling呢?


1.在不同类之间实现Method Swizzling

上面示例是通过Category来新增一个方法然后实现Method Swizzling的, 但有一些场景可能并不适合使用Category(比如私有的类,未获取到该类的声明), 此时我们应该如何来做Method Swizzling呢?

例如已知一个className为
Car
的类中有一个实例方法
- (void)run:(double)speed
, 目前需要Hook该方法对速度小于120才执行run的代码, 按照方法交换的流程, 代码应该是这样的:
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
4041
42
43
44
45
46
47
48
49
50
51
52

#import

@interface MyCar : NSObject
@end

@implementation MyCar

+ (void)load {
Class originalClass = NSClassFromString(@"Car");
Class swizzledClass = [self class];
SEL originalSelector = NSSelectorFromString(@"run:");
SEL swizzledSelector = @selector(xxx_run:);
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);

// 向Car类中新添加一个xxx_run:的方法
BOOL registerMethod = class_addMethod(originalClass,
swizzledSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (!registerMethod) {
return;
}

// 需要更新swizzledMethod变量,获取当前Car类中xxx_run:的Method指针
swizzledMethod = class_getInstanceMethod(originalClass, swizzledSelector);
if (!swizzledMethod) {
return;
}

// 后续流程与之前的一致
BOOL didAddMethod = class_addMethod(originalClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(originalClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

- (void)xxx_run:(double)speed {
if (speed < 120) {
[self xxx_run:speed];
}
}

@end

与之前的流程相比,在前面添加了两个逻辑:

利用runtime向目标类
Car
动态添加了一个新的方法,此时
Car
类与
MyCar
类一样具备了
xxx_run:
这个方法,
MyCar
的利用价值便结束了;

为了完成后续
Car
类中
run:
xxx_run:
的方法交换,此时需要更新swizzledMethod变量为
Car
中的
xxx_run:
方法所对应的Method.

以上所有的逻辑也可以合并简化为以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

+ (void)load {
Class originalClass = NSClassFromString(@"Car");
Class swizzledClass = [self class];
SEL originalSelector = NSSelectorFromString(@"run:");
SEL swizzledSelector = @selector(xxx_run:);
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);

IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);

class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}

简化后的代码便与之前使用Category的方式并没有什么差异, 这样代码就很容易覆盖到这两种场景了, 但我们需要明确此时
class_replaceMethod
所完成的工作却是不一样的.

第一个
class_replaceMethod
与之前的逻辑一致, 当
run:
方法是实现在
Car
类或
Car
的父类, 分别执行
method_setImplementation
class_addMethod
;

第二个
class_replaceMethod
则直接在
Car
类中注册了
xxx_run:
方法, 并且指定的IMP为当前
run:
方法的IMP;


2.如何实现类方法的Method Swizzling

以上的代码都是实现的对实例方法的交换, 那如何来实现对类方法的交换呢, 依旧直接贴代码吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1920
21
22
23
24
25
26
27
28
29
30
31
32
33
34

@interface NSDictionary (Test)
@end

@implementation NSDictionary (Test)

+ (void)load {
Class cls = [self class];
SEL originalSelector = @selector(dictionary);
SEL swizzledSelector = @selector(xxx_dictionary);

// 使用class_getClassMethod来获取类方法的Method
Method originalMethod = class_getClassMethod(cls, originalSelector);
Method swizzledMethod = class_getClassMethod(cls, swizzledSelector);
if (!originalMethod || !swizzledMethod) {
return;
}

IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);

// 类方法添加,需要将方法添加到MetaClass中
Class metaClass = objc_getMetaClass(class_getName(cls));
class_replaceMethod(metaClass,originalSelector,swizzledIMP,swizzledType);
class_replaceMethod(metaClass,swizzledSelector,originalIMP,originalType);
}

+ (id)xxx_dictionary {
id result = [self xxx_dictionary];
return result;
}

@end

相比实例方法的Method Swizzling,流程有两点差异:

获取Method的方法变更为
class_getClassMethod(Class cls, SEL name)
,从函数命名便直观体现了和
class_getInstanceMethod(Class cls, SEL name)
的差别;

对于类方法的动态添加,需要将方法添加到MetaClass中,因为实例方法记录在class的method-list中, 类方法是记录在meta-class中的method-list中的.


3.在类簇中如何实现Method Swizzling

在上面的代码中我们实现了对
NSDictionary
中的
+ (id)dictionary
方法的交换,但如果我们用类似代码尝试对
- (id)objectForKey:(id)key
方法进行交换后, 你便会发现这似乎并没有什么用.

这是为什么呢? 平常我们在Xcode调试时,在下方Debug区域左侧的Variables View中,常常会发现如
__NSArrayI
或是
__NSCFConstantString
这样的Class类型, 这便是在Foundation框架中被广泛使用的类簇, 详情请参看Apple文档class
cluster的内容.

所以针对类簇的Method Swizzling问题就转变为如何对这些类簇中的私有类做Method Swizzling, 在上面介绍的不同类之间做Method Swizzling便已经能解决该问题, 下面一个简单的示例通过交换
NSMutableDictionary
setObject:forKey:
方法,让调用这个方法时当参数object或key为空的不会抛出异常:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1920
21
22
23
24
25
26
27
28
29
30
31
32

@interface MySafeDictionary : NSObject
@end

@implementation MySafeDictionary

+ (void)load {
Class originalClass = NSClassFromString(@"__NSDictionaryM");
Class swizzledClass = [self class];
SEL originalSelector = @selector(setObject:forKey:);
SEL swizzledSelector = @selector(safe_setObject:forKey:);
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);

IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);

class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}

- (void)safe_setObject:(id)anObject forKey:(id)aKey {
if (anObject && aKey) {
[self safe_setObject:anObject forKey:aKey];
}
else if (aKey) {
[(NSMutableDictionary *)self removeObjectForKey:aKey];
}
}

@end


4.在Method Swizzling之后如何恢复

使用了Method Swizzling的各种姿势之后, 是否有考虑如何恢复到交换之前的现场呢?

一种方案就是通过一个开关标识符, 如果需要从逻辑上面恢复到交换之前, 就设置一下这个标识符, 在实现中判定如果设定了该标识符, 逻辑就直接调用原方法的实现, 其它什么事儿也不干, 这是目前大多数代码的实现方法, 当然也是非常安全的方式, 只不过当交换方法过多时, 每一个交换的方法体中都需要增加这样的逻辑, 并且也需要维护大量这些标识符变量, 只是会觉得不够优雅, 所以此处也就不展开详细讨论了.

那下面来讨论一下有没有更好的方案, 以上描述的Method Swizzling各种场景和处理的技巧, 但综合总结之后最核心的其实也只做了两件事情:

class_addMethod 添加一个新的方法, 可能是把其它类中实现的方法添加到目标类中, 也可能是把父类实现的方法添加一份在子类中, 可能是添加的实例方法, 也可能是添加的类方法, 总之就是添加了方法.

交换IMP 交换方法的实现IMP,完成这个步骤除了使用
method_exchangeImplementations
这个方法外, 也可以是调用了
method_setImplementation
方法来单独修改某个方法的IMP, 或者是采用在调用
class_addMethod
方法中设定了IMP而直接就完成了IMP的交换,
总之就是对IMP的交换.

那我们来分别看一下这两件事情是否都还能恢复:

对于
class_addMethod
, 我们首先想到的可能就是有没有对应的remove方法呢, 在Objective-C 1.0的时候有
class_removeMethods
这个方法, 不过在2.0的时候就已经被禁用了, 也就是苹果并不推荐我们这样做, 想想似乎也是挺有道理的, 本来runtime的接口看着就挺让人心惊胆战的, 又是添加又是删除总觉得会出岔子,
所以只能放弃remove的想法, 反正方法添加在那儿倒也没什么太大的影响.

针对IMP的交换, 在Method Swizzling时做的交换动作, 如果需要恢复其实要做的动作还是交换回来罢了, 所以是可以做到的, 不过需要怎样做呢? 对于同一个类, 同一个方法, 可能会在不同的地方被多次做Method Swizzling, 所以要回退某一次的Method Swizzling, 我们就需要记录下来这一次交换的时候是哪两个IMP做了交换, 恢复的时候再换回来即可. 另一个问题是如果已经经过多次交换, 我们怎样找到这两个IMP所对应的Mehod呢, 还好runtime提供了一个
class_copyMethodList
方法,
可以直接取出Method列表, 然后我们就可以逐个遍历找到IMP所对应的Method了, 下面是对上一个示例添加恢复之后实现的代码逻辑:

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
4041
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

#import

@interface MySafeDictionary : NSObject
@end

static NSLock *kMySafeLock = nil;
static IMP kMySafeOriginalIMP = NULL;
static IMP kMySafeSwizzledIMP = NULL;

@implementation MySafeDictionary

+ (void)swizzlling {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
kMySafeLock = [[NSLock alloc] init];
});

[kMySafeLock lock];

do {
if (kMySafeOriginalIMP || kMySafeSwizzledIMP) break;

Class originalClass = NSClassFromString(@"__NSDictionaryM");
if (!originalClass) break;

Class swizzledClass = [self class];
SEL originalSelector = @selector(setObject:forKey:);
SEL swizzledSelector = @selector(safe_setObject:forKey:);
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
if (!originalMethod || !swizzledMethod) break;

IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);

kMySafeOriginalIMP = originalIMP;
kMySafeSwizzledIMP = swizzledIMP;

class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
} while (NO);

[kMySafeLock unlock];
}

+ (void)restore {
[kMySafeLock lock];

do {
if (!kMySafeOriginalIMP || !kMySafeSwizzledIMP) break;

Class originalClass = NSClassFromString(@"__NSDictionaryM");
if (!originalClass) break;

unsigned int outCount = 0;
Method *methodList = class_copyMethodList(originalClass, &outCount);
for (unsigned int idx=0; idx < outCount; idx++) {
Method aMethod = methodList[idx];
IMP aIMP = method_getImplementation(aMethod);
if (aIMP == kMySafeSwizzledIMP) {
method_setImplementation(aMethod, kMySafeOriginalIMP);
}
else if (aIMP == kMySafeOriginalIMP) {
method_setImplementation(aMethod, kMySafeSwizzledIMP);
}
}
kMySafeOriginalIMP = NULL;
kMySafeSwizzledIMP = NULL;
} while (NO);

[kMySafeLock unlock];
}

- (void)safe_setObject:(id)anObject forKey:(id)aKey {
if (anObject && aKey) {
[self safe_setObject:anObject forKey:aKey];
}
else if (aKey) {
[(NSMutableDictionary *)self removeObjectForKey:aKey];
}
}

@end

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: