Fork me on GitHub

深入理解 Aspects 设计原理

最近希望在业务中实现一套基于 AOP 的埋点方案,调研过程中,我花了些时间阅读了一下 Aspects 的源码,对于 Aspects 设计有了一些更深入的理解。因此,通过本文记录我在阅读源码后的一些收获和思考,以供后续进行回顾。

概述

Aspects 是一款轻量且简易的面向切面编程的框架,其基于 Objective-C Runtime 原理实现。Aspects 允许我们对 类的所有实例的实例方法单个实例的实例方法 添加额外的代码,并且支持设置代码的执行时机,包括:beforeinsteadafter 三种。

注意:Aspects 无法为类方法提供面向切面编程的能力

对象类型 目标方法类型 Aspects 是否支持 hook hook 效果
类对象(UIViewController) 类方法(“+”开头的方法) 不支持 -
类对象(UIViewController) 实例方法(“-”开头的方法) 支持 对类的所有实例对象生效
实例对象(vc) 类方法(“+”开头的方法) 不支持 -
实例对象(vc) 实例方法(“-”开头的方法) 支持 对单个实例对象生效

这里我们提出第一个问题:为什么 Aspects 仅支持对实例方法进行 hook?

另一方面,Aspects 的作者在框架的 README 中明确表示不要在生产环境中使用 Aspects。这里我们提出第二个问题:在项目中使用 Aspects 进行 hook 是否有什么坑?

基础

Aspects 巧妙利用了 Objective-C 的消息传递和消息转发机制,实现了一套与 KVO 类似的技术方案。为了能够更加清晰地理解 Aspects 的设计,这里我们简单地回顾一下 Objective-C 的消息传递和消息转发机制。

消息传递

Objective-C 是一门动态语言,其 方法调用 在底层的实现是 消息传递(Message Passing)。本质上,消息发送是 沿着一条引用链依次查找不同的对象,判断该对象是否能够处理消息。在 Objective-C 中,一切都是对象,包括类、元类,消息就是在这些对象之间进行传递的。

因此,我们需要了解这些对象之间的关系。下图所示,为 Objective-C 对象在内存中的引用关系图。

在 Objective-C 中,涉及消息传递的方法主要有两种:实例方法、类方法。下面,我们来分别介绍。

实例方法

对于实例方法,消息传递时,根据当前实例对象的 isa 指针,找到其所属的类对象,并在类对象的方法列表中查找。如果找到,则执行;否则,根据 superclass 指针,找到类对象的超类对象,并在超类对象的方法列表中查找,以此类推,如下所示。

类方法

虽然 Aspects 不支持 hook 类方法,但是为了方便进行对照,这里我们也介绍一下类方法的查找。

对于类方法,消息传递时,根据当前类对象的 isa 指针,找到其所属的元类对象,并在元类对象的方法列表中查找。如果找到,则执行;否则,根据 superclass 指针,找到元类对象的元超类对象,并在元超类对象的方法列表中查找,以此类推,如下所示。

消息转发

如果消息传递无法找到可以处理消息的对象,那么,Objective-C runtime 将进入消息转发(Message Forwarding)。

消息转发包含三个阶段:

  • 动态消息解析
  • 备用接收者
  • 完整消息转发

动态消息解析

当对象接收到未知消息时,首先会调用所属类的实例方法 + (BOOL)resolveInstanceMethod:(SEL)sel 或类方法 + (BOOL)resolveClassMethod:(SEL)sel。我们可以在方法内部动态添加一个“处理方法”,通过 class_addMethod 函数动态添加到类中。比如:

1
2
3
4
5
6
7
8
9
10
11
void dynamicMethodIMP(id self, SEL _cmd) {
// 方法实现
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}

备用接收者

如果上一步无法处理消息,则 runtime 会继续调用 forwardingTargetForSelector: 方法。

