Method Swizzling与JSPatch探究(二)
以下笔记整理于2017-03-22
当时作为无埋点预研的学习整理,由于小组是客户端和前端混合的小组,所以下面尽可能以偏简单的文字在组内分享。
第一部分地址为:Method Swizzling与JSPatch探究(一)
4、兼容性
以hook UIViewController
的viewWillAppear
的方法为例
4.1 仅method Swizzling, 无JSPatch
`ViewController:UIViewController`
- (void)viewWillAppear:(BOOL)animated {
NSLog(@"HELLO 01");
[super viewWillAppear:animated];
NSLog(@"HELLO 02");
}
UIViewController+MLPF.m
- (void)xxx_viewWillAppear:(BOOL)animated {
NSLog(@"HELLO Swizzle01");
[self xxx_viewWillAppear:animated] ;
NSLog(@"HELLO Swizzle02");
}
Output:
HELLO 01
HELLO Swizzle01
HELLO Swizzle02
HELLO 02
IMP变化:注意hook的是UIViewController的viewWillAppear而不是ViewController
调用路径:
4.2 method Swizzling和JSPatch一起,JSPatch覆盖
为了避免swizzle在runtime期间出现诡异的异常,一般会写在+load里面,先于JSPatch加载
- JSPatch,js文件,覆盖原方法
defineClass("ViewController", {
viewWillAppear: function(animated) {
console.log("JSPatch 01");
self.super().viewWillAppear(animated);
console.log("JSPatch 02");
}
}, {});
output:
JSPatch.log: JSPatch 01
HELLO Swizzle01
HELLO Swizzle02
JSPatch.log: JSPatch 02
IMP变化:在之前的前一步的基础上
调用路径
4.3 method Swizzling和JSPatch一起,JSPatch调用原方法
defineClass("ViewController", {
viewWillAppear: function(animated) {
console.log("JSPatch 01");
self.ORIGviewWillAppear(animated);
console.log("JSPatch 02");
}
}, {});
Output:
JSPatch.log: JSPatch 01
HELLO 01
HELLO Swizzle01
HELLO Swizzle02
HELLO 02
JSPatch.log: JSPatch 02
- IMP变化:与前一步一致
- 调用路径:相对之前略复杂,因此打印的地方也附上
4.4 重复hook同一个方法,无JSPatch
重复hook:两处地方都对UIViewController的viewWillAppear进行method swizzle
hook顺序:先UIViewController+MLPF
,后UIViewController+MLPF2
`ViewController:UIViewController`
- (void)viewWillAppear:(BOOL)animated {
NSLog(@"HELLO 01");
[super viewWillAppear:animated];
NSLog(@"HELLO 02");
}
UIViewController+MLPF.m
- (void)xxx_viewWillAppear:(BOOL)animated {
NSLog(@"HELLO Swizzle1-1");
[self xxx_viewWillAppear:animated] ;
NSLog(@"HELLO Swizzle1-2");
}
UIViewController+MLPF02.m
- (void)xxx02_viewWillAppear:(BOOL)animated {
NSLog(@"HELLO Swizzle2-1");
[self xxx02_viewWillAppear:animated];
NSLog(@"HELLO Swizzle2-2");
}
Output:
HELLO 01
HELLO Swizzle2-1
HELLO Swizzle1-1
HELLO Swizzle1-2
HELLO Swizzle2-2
HELLO 02
IMP变化:为什么这样改变前文已经说明
调用路径:
4.5 重复hook同一个方法和JSPatch的兼容性,JSPatch调用原方法
在4.4的基础上增加JSPatch,调用原方法
defineClass("ViewController", {
viewWillAppear: function(animated) {
console.log("JSPatch 01");
self.ORIGviewWillAppear(animated);
console.log("JSPatch 02");
}
}, {});
output:
JSPatch.log: JSPatch 01
HELLO 01
HELLO Swizzle2-1
HELLO Swizzle1-1
HELLO Swizzle1-2
HELLO Swizzle2-2
HELLO 02
JSPatch.log: JSPatch 02
IMP变化:
调用路径:经过前面几步的分析,这里实际上没什么难度
4.6 重复hook同一个方法和JSPatch的兼容性,JSPatch覆盖原方法
这里就不作累述了,经过前面的分析,个人觉得这部分直接给结果应该看得懂,因此这部分流程图就不画了
defineClass("ViewController", {
viewWillAppear: function(animated) {
console.log("JSPatch 01");
self.super().viewWillAppear(animated);
console.log("JSPatch 02");
}
}, {});
JSPatch.log: JSPatch 01
HELLO Swizzle2-1
HELLO Swizzle1-1
HELLO Swizzle1-2
HELLO Swizzle2-2
JSPatch.log: JSPatch 02
4.7 一点补充
hotfix的代码不要出现如下情况,调用原实现,又调super,原实现代码规范是存在调super的,这样会导致重复调用,在4.6的基础上如下试验
defineClass("ViewController", {
viewWillAppear: function(animated) {
console.log("JSPatch 01");
self.ORIGviewWillAppear(animated);
self.super().viewWillAppear(animated);
console.log("JSPatch 02");
}
}, {});
JSPatch.log: JSPatch 01
HELLO 01
HELLO Swizzle2-1
HELLO Swizzle1-1
HELLO Swizzle1-2
HELLO Swizzle2-2
HELLO 02
HELLO Swizzle2-1
HELLO Swizzle1-1
HELLO Swizzle1-2
HELLO Swizzle2-2
JSPatch.log: JSPatch 02
5、结论
实际上,理解了2、3对method swizzling 和 JSPatch hook的原理分析,第四部分的兼容性实验的结果大致也能猜的八九不离十。
因为两者不存在冲突的地方,唯一的重合点就是对函数实现指针的替换
两者共用时,hotfix的方法和method Swizzling hook的函数实现都会按顺序调用,并不会出现覆盖,或者出错的情况
下一步试验:
1、实际上Method Swizzling还是存在一些坑的,上面是常规的实验。后续会更深入一些
2、同时试验以属性的形式KVO监听,进行埋点试验(目前试验了一点,但是case还不全)
6、拓展
当前AOP热门的库Aspects,在hook方案上,跟JSPatch大同小异,但是最后一步在forwardinvocation
,Aspects则是保存block为关联属性。与JSPatch共用时在一些case下会产生crash 或者 覆盖执行等问题
关于Aspects和JSPatch兼容性的情况,这篇文章已经解释的较为详细,可供参考
http://www.jianshu.com/p/dc1deaa1b28e
7、也许你有疑问的地方:
主要针对第3部分的问题
Q:万一,原方法不存在怎么办?
A:实际上JSPatch源码实现,会调一个defineClass函数,这个函数最后都会调用overrideMethod
方法,而overrideMethod
会根据原类是否已定义过这个函数,来决定执行class_addMethod
还是class_replaceMethod
如果是add
那很简单,根本不存在是否兼容性的问题,所以并没有在上文指出
Q:_objc_msgForward
是什么鬼?
A:一个函数指针。发送消息的时候,其实相当于objc_msgSend()
执行的过程,这个函数会从Class的缓存中查找IMP,没找到则向父类Class查找,一直到根类都没找到,则会用_objc_msgForward
函数指针替代IMP。而_objc_msgForward
会尝试进行消息转发
贴一点runtime的伪代码,证明下这不是瞎逼逼
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
imp = lookUpImpOrNil(cls, sel, nil,
YES/*initialize*/, YES/*cache*/, YES/*resolver*/);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
Q:关于type encoding
A:为了协助运行时系统,编码器用字符串为每个方法的返回值和参数类型和方法选择器编码。官方文档:
To assist the runtime system, the compiler encodes the return and argument types for each method in a character string and associates the string with the method selector.
8、Last
有问题欢迎讨论
2017-03-22