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);
__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]; if (identifier) { [aspectContainer addAspect:identifier withOptions:options]; aspect_prepareClassAndHookSelector(self, selector, error); } } }); return identifier; }
|
这段代码做了三件事情.
- 首先生成AspectIdentifier,
- 然后将AspectIdentifier加入到AspectsContainer中,
- 使用
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
| @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); Class klass = aspect_hookClass(self, error); Method targetMethod = class_getInstanceMethod(klass, selector); IMP targetMethodIMP = method_getImplementation(targetMethod); if (!aspect_isMsgForwardIMP(targetMethodIMP)) { const char *typeEncoding = method_getTypeEncoding(targetMethod); 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); }
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);
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String; Class subclass = objc_getClass(subclassName);
if (subclass == nil) { subclass = objc_allocateClassPair(baseClass, subclassName, 0); aspect_swizzleForwardInvocation(subclass); aspect_hookedGetClass(subclass, statedClass); aspect_hookedGetClass(object_getClass(subclass), statedClass); objc_registerClassPair(subclass); }
object_setClass(self, subclass); return subclass; }
|
- 通过运行时的函数
objc_allocateClassPair
定义了一个新的子类.如果是demo执行到这里的话, 生成的子类叫UIImagePickerController_Aspects
.
- 将子类的
forwardInvocation
替换为了自定义的实现函数__ASPECTS_ARE_BEING_CALLED__
.
- 将
UIImagePickerController
实例的isa
指针指向了子类UIImagePickerController_Aspects
.
然后再看aspect_prepareClassAndHookSelector
函数的后半部分.
UIImagePickerController_Aspects
类添加了一个方法aliasSelector
, demo中就是aspect_viewWillDisappear
, 它的实现指向了原来UIImagePickerController
类的viewWillDisappear
的实现.
- 将
UIImagePickerController_Aspects
类的viewWillDisappear
实现指向了_objc_msgForward
, 这样调用就会启动oc的消息转发机制.
到这里, Aspects的hook流程就执行完了, 我们用下边这个图来描述下当前类和方法实现之间的关系
Aspects的实现为什么要生成一个原有类的子类, 个人理解是为了对原有类产生的影响尽可能小.(系统 KVO 也是这么做的)
hook后的执行流程
hook完成后, 我们来看下hook后代码的执行流程.
这一段很重要!!!
- 往
UIImagePickerController
实例发送viewWillDisappear
消息的时候, 首先应该去查找实例所对应的类的方法列表, 由于UIImagePickerController
的isa
指向了UIImagePickerController_Aspects
类, 就会去UIImagePickerController_Aspects
类(子类)中查找, 结果是查找不到viewWillDisappear
实现,
- 然后会去查找父类
UIImagePickerController
的方法列表, 这时候查找到了viewWillDisappear
的实现, 但是实现是指向了_objc_msgForward
(全局 IMP), 直接进入消息转发流程.
- 按照消息转发流程, 注意这里找不到的方法是原方法, 即
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
| static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) { SEL originalSelector = invocation.selector; 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;
aspect_invoke(classContainer.beforeAspects, info); aspect_invoke(objectContainer.beforeAspects, info);
BOOL respondsToAlias = YES; if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) { aspect_invoke(classContainer.insteadAspects, info); aspect_invoke(objectContainer.insteadAspects, info); }else { Class klass = object_getClass(invocation.target); do { if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) { [invocation invoke]; break; } }while (!respondsToAlias && (klass = class_getSuperclass(klass))); }
aspect_invoke(classContainer.afterAspects, info); aspect_invoke(objectContainer.afterAspects, info); }
|
- 对于hook的实例方法, 先拿到之前设置的切片代码信息, 存储在classContainer里.
- 通过invocation调用
UIImagePickerController_Aspects
的aspect_viewWillDisappear
方法, 由于这个方法已经指向了原来的实现viewWillDisappear, 所以就调用了原始的代码.
- 在这之后, 如果Container里有 beforeAspects / insteadAspects / 原方法 / afterAspects, 就调用
到此为止, 就实现了在原来的实例方法执行后, 再执行hook插入的block代码.
OC消息转发机制
最后讲一下消息转发机制, 当系统判定方法不存在时, 会崩溃并抛出 unrecognized selector sent to …
的异常, 但是在崩溃前会给程序三次机会
由上图消息转发流程图可以看出, 系统给了3次机会让我们来拯救.
- 方法决议: 调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。
- 快速转发: 调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。
- 消息转发: 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
- 调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。
- 调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。
JSPatch 实现方法替换也是使用的消息转发
MessageThrottle 实现节流/防抖同样使用了消息转发