如果一个对象实现了这个方法,并返回一个非 nil(也不能是 self) 的对象,则这个对象会作为消息的新接收者,消息会被分发到这个对象。比如:

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString * selString = NSStringFromSelector(aSelector);
if ([selString isEqualToString:@"walk"]) {
return self.otherObject;
}
return [super forwardingTargetForSelector:aSelector];
}

这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。

这步调用 methodSignatureForSelector: 进行方法签名,这可以将函数的参数类型和返回值进行封装。如果返回 nil,则说明消息无法处理并报错 unrecognized selector sent to instance

1
2
3
4
5
6
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"testInstanceMethod"]){
return [NSMethodSignature signatureWithObjcTypes:"v@:"];
}
return [super methodSignatureForSelector: aSelector];
}

如果返回 methodSignature,则进入 forwardInvocation。对象会创建一个表示消息的 NSInvocation 对象,把与尚未处理的消息有关的全部细节都封装在 anInvocation 中,包括 selectortarget,参数。在这个方法中可以修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果依然不能正确响应消息,则报错 unrecognized selector sent to instance

1
2
3
4
5
- (void)forwardInvovation:(NSInvocation)anInvocation {
[anInvocation invokeWithTarget:_helper];
[anInvocation setSelector:@selector(run)];
[anInvocation invokeWithTarget:self];
}

核心原理

Aspects 的核心原理主要包括三个部分:

  • 注册关联对象:在 hook 实例方法时,均会注册关联对象 AspectsContainer
  • 创建动态类:只有在 hook 实例对象的实例方法时,才会创建动态类。
  • 核心方法交换:在 hook 实例方法时,均会对核心方法的实现进行交换。

注册关联对象

当 hook 实例方法时,Aspects 会为 实例对象类对象 注册关联对象 AspectsContainerAspectsContainer 保存了用户 hook 的目标方法、执行闭包、闭包参数、执行时机等信息。下图所示,为 AspectsContainer 引用关系图。

关联对象注册的目标分两种情况,这种设计策略是有原因的:

  • 在实例对象中注册关联对象,可以实现让每个实例对象单独管理 aspects,从而保证实例之间相互不影响。
  • 在类对象中注册关联对象,可以实现让类的每个实例对象共享 aspects,从而实现影响所有实例对象。

创建动态类

当且仅当 hook 实例对象的实例方法时,Aspects 会为实例的所属类 TestClass 创建一个子类 TestClass_Aspects_(同时创建对应的元类),并修改实例的 isa 指针,使其指向 TestClass_Aspects_ 子类,同时 hook TestClass_Aspects_class 方法,使其返回实例的所属类 TestClass,如下图所示。

整体的实现方式与 KVO 原理一致,尤其是修改动态类 class 方法的实现,使得在外部看来,实例的所属类并没有发生任何变化。

这里,我们可以思考一下第三个问题:为什么在 hook 实例对象的实例方法时要创建动态类?

核心方法交换

当 hook 实例方法时,最重要的一步是对 动态创建的类对象(下文简称:动态类对象) 或 原始继承链中的类对象(下文简称:目标类对象) 的两个核心方法与 Aspects 提供的方法进行交换。这两个方法分别是:目标 selector 和 forwardInvocation:。具体的交换逻辑如下图所示。

Aspects 会将目标 selector 的实现设置为 Aspects 提供的 aspect_getMsgForwardIMP 方法的返回值。aspects_getMsgForwardIMP 的返回值本质上是一个能够直接触发消息转发机制的方法。更加特殊的地方在于,这里会直接进入消息转发的最后一步 forwardInvocation:

与此同时,Aspects 会将动态类对象或目标类对象的 forwardInvocation: 的实现设置为 Aspects 提供的 __ASPECTS_ARE_BEING_CALLED__ 方法实现。__ASPECTS_ARE_BEING_CALLED__ 内部会从 实例对象类对象 中取出关联对象 AspectsContainer,并根据其所保存的 hook 信息执行闭包和目标 selector 的原始实现。

注意:对于核心方法交换,Aspects 支持幂等。即如果对同一个实例方法 hook 多次,Aspects 会保证对这两个方法只交换一次

