Aspects框架的源码解读及问题解析
前言
在iOS日常开发中,对某些方法进行hook是很常见的操作。最常见的是使用Category在+load
中进行方法swizzle,它是针对类的,会改变这个类所有实例的行为。但是有时候我们只想针对单个实例进行hook,这种方法就显得无力了。而Aspects
框架可以搞定这个问题。 它的原理是通过Runtime
动态的创建子类,把实例的isa
指针指向新创建的子类,然后在子类中对hook的方法进行处理,这样就支持了对单个实例的hook。Aspects
框架支持对类和实例的hook,API很易用,可以方便的让你在任何地方进行hook,是线程安全的。但是Aspects
框架也有一些缺陷,一不小心就会掉坑里面,我会通过源码解析进行说明。
源码解析
我主要使用图示对Aspects
的源码进行说明,建议参考源码一起查看。要看懂这些内容,需要对isa指针
,消息转发机制
,runtime
有一定的了解,本文中不会对这些内容展开来讲,因为要把这些东西讲清楚,每一项都需要单独写一篇文章了。
主要流程解析
- 它第一个流程是使用关联对象添加
Container
,在这个过程中会进行一些前置条件的判断,例如这个方法是否支持被hook等,如果条件验证通过,就会把这次hook的信息保存起来,在方法调用的时候,查询出来使用。 - 第二个流程是动态创建子类,如果是针对类的hook,则不会走这一步。
- 第三步是替换这个类的
forwardInvocation:
方法为__ASPECTS_ARE_BEING_CALLED__
,这个方法内部会查找到之前创建的Container,然后根据Container中的逻辑进行实际的调用。 - 第四步是将原有方法的
IMP
改为_objc_msgForward
,改完后当调用原有方法时,就会调用_objc_msgForward
,从而触发forwardInvocation:
方法。
我对它的流程做了一个简化的图示,标有每个流程的序号,后面会对每个流程进行解析。流程如下:
图示中的取出对象类型
,是指的调用hook的对象的类型,如果是实例对象
,那么就走类
路径;如果是类
对象,则走元类
路径;如果是kvo
等实际类型不一致的情况,则走其它子类
路径。
①添加Container流程
这个流程中,把hook的逻辑封装成Container,并使用关联对象进行保存。这个过程中会判断hook的方法是否被支持、判断被hook类的继承关系、验证回调block正确性等操作。具体图示如下:
关键代码如下:
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
...
aspect_performLocked(^{ // 加锁
// hook前置条件判断
if (aspect_isSelectorAllowedAndTrack(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;
}
static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
static NSSet *disallowedSelectorList;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
});
// 这里对不支持hook的方法进行过滤
NSString *selectorName = NSStringFromSelector(selector);
if ([disallowedSelectorList containsObject:selectorName]) {
NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
AspectError(AspectErrorSelectorBlacklisted, errorDescription);
return NO;
}
// dealloc只支持AspectPositionBefore类型下调用
AspectOptions position = options&AspectPositionFilter;
if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc.";
AspectError(AspectErrorSelectorDeallocPosition, errorDesc);
return NO;
}
// 判断是否存在这个方法
if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName];
AspectError(AspectErrorDoesNotRespondToSelector, errorDesc);
return NO;
}
// 这里禁止有继承关系的类hook同一个方法,代码量较多,不是关键内容,这里不贴出
if (class_isMetaClass(object_getClass(self))) {
...
}
return YES;
}
/// AspectsContainer内部添加AspectIdentifier的实现。
/// 这里可以看出对同一个方法的多次hook都会被调用,不会出现后面hook的覆盖前面的情况。
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options {
NSParameterAssert(aspect);
NSUInteger position = options&AspectPositionFilter;
switch (position) {
case AspectPositionBefore: self.beforeAspects = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break;
case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break;
case AspectPositionAfter: self.afterAspects = [(self.afterAspects ?:@[]) arrayByAddingObject:aspect]; break;
}
}
复制代码
- 从源码中可以看到,不支持的hook方法有
[NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
。其中retain
,release
,autorelease
在arc下是被禁用的,框架本身是hook
了forwardInvocation:
进行实现的,所以对它的hook也不支持。 dealloc
只支持AspectPositionBefore
类型,使用AspectPositionInstead
会导致系统默认的dealloc
操作被替换无法执行而出现问题。AspectPositionAfter
类型,调用时对象可能已经已经被释放了,从而引发野指针错误。Aspects
禁止有继承关系的类hook同一个方法,具体可以参见它的一个issue,它报告了这样操作会导致死循环,我会在文章后面再进行说明。Aspects
使用block
进行hook的调用,涉及到方法参数的传递和返回值问题,所以其中会对block进行校验。
②runtime创建子类
iOS中的KVO
就是通过runtime
动态创建子类,然后在子类中重写对应的setter
方法来实现的,Aspects
支持对单个实例的hook原理与此有一些类似。图示如下:具体说明请查看源码中的注释
// 执行hook
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
// 针对实例类型,会通过runtime动态创建子类。类类型则直接hook。
Class klass = aspect_hookClass(self, error);
...
}
static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
Class statedClass = self.class;
Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass);
// 已经被hook过的类,直接返回
if ([className hasSuffix:AspectsSubclassSuffix]) {
return baseClass;
// 是元类(MetaClass),则代表是对类进行hook。(非单个实例)
}else if (class_isMetaClass(baseClass)) {
// 内部是将类的forwardInvocation:方法替换为__ASPECTS_ARE_BEING_CALLED__
return aspect_swizzleClassInPlace((Class)self);
// 可能是一个KVO对象等情况,传入实际的类型进行hook。
}else if (statedClass != baseClass) {
return aspect_swizzleClassInPlace(baseClass);
}
// 单个实例的情况,动态创建子类进行hook.
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);
if (subclass == nil) {
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
if (subclass == nil) {
NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
return 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:
这部分就是把原有的forwardInvocation:
替换为自定义的实现:__ASPECTS_ARE_BEING_CALLED__
。源码如下:
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
NSCParameterAssert(klass);
// If there is no method, replace will act like class_addMethod.
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
复制代码
替换后的对应关系图示如下:
④hook方法交换IMP:
图示如下:
第③步和第④步可能有些同学会感到疑惑,为什么要替换forwardInvocation
以及为什么要将hook的方法的IMP
替换为_objc_msgForward
,这个和iOS的消息转发机制
有关,可以自行查找相关资料,这里就不做说明了。需要注意的是有些框架也是通过iOS的消息发送机制来做一些操作,例如JSPatch
,使用的时候需要注意,避免发生冲突。
被hook方法的调用流程
当hook注入后,对hook方法进行调用时,调用流程就会发生变化。图示如下:
从上述解析过程中,我们可以看到Aspects
这个框架是设计的很巧妙的,从中可以看到非常多runtime
知识的应用。但是作者并不推荐在实际项目中进行使用:
因为Apsects对类的底层进行了修改,这种修改是基础方面的修改,需要考虑到各种场景和边界问题,一旦某方面考虑不周,就会引发出一些未知问题。另外这个框架是有缺陷的,很久没有进行更新了,我对它的已知问题点进行了总结,在下面进行说明。如果有未总结到位的,欢迎补充。
问题点
基于类的hooking,同一条继承链条上的所有类,一个方法只能被hook一次,后hook的无效。
之前这样会出现死循环,后面作者进行了修改,对这个行为进行了禁止
并加了错误提示。详见这个issue
@interface A : NSObject
- (void)foo;
@end
@implementation A
- (void)foo {
NSLog(@"%s", __PRETTY_FUNCTION__);
}
@end
@interface B : A @end
@implementation B
- (void)foo {
NSLog(@"%s", __PRETTY_FUNCTION__);
[super foo]; // 导致死循环的代码
}
@end
int main(int argc, char *argv[]) {
[B aspect_hookSelector:@selector(foo) atPosition:AspectPositionBefore withBlock:^(id object, NSArray *arguments) {
NSLog(@"before -[B foo]");
}];
[A aspect_hookSelector:@selector(foo) atPosition:AspectPositionBefore withBlock:^(id object, NSArray *arguments) {
NSLog(@"before -[A foo]");
}];
B *b = [[B alloc] init];
[b foo]; // 调用后死循环
}
复制代码
我们都知道,super
是从它的父类开始查找方法,然后传入self
进行调用。 根据我们之前对源码的解析,在这里调用[super foo]
后会从父类查找foo
的IMP
,查到后发现父类的IMP
已经被替换为_objc_msgForward
,然后传入self
调用。 因为是传入的self
,所以实际会调用到它自身的forwardInvocation:
,这样就导致了死循环。
针对单个实例的hook,hook后使用kvo没问题,使用kvo后hook会出现问题。
这里通过代码进行说明,以Animal对象为例:
@interface Animal : NSObject
@property(strong, nonatomic) NSString * name;
@end
@implementation Animal
- (void)testKVO {
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
self.name = @"Animal";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"observeValueForKeyPath keypath:%@ name:%@", keyPath, self.name);
}
- (void)dealloc {
[self removeObserver:self forKeyPath:@"name"];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Animal *animal = [[Animal alloc] init];
[animal testKVO];
// 这里如果改为针对类进行hook,则不会存在问题,因为类hook修改的是Animal类,而实例hook修改的是NSKVONotifying_Animal类
[animal aspect_hookSelector:@selector(setName:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo, NSString *name){
NSLog(@"aspects hook setName");
} error:nil];
// 这里会crash
animal.name = @"ChangedAnimalName";
}
}
复制代码
异常原因分析图示如下:
上面是继承链和方法调用流程的图示,可以看出,_NSSetObjectValueAndNotify
是被aspects__setName:
调用的,_NSSetObjectValueAndNotify
的内部实现逻辑是取调用它的selector
,去父类查找方法,即aspects__setName:
方法,而Animal
对象并没有这个方法的实现,这就导致了crash。
与category的共存问题
先用aspects
进行hook,再使用category
进行hook,会导致crash。反之则没有问题。样例代码如下:
@interface Animal : NSObject
@property(strong, nonatomic) NSString * name;
@end
@implementation Animal
- (void)setName:(NSString *)name {
NSLog(@"%s", __func__);
_name = name;
}
@end
@interface Animal(hook)
+ (void)categoryHook;
@end
@implementation Animal(hook)
+ (void)categoryHook {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [super 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);
});
}
- (void)lx_setName:(NSString *)name {
NSLog(@"%s", __func__);
[self lx_setName:name];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Animal *animal = [[Animal alloc] init];
[Animal aspect_hookSelector:@selector(setName:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo, NSString *name){
NSLog(@"aspects hook setName");
} error:nil];
[Animal categoryHook];
// 调用后crash:[Animal lx_setName:]: unrecognized selector sent to instance 0x100608dc0
animal.name = @"ChangedAnimalName";
}
}
复制代码
这个与__ASPECTS_ARE_BEING_CALLED__
的内部逻辑有关,里面会对调用的方法添加前缀aspect__
进行调用,以调用到原始的IMP
,但是category
hook后破坏了这个流程。图示如下:
根据上述图示,实际只有aspects__setName
,没有aspects__lx_setName
,导致找不到方法而crash
基于类的hook,如果对同一个类同时hook类方法和实例方法,那么后hook的方法调用时会crash。样例代码如下:
@interface Animal : NSObject
- (void)testInstanceMethod;
+ (void)testClassMethod;
@end
@implementation Animal
- (void)testInstanceMethod {
NSLog(@"%s", __func__);
}
+ (void)testClassMethod {
NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Animal *animal = [[Animal alloc] init];
[Animal aspect_hookSelector:@selector(testInstanceMethod) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){
NSLog(@"aspects hook testInstanceMethod");
} error:nil];
[object_getClass([Animal class]) aspect_hookSelector:@selector(testClassMethod) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo){
NSLog(@"aspects hook testClassMethod");
} error:nil];
[animal testInstanceMethod];
// crash: "+[Animal testClassMethod]: unrecognized selector sent to class 0x1000114a0"
[Animal testClassMethod];
}
}
复制代码
这样的调用在日常开发中非常正常,但是它会导致crash。它是由于aspect_swizzleClassInPlace
方法中的逻辑缺陷导致的。
static Class aspect_swizzleClassInPlace(Class klass) {
NSCParameterAssert(klass);
// Animal类对象与Animal元类对象会得到同一个字符串。
NSString *className = NSStringFromClass(klass);
NSLog(@"aspect_swizzleClassInPlace %@ %p", klass, object_getClass(klass));
_aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
// 类对象和元类对象得到同一个className,这里后加入的会被错误的过滤掉。
if (![swizzledClasses containsObject:className]) {
aspect_swizzleForwardInvocation(klass);
[swizzledClasses addObject:className];
}
});
return klass;
}
复制代码
从上述代码可以看到,它的去重逻辑只是简单的字符串判断,取Animal的元类
和类
名得到同一个字符串Animal
,导致后添加的被过滤,当调用后被hook的方法后,执行_objc_msgForward
,因为后hook的aspect_swizzleForwardInvocation
被过滤了没有执行,所以找不到forwardInvocation:
的IMP
,导致了crash。
_objc_msgForward会出现冲突的问题
内部是通过消息转发机制来实现的,使用时要注意,避免与其它使用_objc_msgForward
或相关逻辑的框架发生冲突。
性能问题
hook后的方法,通过原有消息机制找到IMP
后,并不会直接调用。而是会进行消息转发进入到__ASPECTS_ARE_BEING_CALLED__
方法,内部再通过key取出相应的Coantiner
进行调用,相对于未hook之前,额外增加了调用成本。所以不建议对频繁调用的方法和在项目中大量使用。
线程问题
框架内部为了保证线程安全,有进行加锁,但是使用的是自旋锁OSSpinLock
,存在线程反转的问题,在iOS10已经被标记为弃用。
对类方法的hook,需要使用object_getClass来获取元类对象进行hook
这个不是框架问题,而是有些同学不知道如何对类方法
进行hook,这里进行说明。
@interface Animal : NSObject
+ (void)testClassMethod;
@end
// 需要通过object_getClass来获取元类对象进行hook
[object_getClass(Animal) aspect_hookSelector:@selector(testClassMethod)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo){
NSLog(@"aspects hook setName");
} error:null];
https://juejin.cn/post/7025783407540076581