
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 |
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { |
1 |
static void forwardTouchMethod(id self, SEL _cmd, NSSet *touches, UIEvent *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 |
static void swizzle(Class classToSwizzle, |
具体过程解析
如果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 |
RSSWizzleImpProvider originalImpProvider = ^IMP{ |
Swizzle 方法的某个部分
1 |
OSSpinLockLock(&lock); |
这两个方法有个共享变量 originalIMP,这就意味着,可能会出现线程安全问题。再仔细看下代码
1 |
OSSpinLockLock(&lock); |
这个共享变量,和条件判断相关。敏锐的同学一眼就能看出来,在不加锁的情况下,当不同的线程对 这两段代码进行执行的时候,就会出现,即使if (NULL == imp){通过了,但实际上,另一条线程执行了class_replaceMethod()。这时就会出现问题。
在加锁之后,在同一时间段内,只有一个线程能访问改变这个变量的代码。避免了共享变量导致的线程安全问题。
采用Block添加实现,没有命名冲突问题
连命名的机会都没有…
1 |
RSSwizzleInstanceMethod(classToSwizzle, |
采用block 添加实现,只是改变了原来的IMP ,Selector没有改变,实现的_cmd并没有改变
参数_cmd是当前方法的selector
swizzle method 可能会导致的_cmd 参数改变,例如
我们有originMethod和newMethod,他们分别对应着 OriginIMP,和NewIMP.
当我们交换方法实现后:
1 |
originmethod -> NewIMP |
要想调用原来的实现,我们需要调用 newMethod 这就导致了 相同的IMP 但是_cmd 却改变了




近期评论