具体实现

下面,我们来通过源码,具体分析一下 Aspects 中的设计细节。

数据结构

首先,简要介绍一下 Aspects 定义的数据结构,主要包括三种数据结构:

  • AspectsContainer
  • AspectIdentifier
  • AspectInfo

AspectsContainer

如下所示,为 AspectsContainer 的数据结构定义。AspectsContainer 是 Aspects 所有信息的根容器,其包含了三个数组,用于保存三种类型的 AspectIdentifier

  • beforeAspects:用于保存执行时机为 AspectPositionBeforeAspectIdentifier
  • insteadAspects:用于保存执行时机为 AspectPositionInsteadAspectIdentifier
  • afterAspects:用于保存执行时机为 AspectPositionAfterAspectIdentifier

除此之外,AspectsContainer 还提供了对于数组进行增删操作的方法。

1
2
3
4
5
6
7
8
9
// Tracks all aspects for an object/class.
@interface AspectsContainer : NSObject
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- (BOOL)removeAspect:(id)aspect;
- (BOOL)hasAspects;
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end

AspectIdentifer

如下所示,为 AspectIdentifier 的数据结构定义。AspectIdentifier 是用于表示一个 aspect 的相关信息,其包含了目标 selector、执行闭包、闭包签名、目标对象、执行时机等。

1
2
3
4
5
6
7
8
9
10
// Tracks a single aspect.
@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id<AspectInfo>)info;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end

AspectInfo

如下所示,为 AspectInfo 的数据结构定义。AspectInfo 的作用是保存目标 selector 的原始实现的执行环境。由于目标 selector 会被交换方法实现,因此 originalInvocationselector 其实是 Aspects 交换的 selector,即 aspects__SEL

1
2
3
4
5
6
@interface AspectInfo : NSObject <AspectInfo>
- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation;
@property (nonatomic, unsafe_unretained, readonly) id instance;
@property (nonatomic, strong, readonly) NSArray *arguments;
@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
@end

代码流程

如下所示,Aspects 对外提供两个接口,分别用于 hook 类方法和实例方法,即添加 aspect。

1
2
3
4
5
6
7
8
9
10
11
/// Adds a block of code before/instead/after the current `selector` for a specific class.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;

两者的内部实现都只调用了同一个方法 aspect_add,其内部实现逻辑如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
NSCParameterAssert(self);
NSCParameterAssert(selector);
NSCParameterAssert(block);

__block AspectIdentifier *identifier = nil;
aspect_performLocked(^{
// 判断是否允许 add aspect
// 如果允许,会顺带构建 tracker 链。
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
// 加载或创建 container,每个 selector 对应一个 container。
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
[aspectContainer addAspect:identifier withOptions:options];

// Modify the class to allow message interception.
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}

aspect_add 方法内部实现中,首先通过 aspect_isSelectorAllowedAndTrack 方法判断是否允许添加 aspect。如果允许,则初始化 AspectsContainer,并将其设置成实例对象或类对象的关联对象。一个 selector 对应一个 container,一个实例对象或类对象可包含多个 container。最后通过 aspect_prepareClassAndHookSelector 执行核心方法交换,对于实例对象,还会创建动态类。

aspect_isSelectorAllowedAndTrack

Aspects 通过 aspect_isSelectorAllowedAndTrack 方法来判断是否允许添加 aspect,如果允许则进行追踪。具体实现逻辑如下所示。

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
71
72
73
74
75
76
77
78
79
80
81
static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
// part 1
// 静态变量,作为黑名单
static NSSet *disallowedSelectorList;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
// 不允许添加 aspect 的方法黑名单
disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
});

// 检查方法是否属于黑名单
NSString *selectorName = NSStringFromSelector(selector);
if ([disallowedSelectorList containsObject:selectorName]) {
NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
AspectError(AspectErrorSelectorBlacklisted, errorDescription);
return NO;
}

