星的天空的博客

种一颗树,最好的时间是十年前,其次是现在。

0%

Aspects框架的源码解读及问题解析

前言

在iOS日常开发中,对某些方法进行hook是很常见的操作。最常见的是使用Category在+load中进行方法swizzle,它是针对类的,会改变这个类所有实例的行为。但是有时候我们只想针对单个实例进行hook,这种方法就显得无力了。而Aspects框架可以搞定这个问题。
它的原理是通过Runtime动态的创建子类,把实例的isa指针指向新创建的子类,然后在子类中对hook的方法进行处理,这样就支持了对单个实例的hook。Aspects框架支持对类和实例的hook,API很易用,可以方便的让你在任何地方进行hook,是线程安全的。但是Aspects框架也有一些缺陷,一不小心就会掉坑里面,我会通过源码解析进行说明。

源码解析

我主要使用图示对Aspects的源码进行说明,建议参考源码一起查看。要看懂这些内容,需要对isa指针消息转发机制runtime有一定的了解,本文中不会对这些内容展开来讲,因为要把这些东西讲清楚,每一项都需要单独写一篇文章了。

主要流程解析

  1. 它第一个流程是使用关联对象添加Container,在这个过程中会进行一些前置条件的判断,例如这个方法是否支持被hook等,如果条件验证通过,就会把这次hook的信息保存起来,在方法调用的时候,查询出来使用。
  2. 第二个流程是动态创建子类,如果是针对类的hook,则不会走这一步。
  3. 第三步是替换这个类的forwardInvocation:方法为__ASPECTS_ARE_BEING_CALLED__,这个方法内部会查找到之前创建的Container,然后根据Container中的逻辑进行实际的调用。
  4. 第四步是将原有方法的IMP改为_objc_msgForward,改完后当调用原有方法时,就会调用_objc_msgForward,从而触发forwardInvocation:方法。

我对它的流程做了一个简化的图示,标有每个流程的序号,后面会对每个流程进行解析。流程如下:

图示中的取出对象类型,是指的调用hook的对象的类型,如果是实例对象,那么就走路径;如果是对象,则走元类路径;如果是kvo等实际类型不一致的情况,则走其它子类路径。

①添加Container流程

这个流程中,把hook的逻辑封装成Container,并使用关联对象进行保存。这个过程中会判断hook的方法是否被支持、判断被hook类的继承关系、验证回调block正确性等操作。具体图示如下:

关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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;
}
}
  1. 从源码中可以看到,不支持的hook方法有[NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];。其中retain, release, autorelease在arc下是被禁用的,框架本身是hookforwardInvocation:进行实现的,所以对它的hook也不支持。
  2. dealloc只支持AspectPositionBefore类型,使用AspectPositionInstead会导致系统默认的dealloc操作被替换无法执行而出现问题。 AspectPositionAfter类型,调用时对象可能已经已经被释放了,从而引发野指针错误。
  3. Aspects禁止有继承关系的类hook同一个方法,具体可以参见它的一个issue,它报告了这样操作会导致死循环,我会在文章后面再进行说明。
  4. Aspects使用block进行hook的调用,涉及到方法参数的传递和返回值问题,所以其中会对block进行校验。

②runtime创建子类

iOS中的KVO就是通过runtime动态创建子类,然后在子类中重写对应的setter方法来实现的,Aspects支持对单个实例的hook原理与此有一些类似。图示如下:

具体说明请查看源码中的注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 执行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__。源码如下:

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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]后会从父类查找fooIMP,查到后发现父类的IMP已经被替换为_objc_msgForward,然后传入self调用。 因为是传入的self,所以实际会调用到它自身的forwardInvocation:,这样就导致了死循环。

针对单个实例的hook,hook后使用kvo没问题,使用kvo后hook会出现问题。

这里通过代码进行说明,以Animal对象为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@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。反之则没有问题。样例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@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。样例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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方法中的逻辑缺陷导致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,这里进行说明。

1
2
3
4
5
6
7
8
9
10
@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];