Method Swizzling 的正确途径

  1. 在Objective-C 中方法交换有什么危险
    1. Swizzling 改变方法的参数 例子
      1. 正确的hook方式
  2. RSSwizzle
    1. 那么影响Swizzle的结果到底是是什么呢?
  3. RSSwizzle 加锁,保证线程安全
  4. 采用Block添加实现,没有命名冲突问题
  5. 采用block 添加实现,只是改变了原来的IMP ,Selector没有改变,实现的_cmd并没有改变

iOS 平台开发,有时会使用到Method Swizzling, 但Method Swizzling 在使用过程中有许多需要注意的问题,本文将介绍将会产生的问题,并且分析 RSSwizzle 是如何解决这些问题的。

在Objective-C 中方法交换有什么危险

What are the Dangers of Method Swizzling in Objective C?
stackoverflow 上的这个回答十分精彩。

- Method swizzling 并不是原子操作
- 改变了不是我们自己代码的行为
- 有可能出现命名冲突
- Swizzling 改变方法的参数
- Swizzles 顺序问题
- 难于理解
- 难于Debug

Swizzling 改变方法的参数 例子

使用method_exchangeImplementations更改方法的实现,会导致一个问题,origin_imp 如果使用了 _cmd 参数,hook之后的_cmd 是不符合预期的。

hook touchesBegan 过的同学应该遇到这种问题。

1
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

这个函数里面 调用了 forwardTouchMethod , 反汇编后类似这种。

1
2
3
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
forwardTouchMethod(self, _cmd, touches, event);
}
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
static void forwardTouchMethod(id self, SEL _cmd, NSSet *touches, UIEvent *event) {
// The responder chain is used to figure out where to send the next touch
UIResponder *nextResponder = [self nextResponder];
if (nextResponder && nextResponder != self) {
// Not all touches are forwarded - so we filter here.
NSMutableSet *filteredTouches = [NSMutableSet set];
[touches enumerateObjectsUsingBlock:^(UITouch *touch, BOOL *stop) {
// Checks every touch for forwarding requirements.
if ([touch _wantsForwardingFromResponder:self toNextResponder:nextResponder withEvent:event]) {
[filteredTouches addObject:touch];
}else {
// This is interesting legacy behavior. Before iOS 5, all touches are forwarded (and this is logged)
if (!_UIApplicationLinkedOnOrAfter(12)) {
[filteredTouches addObject:touch];
// Log old behavior
static BOOL didLog = 0;
if (!didLog) {
NSLog(@"Pre-iOS 5.0 touch delivery method forwarding relied upon. Forwarding -%@ to %@.", NSStringFromSelector(_cmd), nextResponder);
}
}
}
}];
// here we basically call [nextResponder touchesBegan:filteredTouches event:event];
[nextResponder performSelector:_cmd withObject:filteredTouches withObject:event];
}
}

如果我们exchange了 imp, [nextResponder performSelector:_cmd withObject:filteredTouches withObject:event]; 是没有相应的实现的,_cmd 就变成了 我们替换的 sel. 显然,nextResponder没有实现相应的方法,就会crash。

正确的hook方式

方案一:
直接替换 method 的 IMP.
method_setImplementation, 在新的IMP中调用原始的 IMP.

RSSwizzle

RSSwizzle 实现方式

1. 根据block生成NEW IMP
2. replace 目标方法的实现
3. block可以获取原来的IMP

核心交换代码

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
static void swizzle(Class classToSwizzle,
SEL selector,
RSSwizzleImpFactoryBlock factoryBlock)
{
Method method = class_getInstanceMethod(classToSwizzle, selector);
NSCAssert(NULL != method,
@"Selector %@ not found in %@ methods of class %@.",
NSStringFromSelector(selector),
class_isMetaClass(classToSwizzle) ? @"class" : @"instance",
classToSwizzle);
NSCAssert(blockIsAnImpFactoryBlock(factoryBlock),
@"Wrong type of implementation factory block.");
__block OSSpinLock lock = OS_SPINLOCK_INIT;
// To keep things thread-safe, we fill in the originalIMP later,
// with the result of the class_replaceMethod call below.
__block IMP originalIMP = NULL;
// This block will be called by the client to get original implementation and call it.
RSSWizzleImpProvider originalImpProvider = ^IMP{
// It's possible that another thread can call the method between the call to
// class_replaceMethod and its return value being set.
// So to be sure originalIMP has the right value, we need a lock.
OSSpinLockLock(&lock);
IMP imp = originalIMP;
OSSpinLockUnlock(&lock);
if (NULL == imp){
// If the class does not implement the method
// we need to find an implementation in one of the superclasses.
Class superclass = class_getSuperclass(classToSwizzle);
imp = method_getImplementation(class_getInstanceMethod(superclass,selector));
}
return imp;
};
RSSwizzleInfo *swizzleInfo = [RSSwizzleInfo new];
swizzleInfo.selector = selector;
swizzleInfo.impProviderBlock = originalImpProvider;
// We ask the client for the new implementation block.
// We pass swizzleInfo as an argument to factory block, so the client can
// call original implementation from the new implementation.
id newIMPBlock = factoryBlock(swizzleInfo);
const char *methodType = method_getTypeEncoding(method);
NSCAssert(blockIsCompatibleWithMethodType(newIMPBlock,methodType),
@"Block returned from factory is not compatible with method type.");
IMP newIMP = imp_implementationWithBlock(newIMPBlock);
// Atomically replace the original method with our new implementation.
// This will ensure that if someone else's code on another thread is messing
// with the class' method list too, we always have a valid method at all times.
//
// If the class does not implement the method itself then
// class_replaceMethod returns NULL and superclasses's implementation will be used.
//
// We need a lock to be sure that originalIMP has the right value in the
// originalImpProvider block above.
OSSpinLockLock(&lock);
// originIMP get value from here
originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType);
OSSpinLockUnlock(&lock);
}