// 对于 dealloc 方法,只允许在 before 阶段进行 hook
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;
}

// 不能 hook 不存在的实例方法
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;
}

// part 2
// 如果 hook 目标是类对象,必须保证类继承链上,只允许对一个方法进行一次 hook
if (class_isMetaClass(object_getClass(self))) {
Class klass = [self class];
NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
Class currentClass = [self class];
// 检查继承链中是否 hook 过目标类方法
do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
// 依次遍历超类直到根类,根据类对应的 track 进行判断
// 如果类方法已经 hook 过,则进一步判断
if ([tracker.selectorNames containsObject:selectorName]) {

if (tracker.parentEntry) {
// 如果父类中 hook 过,则不允许 hook
// 同时查找最顶层 tracker,打印日志
AspectTracker *topmostEntry = tracker.parentEntry;
while (topmostEntry.parentEntry) {
topmostEntry = topmostEntry.parentEntry;
}
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(topmostEntry.trackedClass)];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
} else if (klass == currentClass) {
// 如果当前类的方法已经 hook 过,则允许 hook
return YES;
}
}
} while ((currentClass = class_getSuperclass(currentClass)));

// 如果继承链上没有类对目标方法hook过,则允许 hook,并记录 tracker
// Add the selector as being modified.
currentClass = klass;
AspectTracker *parentTracker = nil;
do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
if (!tracker) {
tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker];
swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
}
[tracker.selectorNames addObject:selectorName];
// All superclasses get marked as having a subclass that is modified.
parentTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
}

return YES;
}

aspect_isSelectorAllowedAndTrack 的内部逻辑可以分为两部分:方法黑名单检查对象类型检查

对于方法黑名单检查,可细分为三个步骤:

  • 判断目标方法是不是 retainreleaseautorelease 等,如果是,则不允许 hook。
  • 如果目标方法是 dealloc,则只允许 hook before 时机,其他时机,则不允许 hook。
  • 进一步确认 hook 的目标方法是否存在,如果不存在,则不允许 hook。

对于对象类型检查,如果对象类型是实例对象,则允许 hook。如果对象类型是类对象,则进一步判断。根据目标类对象,遍历继承链,对于继承链中的每一个类对象,从全局字典 swizzledClassesDict 中读取对应的追踪器 AspectTracker。根据追踪器的记录,我们可以处理两种情况:

  • 如果目标类没有 hook 过目标方法,但其父类 hook 过,则不允许 hook。
  • 如果目标类 hook 过父类方法,但其子类没有 hook 过,则允许 hook。

如下图所示,为追踪器工作原理示意图。

当对 SubClass 类对象 hook 实例方法 SEL01 时,Aspects 会从 SubClass 类对象开始,遍历其继承链,读取继承链上的每一个类对象所对应的追踪器(如果没有则创建),将目标方法 SEL01 保存至其内部的 selectorNames 数组中作为记录。

后续,如果对 Class 类对象 hook 实例方法 SEL01 时,由于其子类 SubClass 已经 hook 过同名方法,则不允许 Class 对其再次 hook。根据消息传递的原理,对 Class 进行 hook 是不会生效的,因为子类 SubClass 会在消息传递链中提前返回 SEL01。所以,Aspects 的设计不允许在这种情况下再次 hook 同名方法。

当然,如果对 Class 类对象 hook 实例方法 SEL02 时,由于所有其子类均没有 hook 过同名方法,因此允许 Class 对其再次 hook。

本质上,Aspects 利用了 正向的类对象继承链反向的追踪器链,通过全局字典 swizzledClassDict 进行绑定,形成了一个双向链表,便于判断是否允许对类对象的实例方法进行 hook。

aspect_prepareClassAndHookSelector

