研究版本官方最新版本Release 1.4.2
现象
- 在对Obj进行
先KVO再Hook
其setter函数后,调用obj的setter函数崩溃。 - 反之,若
先Hook再KVO
,则正常运行。
原因
1.先对比两种添加顺序,导致的obj
的isa
和函数列表
的不同。
通过下面的函数打印出,当前的obj→isa
及其函数列表
。
NSLog(@"\nclass - %@\nclass methods - %@", object_getClass(self.testObj), [self allMethodsWithClass:object_getClass(self.testObj)]);
NSLog(@"\nclass - %@\nclass methods - %@", self.testObj.class, [self allMethodsWithClass:self.testObj.class]);
打印结果去除时间戳信息,显示如下
-
先KVO再进行Hook
-
先hook再进行KVO
对比可以发现,如果先KVO再hook
,KVO会先动态生成一个NSKVONotifying_
前缀的子类,并且会重写setName:
,添加class
等函数,将class
的返回结果指向父类。 Aspect
在hook的时候,如果发现obj.class
和object_getClass(obj)
不相等的话(正常发生在对象被KVO重写过isa),则不会动态地再建立一个子类,而直接会原地hook,添加aspects__setName:
和forwardInvocation:
,交换aspects__setName:
和setName:
的函数实现。(此时交换的是已经被KVO过的NSKVONotifying_AKObject
的setName:
函数实现)
此时对name
进行赋值的时候,对obj
调用setName:
,在Aspect
中如果不是使用指定AspectPositionInstead
的option替换原方法实现,则会走原方法实现,则将对obj
调用交换后的方法名aspects__setName:
(对应原方法实现),NSKVONotifying_AKObject
在处理aspects__setName:
时候会调用其父类的方法,但是并不会保存父类原来的setter
的方法名,而是直接将自己当前的selector(_cmd)
当做方法名,并对父类AKObject进行调用,但是父类中并没有实现aspects__setName:
,所以产生崩溃。
对于先hook再进行KVO
的情况,setName:
原类及其两个生成的子类都有对应的方法实现,所以可以正常响应。
解决KVO及Aspects共存的PR
git地址:github.com/doggy/Aspec…
解决共用的途径
这个PR创造性的在调用原函数实现
那一步骤上做了优化,要调用原函数实现时,先交换回原始函数名及原始的函数实现
,再对obj
调用原函数名
,此时在NSKVONotifying_AKObject
的setName:
中拿到的_cmd
就是原始的selector
,父类中也有对应的方法实现,一切都能正常运行。然后再把invocation的selector
及对应子类中的方法实现
交换回去,完成对原始函数实现的调用。
存在的其他问题
1.先hook再KVO
,如果在清除KVO前对AspectToken进行remove,崩溃。
id token = [object aspect_hookSelector...];// add hook
[object addObserver...]; // add KVO
[token remove]; // crash
原因:在先hook再KVO
这过程中,obj.isa
经历的变化为
NSObject
→ NSObject_Aspects_
→ NSKVONotifying_NSObject_Aspects_
假如在移除KVO前移除Aspects的hook,Aspects需要把当前子类还原为原始父类,
Aspects的处理逻辑是去除_Aspects_
后缀,并将isa
指向去除后缀后的类,
那isa
的变化过程为
NSKVONotifying_NSObject_Aspects_
→ NSKVONotifying_NSObject
但是,此时程序中并没有存在NSKVONotifying_NSObject
这个类,因此程序会崩溃。(这个坑在最新版的Aspects中仍然存在,且暂时无解决方法)
2.如果是先KVO再Hook
的情况,要注意在obj
的dealloc
时才能remove调所有观察者,如果提前remove调所有观察者,hook是对NSKVONotifying_NSObject
的方法hook,但移除所有观察者后就会把对应的NSKVONotifying_NSObject
类销毁,此时isa
NSKVONotifying_NSObject
→ NSObject
原有的hook将会失效。
更好的hook方案
There's known issues with this approach, and to this date (February 2019) I STRICTLY DO NOT RECOMMEND TO USE Aspects IN PRODUCTION CODE.
既然,Aspects和KVO共存存在这么多坑,Aspects作者也不推荐在生产中使用该库。那我们有没有一个更好的代替者呢?
答案是来自饿了吗开源的Stinger。
Stinger使用libffi及解析方法签名构建壳函数,替换原方法实现以感知方法调用和捕获参数;使用同一cif模板及函数指针直接执行原实现和所有切面block。 Stinger不使用消息转发指针替换原实现,hook兼容性更好;调用方法不经过消息转发过程,执行原实现及切面代码过程中无手动构建invocation等,效率更高。
测试发现,stinger在与其他hook框架(如Aspects)共用的时候,具有良好的兼容性,并且解决了上述所讲的其他存在的问题。唯一存在的问题是先KVO再Hook
的情况下,由于经过KVO后,父类中不存在交换后函数名前缀为st_original_
的setter的函数实现,因此在这种情况下调用setter会引起崩溃。但这并不是Hook框架存在的问题,应尽量避免先KVO再Hook
的用法。
下面说说Stinger是如何解决与KVO共用时,在移除KVO观察者前移除Stinger的hook,却不会引起崩溃。
上面说到Aspects移除本身所有hook后,会把自身的isa
置回。但是Stinger却不会这么做,而仅仅只是把对应的hook信息清除,不会修改该对象的isa
。
// 清除infos中的对应identifier的hook信息
- (BOOL)_removeInfoForIdentifier:(STIdentifier)identifier inInfos:(NSMutableArray<id<STHookInfo>> *)infos {
[_lock lock];
BOOL flag = NO;
for (int i = 0; i < infos.count; i ++) {
id<STHookInfo> info = infos[i];
if ([info.identifier isEqualToString:identifier]) {
[infos removeObject:info];
[_identifiers removeObject:identifier];
flag = YES;
break;
}
}
[_lock unlock];
return flag;
}
因此保证了后面修改isa
的KVO动态生成的子类一直存在。也因此移除先修改isa
的Stinger功能不会影响到对应属性的KVO功能。
总结
那么,我们应该如何正确且安全地使用同时使用Aspects和KVO呢?
- 在对同一个对象的某一个实例变量使用Aspects及KVO时,尽量不要Remove对应的AspectToken,确保不会发生移除后的子类不存在的情况。
- 注意
先KVO再Hook
这种情况下,hook的有效期。(仅适用于加上PR115的Aspects) - 如果实在觉得麻烦,那可以在需要使用Aspects和KVO共存的地方,直接换成在
+(void)load
中对方法进行替换实现对应功能。 - 最推荐的用法,使用Stinger代替,注意避免对某个实例变量的
setter
方法进行先KVO后hook
的操作。