Aspect 与 消息转发

Aspect 与 消息转发

Aspects是iOS面向切面编程的第三方库, 它可以在不改变原有代码的情况下, 在任意函数之前或之后插入代码, Aspects 的实现是基于 Runtime 的消息转发机制.

Aspects 源码分析

先看一段官方 demo 的示例代码, 比较简单的 hook 了 viewWillDisappear: 方法

1
2
3
4
5
6
[testController aspect_hookSelector:@selector(viewWillDisappear:) withOptions:0 usingBlock:^(id<AspectInfo> info, BOOL animated) {
UIViewController *controller = [info instance];
if (controller.isBeingDismissed || controller.isMovingFromParentViewController) {
[[[UIAlertView alloc] initWithTitle:@"Popped" message:@"Hello from Aspects" delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Ok", nil] show];
}
} error:NULL];

hook 过程

我们从入口函数进入开始跟踪代码, 调用了aspect_add函数, 代码如下:

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

// 1. 生成AspectIdentifier
__block AspectIdentifier *identifier = nil;
aspect_performLocked(^{
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
// 2. 将AspectIdentifier加入到AspectsContainer中
if (identifier) {
[aspectContainer addAspect:identifier withOptions:options];
// 3. 替换方法
// Modify the class to allow message interception.
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}

这段代码做了三件事情.

  1. 首先生成AspectIdentifier,
  2. 然后将AspectIdentifier加入到AspectsContainer中,
  3. 使用objc_setAssociatedObject 将 AspectsContainer 设置为 vc 的动态属性, 属性名为前缀 “aspects__”加上 selector 名, 如 aspects__viewWillDisappear,

AspectsContainer

AspectsContainer的定义如下, 它负责容纳AspectIdentifier, 可以在before, instead, after数组里放入多个AspectIdentifier, 从名称可以看出这些AspectIdentifier所执行的时机.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

替换方法

其次调用aspect_prepareClassAndHookSelector函数, 这是最关键的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
// 1. aspect_hookClass 替换 forwardInvocation
Class klass = aspect_hookClass(self, error);
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
// Make a method alias for the existing method implementation, it not already copied.
const char *typeEncoding = method_getTypeEncoding(targetMethod);
// 2. 生成新的 selector, 并加到类中, 后续通过消息转发执行
SEL aliasSelector = aspect_aliasForSelector(selector);
if (![klass instancesRespondToSelector:aliasSelector]) {
__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);
}

// 3. 原方法指向 _objc_msgForward
// We use forwardInvocation to hook in.
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}

我们先来看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
static Class aspect_hookClass(NSObject *self, NSError **error) {
Class statedClass = self.class;
Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass);

// Default case. Create dynamic subclass.
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);

if (subclass == nil) {
// 1. 生成子类
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
// 2. 替换 forwardInvocation
aspect_swizzleForwardInvocation(subclass);
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
// 3. 修改 isa
objc_registerClassPair(subclass);
}

object_setClass(self, subclass);
return subclass;
}
  1. 通过运行时的函数objc_allocateClassPair定义了一个新的子类.如果是demo执行到这里的话, 生成的子类叫UIImagePickerController_Aspects.
  2. 将子类的forwardInvocation替换为了自定义的实现函数__ASPECTS_ARE_BEING_CALLED__.
  3. UIImagePickerController实例的isa指针指向了子类UIImagePickerController_Aspects.

然后再看aspect_prepareClassAndHookSelector函数的后半部分.

  1. UIImagePickerController_Aspects类添加了一个方法aliasSelector, demo中就是aspect_viewWillDisappear, 它的实现指向了原来UIImagePickerController类的viewWillDisappear的实现.
  2. UIImagePickerController_Aspects类的viewWillDisappear实现指向了_objc_msgForward, 这样调用就会启动oc的消息转发机制.

到这里, Aspects的hook流程就执行完了, 我们用下边这个图来描述下当前类和方法实现之间的关系

Aspects的实现为什么要生成一个原有类的子类, 个人理解是为了对原有类产生的影响尽可能小.(系统 KVO 也是这么做的)

hook后的执行流程

hook完成后, 我们来看下hook后代码的执行流程.

这一段很重要!!!

  1. UIImagePickerController实例发送viewWillDisappear消息的时候, 首先应该去查找实例所对应的类的方法列表, 由于UIImagePickerControllerisa指向了UIImagePickerController_Aspects类, 就会去UIImagePickerController_Aspects类(子类)中查找, 结果是查找不到viewWillDisappear实现,
  2. 然后会去查找父类UIImagePickerController的方法列表, 这时候查找到了viewWillDisappear的实现, 但是实现是指向了_objc_msgForward(全局 IMP), 直接进入消息转发流程.
  3. 按照消息转发流程, 注意这里找不到的方法是原方法, 即viewWillDisappear, 但是使用methodSignatureForSelector: 还是能拿到这个方法的签名的, 拿到签名后, 系统会调用UIImagePickerController_Aspects类的forwardInvocation方法, forwardInvocation方法被我们替换成了自定义实现__ASPECTS_ARE_BEING_CALLED__, 最终就进入了这个方法.

__ASPECTS_ARE_BEING_CALLED__的省略后的代码如下:

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
// This is the swizzled forwardInvocation: method.
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
SEL originalSelector = invocation.selector;
// 给invocation.selector 的 name 添加了aspects_前缀
SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
invocation.selector = aliasSelector;
AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
NSArray *aspectsToRemove = nil;

// 先执行 Hook 时机为调用原方法之前的方法
// Before hooks.
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);

// Instead hooks.
BOOL respondsToAlias = YES;
if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
//如果 option 为替代原方法, 那么这里执行替代方法, 原方法不执行
aspect_invoke(classContainer.insteadAspects, info);
aspect_invoke(objectContainer.insteadAspects, info);
}else {
// 如果 option 不是替代原方法, 那么在这里去执行原来的方法
Class klass = object_getClass(invocation.target);
do {
if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
[invocation invoke];
break;
}
}while (!respondsToAlias && (klass = class_getSuperclass(klass)));
}

// 原方法已经执行完了, 在这里执行 Hook 时机为调用原方法之后的方法
// After hooks.
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
}
  1. 对于hook的实例方法, 先拿到之前设置的切片代码信息, 存储在classContainer里.
  2. 通过invocation调用UIImagePickerController_Aspectsaspect_viewWillDisappear方法, 由于这个方法已经指向了原来的实现viewWillDisappear, 所以就调用了原始的代码.
  3. 在这之后, 如果Container里有 beforeAspects / insteadAspects / 原方法 / afterAspects, 就调用

到此为止, 就实现了在原来的实例方法执行后, 再执行hook插入的block代码.

OC消息转发机制

最后讲一下消息转发机制, 当系统判定方法不存在时, 会崩溃并抛出 unrecognized selector sent to … 的异常, 但是在崩溃前会给程序三次机会
image1

由上图消息转发流程图可以看出, 系统给了3次机会让我们来拯救.

  1. 方法决议: 调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。
  2. 快速转发: 调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。
  3. 消息转发: 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
    1. 调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。
    2. 调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。

JSPatch 实现方法替换也是使用的消息转发
MessageThrottle 实现节流/防抖同样使用了消息转发

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道