如下所示,为 aspect_prepareClassAndHookSelector 的实现逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
// Aspects_Class_
Class klass = aspect_hookClass(self, error);
// 读取 Aspects_Class_ 的 selector 方法
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
// IMP 不能是 _objc_msgForward 或 _objc_msgForward_stret
const char *typeEncoding = method_getTypeEncoding(targetMethod);
// aspects__SEL
SEL aliasSelector = aspect_aliasForSelector(selector);
if (![klass instancesRespondToSelector:aliasSelector]) {
// 如果不存在 aspects__SEL,即没有被交换过,则新增一个 aspects__SEL 方法,其实现指向 selector IMP
__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
}

// 将 selector 方法的实现指向 _objc_msgForward,从而直接触发消息转发
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}

其中 aspect_hookClass 将判断对象类型,如果是实例对象,则创建一个动态类对象返回;如果是类对象,则返回对应的类对象。

基于 aspect_hookClass 返回的对象,Aspects 将修改该对象的两个方法,使其指向 Aspects 的两个方法实现,即上述我们介绍的 核心方法交换

aspect_prepareClassAndHookSelector 的实现中,Aspects 会在进行方法交换之前进行检查,避免重复交换,从而实现幂等。

aspect_hookClass

如下所示,为 aspect_hookClass 的实现逻辑。

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
static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
Class statedClass = self.class; // 其所声明的类
Class baseClass = object_getClass(self);// isa
NSString *className = NSStringFromClass(baseClass);

if ([className hasSuffix:AspectsSubclassSuffix]) {
// 如果是实例对象,且实例对象已经 hook 过方法,即其 isa 指向的是动态类对象 Aspects_Class_,则直接复用动态类对象
return baseClass;

} else if (class_isMetaClass(baseClass)) {
// 如果是类对象,则将该类对象 forwardInvocation: 的实现设置为 _aspects_forwardInvocation:。
// 方法交换完成后,返回该类对象。
return aspect_swizzleClassInPlace((Class)self);

} else if (statedClass != baseClass) {
// 如果是被 KVO 的实例对象。
// baseClass 为 KVO 所创建的动态类,则直接对 KVO 创建的动态类对象进行方法交换,交换 forwardInvocation: 与 _aspects_forwardInvocation: 的方法实现。
return aspect_swizzleClassInPlace(baseClass);
}

// 如果是实例对象,且实例对象未 hook 过方法,则创建动态子类 Aspects_Class_
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_forwardInvocation: 进行方法交换
aspect_swizzleForwardInvocation(subclass);
// 将动态类对象的 class 设置成 statedClass
aspect_hookedGetClass(subclass, statedClass);
// 将动态类对象的元类的 class 设置成 stateClass
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}

// 设置 isa 指针,指向 subclass
object_setClass(self, subclass);
return subclass;
}

aspect_hookClass 方法主要用于 选择对哪个对象的目标方法执行 hook,这里面包含了 4 种具体的情况,依次为:

  • 如果目标对象是实例对象,且实例对象曾经 hook 过方法,则直接返回已创建的动态类对象。
  • 如果目标对象是类对象,则对类对象的 forwardInvocation: 方法的实现设置为 Aspects 提供的 _aspects_forwardInvocation:,并返回该类对象。
  • 如果目标对象是被 KVO 的对象,则直接复用 KVO 所创建的动态类,并对动态类对象的 forwardInvocation: 方法的实现设置为 Aspects 提供的 _aspects_forwardInvocation:,并返回 KVO 的动态类对象。
  • 如果目标对象是实例对象,且实例对象没有 hook 过方法,则创建一个动态类对象 Aspects_Class_,同时包括元类对象,并对动态类对象的 forwardInvocation: 方法执行方法交换,并且设置动态类与原始类之间的关系,最终返回动态类对象。

相关问题

本节,我们将来介绍上文所提出的几个问题。

问题一:为什么 Aspects 仅支持对实例方法进行 hook?