具体过程解析

如果hook的方法在hook的类中有实现

1. block生成新的IMP
2. 替换IMP, 这时候拿到原始的ORIGINIMP
3. block接受了一个RSSwizzleInfo参数,从参数中可以拿到当时保存的获得IMP的block
4. 由于block中存储的是originIMP ,所以获得的是原始的实现

如果hook的方法在子类中无实现

1. block生成新的IMP
2. 替换IMP(由于没有实现,相当于add了IMP), 原始的实现为nil
3. block 中我们调用calloriginIMP,这个方法实际调用了一个block originalImpProvider
4. 这个block的从父类找到相应的实现(注意,这里实际上是在 调用方法 时才会触发)

这种情况是调用时,动态获得 当时的方法实现,所以可以避免hook顺序带来的问题。

那么影响Swizzle的结果到底是是什么呢?

Swizzle实现方式本质上就是改变方法的IMP 为 NewIMP, 并调用原先的originIMP

只hook一个是没问题,但是涉及到多次hook, 并且hook的方法可能为一个时, 他们的顺序就会导致不同的结果,因为顺序不同,Swizzle时,Method 相应的 IMP 不相同。
这里我们更关注,父子类+hook 同一个方法产生的问题。

那么RSSwizzle 怎么解决问题的呢?

父类有method, 子类没有实现method.
我们有如下的IMP:superIMP,superNewIMP,subNewIMp
此时,我们Swizzle 父类的method 为 superNewIMP
Swizzle子类的method 为 subNewIMp

首先关注我们期望的调用顺序

1. subNewIMP
2. superNewIMP
3. superIMP

我们先hook父类,再hook子类后,的调用顺序

1. subNewImp
2. superNewIMP
3. superIMp

先hook子类再hook父类

1. subNewIMP
2. superIMP

为什么会有差异?
因为当我们在hook子类方法时,原先的方法实现是不同的。

解决问题
那就要保证,即使hook的顺序不同,也能正确取到相应的IMP
那我们保证,子类在调用相应方法的时候,取到的IMP是父类当前的IMP就可以(这样就和Swizzle的时间顺序没有了关系)

RSSwizzle 加锁,保证线程安全

originalImpProvider 的代码

1
2
3
4
5
6
7
8
9
10
RSSWizzleImpProvider originalImpProvider = ^IMP{
OSSpinLockLock(&lock);
IMP imp = originalIMP;
OSSpinLockUnlock(&lock);
if (NULL == imp){
Class superclass = class_getSuperclass(classToSwizzle);
imp = method_getImplementation(class_getInstanceMethod(superclass,selector));
}
return imp;
}

Swizzle 方法的某个部分

1
2
3
OSSpinLockLock(&lock);
originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType);
OSSpinLockUnlock(&lock);

这两个方法有个共享变量 originalIMP,这就意味着,可能会出现线程安全问题。再仔细看下代码

1
2
3
4
OSSpinLockLock(&lock);
IMP imp = originalIMP;
OSSpinLockUnlock(&lock);
if (NULL == imp){

这个共享变量,和条件判断相关。敏锐的同学一眼就能看出来,在不加锁的情况下,当不同的线程对 这两段代码进行执行的时候,就会出现,即使if (NULL == imp){通过了,但实际上,另一条线程执行了class_replaceMethod()。这时就会出现问题。

在加锁之后,在同一时间段内,只有一个线程能访问改变这个变量的代码。避免了共享变量导致的线程安全问题。

采用Block添加实现,没有命名冲突问题

连命名的机会都没有…

1
2
3
4
5
6
7
8
9
10
11
12
13
RSSwizzleInstanceMethod(classToSwizzle,
@selector(calculate:),
RSSWReturnType(int),
RSSWArguments(int number),
RSSWReplacement(
{
// The following code will be used as the new implementation.
// Calling original implementation.
int res = RSSWCallOriginal(number);
// Returning modified return value.
return res + 1;
}), 0, NULL);

采用block 添加实现,只是改变了原来的IMP ,Selector没有改变,实现的_cmd并没有改变

参数_cmd是当前方法的selector

swizzle method 可能会导致的_cmd 参数改变,例如
我们有originMethodnewMethod,他们分别对应着 OriginIMP,和NewIMP.
当我们交换方法实现后:

1
2
originmethod -> NewIMP
newMethod -> OriginIMP

要想调用原来的实现,我们需要调用 newMethod 这就导致了 相同的IMP 但是_cmd 却改变了

script>