前言
Aspects框架也有一些缺陷,一不小心就会掉坑里面,我会通过源码解析进行说明。
源码解析
runtime有一定的了解,本文中不会对这些内容展开来讲,因为要把这些东西讲清楚,每一项都需要单独写一篇文章了。
主要流程解析
- Container,在这个过程中会进行一些前置条件的判断,例如这个方法是否支持被hook等,如果条件验证通过,就会把这次hook的信息保存起来,在方法调用的时候,查询出来使用。
- 第二个流程是动态创建子类,如果是针对类的hook,则不会走这一步。
- __ASPECTS_ARE_BEING_CALLED__,这个方法内部会查找到之前创建的Container,然后根据Container中的逻辑进行实际的调用。
- forwardInvocation:方法。
我对它的流程做了一个简化的图示,标有每个流程的序号,后面会对每个流程进行解析。流程如下:
其它子类路径。
①添加Container流程
这个流程中,把hook的逻辑封装成Container,并使用关联对象进行保存。这个过程中会判断hook的方法是否被支持、判断被hook类的继承关系、验证回调block正确性等操作。具体图示如下:
关键代码如下:
NSError **error) {
...
aspect_performLocked(^{ // 加锁
// hook前置条件判断
self, selector, options, error)) {
// 用selector作key,通过关联对象获得Container对象。
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
// 内部会判断block与hook的selector是否匹配,不匹配返回nil。
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
// 添加identifier,包含了hook的类型和回调。
[aspectContainer addAspect:identifier withOptions:options];
// Modify the class to allow message interception.
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}
NSError **error) {
NSSet *disallowedSelectorList;
dispatch_once_t pred;
dispatch_once(&pred, ^{
disallowedSelectorList = [nil];
});
// 这里对不支持hook的方法进行过滤
NSStringFromSelector(selector);
if ([disallowedSelectorList containsObject:selectorName]) {
NO;
}
// dealloc只支持AspectPositionBefore类型下调用
AspectOptions position = options&AspectPositionFilter;
NO;
}
// 判断是否存在这个方法
self.class instancesRespondToSelector:selector]) {
self.class), selectorName];
AspectError(AspectErrorDoesNotRespondToSelector, errorDesc);
NO;
}
// 这里禁止有继承关系的类hook同一个方法,代码量较多,不是关键内容,这里不贴出
self))) {
...
}
YES;
}
/// AspectsContainer内部添加AspectIdentifier的实现。
/// 这里可以看出对同一个方法的多次hook都会被调用,不会出现后面hook的覆盖前面的情况。
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options {
NSParameterAssert(aspect);
NSUInteger position = options&AspectPositionFilter;
switch (position) {
break;
break;
break;
}
}
复制代码
- forwardInvocation:进行实现的,所以对它的hook也不支持。
-
AspectPositionAfter类型,调用时对象可能已经已经被释放了,从而引发野指针错误。 -
issue,它报告了这样操作会导致死循环,我会在文章后面再进行说明。 -
block进行hook的调用,涉及到方法参数的传递和返回值问题,所以其中会对block进行校验。
②runtime创建子类
具体说明请查看源码中的注释
// 执行hook
NSError **error) {
NSCParameterAssert(selector);
// 针对实例类型,会通过runtime动态创建子类。类类型则直接hook。
Class klass = aspect_hookClass(self, error);
...
}
NSError **error) {
self);
Class statedClass = self.class;
Class baseClass = object_getClass(self);
NSStringFromClass(baseClass);
// 已经被hook过的类,直接返回
if ([className hasSuffix:AspectsSubclassSuffix]) {
return baseClass;
// 是元类(MetaClass),则代表是对类进行hook。(非单个实例)
}if (class_isMetaClass(baseClass)) {
// 内部是将类的forwardInvocation:方法替换为__ASPECTS_ARE_BEING_CALLED__
self);
// 可能是一个KVO对象等情况,传入实际的类型进行hook。
}if (statedClass != baseClass) {
return aspect_swizzleClassInPlace(baseClass);
}
// 单个实例的情况,动态创建子类进行hook.
char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);
nil) {
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
nil) {
nil;
}
// 内部是将类的forwardInvocation:方法替换为__ASPECTS_ARE_BEING_CALLED__
aspect_swizzleForwardInvocation(subclass);
// 重写class方法,返回之前的类型,而不是新创建的子类。避免hook后,类型判断出现问题。
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
object_setClass(self, subclass);
return subclass;
}
复制代码
③替换forwardInvocation:
__ASPECTS_ARE_BEING_CALLED__。源码如下:
void aspect_swizzleForwardInvocation(Class klass) {
NSCParameterAssert(klass);
// If there is no method, replace will act like class_addMethod.
IMP originalImplementation = class_replaceMethod(klass, if (originalImplementation) {
class_addMethod(klass, NSStringFromClass(klass));
}
复制代码
④hook方法交换IMP:
图示如下:
JSPatch,使用的时候需要注意,避免发生冲突。
被hook方法的调用流程
当hook注入后,对hook方法进行调用时,调用流程就会发生变化。图示如下:
因为Apsects对类的底层进行了修改,这种修改是基础方面的修改,需要考虑到各种场景和边界问题,一旦某方面考虑不周,就会引发出一些未知问题。另外这个框架是有缺陷的,很久没有进行更新了,我对它的已知问题点进行了总结,在下面进行说明。如果有未总结到位的,欢迎补充。
问题点
基于类的hooking,同一条继承链条上的所有类,一个方法只能被hook一次,后hook的无效。
issue
NSObject
- (void)foo;
@end
A
- (void)foo {
@end
end
B
- (void)foo {
// 导致死循环的代码
}
@end
char *argv[]) {
[B aspect_hookSelector:NSArray *arguments) {
NSArray *arguments) {
// 调用后死循环
}
复制代码
forwardInvocation:,这样就导致了死循环。
针对单个实例的hook,hook后使用kvo没问题,使用kvo后hook会出现问题。
这里通过代码进行说明,以Animal对象为例:
NSObject
NSString * name;
@end
Animal
- (void)testKVO {
[nil];
void *)context {
self.name);
}
- (void)dealloc {
[@end
char * argv[]) {
@autoreleasepool {
Animal *animal = [[Animal alloc] init];
[animal testKVO];
// 这里如果改为针对类进行hook,则不会存在问题,因为类hook修改的是Animal类,而实例hook修改的是NSKVONotifying_Animal类
[animal aspect_hookSelector:@selector(setName:)
withOptions:AspectPositionAfter
usingBlock:^(NSString *name){
nil];
// 这里会crash
animal.name = 复制代码
Animal对象并没有这个方法的实现,这就导致了crash。
与category的共存问题
category进行hook,会导致crash。反之则没有问题。样例代码如下:
NSObject
NSString * name;
@end
Animal
- (NSString *)name {
@end
hook)
+ (void)categoryHook;
@end
hook)
+ (void)categoryHook {
dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class];
SEL originalSelector = @selector(setName:);
SEL swizzledSelector = @selector(lx_setName:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (NSString *)name {
self lx_setName:name];
}
@end
char * argv[]) {
@autoreleasepool {
Animal *animal = [[Animal alloc] init];
[Animal aspect_hookSelector:NSString *name){
nil];
[Animal categoryHook];
// 调用后crash:[Animal lx_setName:]: unrecognized selector sent to instance 0x100608dc0
animal.name = 复制代码
aspects__lx_setName,导致找不到方法而crash
基于类的hook,如果对同一个类同时hook类方法和实例方法,那么后hook的方法调用时会crash。样例代码如下:
NSObject
- (void)testInstanceMethod;
+ (void)testClassMethod;
@end
Animal
- (void)testInstanceMethod {
void)testClassMethod {
@end
char * argv[]) {
@autoreleasepool {
Animal *animal = [[Animal alloc] init];
[Animal aspect_hookSelector:id<AspectInfo> aspectInfo){
nil];
[object_getClass([Animal id<AspectInfo> aspectInfo){
nil];
[animal testInstanceMethod];
复制代码
aspect_swizzleClassInPlace方法中的逻辑缺陷导致的。
static Class aspect_swizzleClassInPlace(Class klass) {
NSCParameterAssert(klass);
// Animal类对象与Animal元类对象会得到同一个字符串。
NSStringFromClass(klass);
NSMutableSet *swizzledClasses) {
// 类对象和元类对象得到同一个className,这里后加入的会被错误的过滤掉。
if (![swizzledClasses containsObject:className]) {
aspect_swizzleForwardInvocation(klass);
[swizzledClasses addObject:className];
}
});
return klass;
}
复制代码
IMP,导致了crash。
_objc_msgForward会出现冲突的问题
_objc_msgForward或相关逻辑的框架发生冲突。
性能问题
Coantiner进行调用,相对于未hook之前,额外增加了调用成本。所以不建议对频繁调用的方法和在项目中大量使用。
线程问题
OSSpinLock,存在线程反转的问题,在iOS10已经被标记为弃用。
对类方法的hook,需要使用object_getClass来获取元类对象进行hook
类方法进行hook,这里进行说明。
NSObject
+ (void)testClassMethod;
@end
// 需要通过object_getClass来获取元类对象进行hook
[object_getClass(Animal) aspect_hookSelector:@selector(testClassMethod)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo){
https://juejin.cn/post/7025783407540076581