在 Aspects 的实现中,在判断能够添加 aspect 的逻辑中,会通过 aspect_isCompatibleBlockSignature 方法来判断 block 与 selector 的方法签名是否匹配,如下所示。其中,它会通过类的 instanceMethodSignatureForSelector 方法获取 selector 的方法签名。对于类方法,通过这种方式必然返回 nil,从而导致判断条件无法满足,因此无法 hook 类方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) {
NSCParameterAssert(blockSignature);
NSCParameterAssert(object);
NSCParameterAssert(selector);

BOOL signaturesMatch = YES;

// 对于类方法,通过 instanceMethodSignatureForSelector: 读取必然返回 nil
NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
signaturesMatch = NO;
} else {
...
}
...
}

问题二:在项目中使用 Aspects 进行 hook 是否有什么坑?

如果我们真正理解了 Aspects 的设计原理,很容易明白为什么作者不推荐在生产环境中使用 Aspects。事实上,在实际的项目开发中,我们经常会用到对已有方法进行 hook。当然,我们可以保证自己写的代码只使用 Aspects 进行 hook,但是我们无法确定引入的第三方库是否使用其他方式对方法进行 hook。那么,这时候埋下了未知的风险。

如上图所示,假如我们对 SELbcq_SEL 进行了 swizzle。那么,bcq_SEL 的实现将指向 SEL 的实现 aspect_getMsgForwardIMPSEL 的实现将指向 bcq_SEL 的实现 bcq_IMP

在有些情况下,比如:hook viewWillAppear: 方法。bcq_IMP 里会再次调用 bcq_SEL,从而再次调用原始实现。这时候,我们调用 SEL,它最终仍然会调用 aspect_getMsgForwardIMP,Aspects 的设置不受影响。

但是有些情况下,bcq_IMP 的内部逻辑可能只在特定条件下调用原始实现,其他条件下调用自定义实现。这时候,我们调用 SEL,在某些条件下将不会触发 aspect_getMsgForwardIMP,最终导致 Aspects 的设置不生效。

显而易见,在生产环境在使用 Aspects 的确可能会出现不确定的异常问题。因此,作者不建议我们在生产环境中使用 Aspects。

问题三:为什么在 hook 实例对象的实例方法时要创建动态类?

对于实例对象的实例方法,我们显然不能直接 hook 继承链中的类对象,否则将影响类的所有实例的实例方法。因此,Aspects 选择了一种类似于 KVO 的设计,动态创建一个子类,并将实例对象的 isa 指针指向动态子类。动态子类的 class 方法则指向实例对象的声明类,从而是外部看来没有任何变化。

这种做法,为实例对象单独开辟了一条继承链分支,如下图所示。只有被 hook 的实例对象才会走这条分支继承链,因此不影响其他实例。

如果对同一个类的多个实例进行 Aspects,那么会怎么样?从上图中,我们也能猜到,Aspects 会复用动态子类。只不过 hook 的闭包由各个实例对象自己管理而已。

总结

通过分析 Aspects 的源码及其设计原理,我们同时加深了对于 Objective-C Runtime 的理解。从中,我们也了解到 Aspects 的局限性,引入需谨慎。

在 Aspects 中,我们看到了很多 Objective-C 的黑魔法 API,比如:

  • _objc_msgForward/_objc_msgForward_stret:直接触发 forwardInvocation:
  • objc_allocateClassPair:动态创建类对象和元类对象
  • objc_registerClassPair:注册类对象和元类对象
  • object_setClass:设置 isa 指针指向。

除此之外,Aspects 使用了非常底层的方式实现了闭包的参数检查与匹配,这一块非常值得我们深入学习,后续有机会我们再来研究一下。

最后,向作者表达一下敬意!如果对 Objective-C 底层原理没有如此深刻的理解,一般人是写不出来这样的框架的!

参考

  1. Aspects
  2. Objective-C Runtime 消息传递与转发
  3. Friday Q&A 2009-03-27: Objective-C Message Forwarding
  4. 面向切面编程之 Aspects 源码解析及应用
  5. object_getClass(obj)与[obj class]的区别
  6. Type Encodings
  7. Aspects关联&调用流程浅析
  8. Aspects源码分析
欣赏此文?求鼓励,求支持!