1.前言
aop
编程(面向切面编程),其原理也就是在不更改正常的业务处理流程的前提下,通过生成一个动态代理类,从而实现对目标对象嵌入附加的操作。
在 iOS
中,要想实现相似的效果也很简单,利用 OC
的动态性,通过 swizzling method
改变目标函数的 selector
所指向的实现,然后在新的实现中实现附加的操作,完成之后再回到原来的处理逻辑。想明白这些之后,我就打算动手实现,当然并没有重复造轮子,我在 github
发现了一个基于 swizzling method
的开源框架 Aspects 。这个库的代码量比较小,总共就一个类文件,使用起来也比较方便,比如你想统计某个controller
的 viewwillappear
的调用次数,你只需要引入 Aspect.h
头文件,然后在合适的地方初始化如下代码即可。
- (void)addKvLogAspect {
[self wr_Aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^{
KVLog_ReviewTimeline(ReviewTimeline_Open_Tab);
}error:NULL];
}
这篇文章主要是介绍 Aspects
源码以及其思路,以及我在实际应用中遇到的一些问题。对 swizzling method
不了解的同学可以先去网上了解一下,下面的内容是基于大家对swizzling method
有一定的了解的基础上的。
2. 基本原理
我们知道 OC
是动态语言,我们执行一个函数的时候,其实是在发一条消息:[receiver message]
,这个过程就是根据 message
生成 selector
,然后根据 selector
寻找指向函数具体实现的指针 IMP
,然后找到真正的函数执行逻辑。这种处理流程给我们提供了动态性的可能,试想一下,如果在运行时,动态的改变了 selector
和 IMP
的对应关系,那么就能使得原来的[receiver message]
进入到新的函数实现了。
那么具体怎么实现这样的动态替换了?
直观的一种方案是提供一个统一入口,如commonImp
,将所有需要 hook
的函数都指向这个函数,然后在这里,提取相关信息进行转发,JSPatch 实现原理详解对此方案的可行性有进行分析,对于64位机器可能会有点问题。另外一个方法就是利用 oc
自己的消息转发机制进行转发,Aspects
的大体思路,基本上是顺着这个来的。为了更好的解释这个过程,我们先来看一下消息具体是怎么找到对应的imp
的,见下图。
[图片上传失败...(image-63d043-1546396164997)]
从上面我们可以发现,在发消息的时候,如果selector
有对应的 IMP
,则直接执行,如果没有,oc
给我们提供了几个可供补救的机会,依次有 resolveInstanceMethod
、forwardingTargetForSelector
、forwardInvocation
。Aspects 之所以选择在 forwardInvocation
这里处理是因为,这几个阶段特性都不太一样:resolvedInstanceMethod
适合给类/对象动态添加一个相应的实现,forwardingTargetForSelector
适合将消息转发给其他对象处理,相对而言,forwardInvocation
是里面最灵活,最能符合需求的。因此 Aspects 的方案就是,对于待 hook 的 selector,将其指向 objc_msgForward
/ _objc_msgForward_stret
,同时生成一个新的 aliasSelector
指向原来的 IMP,并且 hook 住 forwardInvocation
函数,使他指向自己的实现。按照上面的思路,当被 hook 的 selector 被执行的时候,首先根据 selector 找到了 objc_msgForward
/ _objc_msgForward_stret
,而这个会触发消息转发,从而进入 forwardInvocation
。同时由于 forwardInvocation
的指向也被修改了,因此会转入新的 forwardInvocation
函数,在里面执行需要嵌入的附加代码,完成之后,再转回原来的 IMP。
3. 源码分析
3.1 数据结构
介绍完大致思路之后,下面将从代码层来来具体分析。从头文件中可以看到使用aspects有两种使用方式:1)类方法 2)实例方法
+ (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add((id)self, selector, options, block, error);
}
/// @return A token which allows to later deregister the aspect.
- (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add(self, selector, options, block, error);
}
两者的主要原理基本差不多,这里不做一一介绍,只是以实例方法为例进行说明。在介绍之前,先介绍里面几个重要的数据结构:
AspectOptions
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
AspectPositionAfter = 0, /// Called after the original implementation (default)
AspectPositionInstead = 1, /// Will replace the original implementation.
AspectPositionBefore = 2, /// Called before the original implementation.
AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};
这里表示了 block 执行的时机,也就是额外操作的执行时机,在我的应用场景中就是打点逻辑的执行时机,它可以在原始函数执行之前,也可以是执行之后,甚至可以完全替换掉原来的逻辑。
AspectsContainer
一个对象或者类的所有的 Aspects 整体情况
@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
AspectIdentifier
一个 Aspect 的具体内容
// 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)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
这里主要包含了单个的 aspect 的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等
AspectInfo
一个 Aspect 执行环境,主要是 NSInvocation 信息。
@interface AspectInfo : NSObject
- (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
3.2 代码流程
有了上面的了解,我们就能更好的分析整个 apsects 的执行流程。添加一个 aspect 的关键流程如下图所示:
[图片上传失败...(image-ab743e-1546397180123)]
从代码来看,要想使用 aspects ,首先要添加一个 aspect ,可以通过上面介绍的类/实例方法。关键代码实现如下:
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(^{
//判断能否hook
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];
// Modify the class to allow message interception.
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}
这个过程基本和上面的流程图一致,这里重点介绍几个关键部分。
3.2.1 判断能否被 hook
对于对象实例而言,这里主要是根据黑名单,比如 retain forwardInvocation
等这些方法在外部是不能被hook
,(对于类对象还要确保同一个类继承关系层级中,只能被 hook
一次,因此这里需要判断子类,父类有没有被 hook
,之所以做这样的实现,主要是为了避免出现死循环的出现,这里有相关的讨论)。如果能够 hook
,则继续下面的步骤。
3.2.2 swizzling method
这是真正的核心逻辑,swizzling method
主要有两部分,一个是对对象的 forwardInvocation
进行 swizzling
,另一个是对传入的 selector
进行 swizzling
.
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
Class klass = aspect_hookClass(self, error);//1 swizzling forwardInvocation
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {//2 swizzling method
// Make a method alias for the existing method implementation, it not already copied.
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);
}
// We use forwardInvocation to hook in.
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}
3.2.2.1 swizzling forwardInvocation:
aspect_hookClass
函数主要swizzling
类/对象的 forwardInvocation
函数,aspects
的真正的处理逻辑都是在 forwradInvocation
函数里面进行的。对于对象实例而言,源代码中并没有直接 swizzling
对象的 forwardInvocation
方法,而是动态生成一个当前对象的子类,并将当前对象与子类关联,然后替换子类的 forwardInvocation
方法(这里具体方法就是调用了 object_setClass(self, subclass)
,将当前对象 isa
指针指向了 subclass
,同时修改了subclass
以及其 subclass metaclass
的 class
方法,使他返回当前对象的 class
。,这个地方特别绕,它的原理有点类似kvo
的实现,它想要实现的效果就是,将当前对象变成一个 subclass
的实例,同时对于外部使用者而言,又能把它继续当成原对象在使用,而且所有的swizzling
操作都发生在子类,这样做的好处是你不需要去更改对象本身的类,也就是,当你在 remove aspects
的时候,如果发现当前对象的 aspect 都被移除了,那么,你可以将isa
指针重新指回对象本身的类,从而消除了该对象的 swizzling
,同时也不会影响到其他该类的不同对象)。对于每一个对象而言,这样的动态对象只会生成一次,这里 aspect_swizzlingForwardInvocation
将使得 forwardInvocation
方法指向 aspects
自己的实现逻辑 ,具体代码如下:
static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
Class statedClass = self.class;
Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass);
// Already subclassed
if ([className hasSuffix:AspectsSubclassSuffix]) {
return baseClass;
// We swizzle a class object, not a single object.
}else if (class_isMetaClass(baseClass)) {
return aspect_swizzleClassInPlace((Class)self);
// Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
}else if (statedClass != baseClass) {
return aspect_swizzleClassInPlace(baseClass);
}
// Default case. Create dynamic subclass.
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);
//生成动态子类,并swizzling forwardInvocation方法
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;
}
aspect_swizzleForwardInvocation(subclass);
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
//将当前self设置为子类,这里其实只是更改了self的isa指针而已
object_setClass(self, subclass);
return subclass;
}
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));
}
由于子类本身并没有实现 forwardInvocation
,隐藏返回的 originalImplementation
将为空值,所以也不会生成 NSSelectorFromString(AspectsForwardInvocationSelectorName)
。
3.2.2.2 swizzling selector
当 forwradInvocation
被 hook
之后,接下来,将对传入的 selector
进行hook
,这里的做法是,将 selector
指向了转发 IMP
,同时生成一个aliasSelector
,指向了原来的IMP
,同时为了放在重复 hook
,做了一个判断,如果发现 selector
已经指向了转发IMP
,那就就不需要进行交换了,代码如下
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)) {
// Make a method alias for the existing method implementation, it not already copied.
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);
}
// We use forwardInvocation to hook in.
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}
3.2.3 handle ForwardInvocation
基于上面的代码分析知道,转发最终的逻辑代码最终转入 __ASPECTS_ARE_BEING_CALLED__
函数的处理中。这里,需要处理的部分包括额外处理代码(如打点代码)以及最终重新转会原来的 selector 所指向的函数,其实现代码如下:
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
...
// Before hooks. 原来逻辑之前执行
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);
// Instead hooks.
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];//根据aliasSelector找到原来的逻辑并执行
break;
}
}while (!respondsToAlias && (klass = class_getSuperclass(klass)));
}
// After hooks. 原来逻辑之后执行
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
// If no hooks are installed, call original implementation (usually to throw an exception)
if (!respondsToAlias) {//找不到aliasSelector的IMP实现,没有找到原来的逻辑,进行消息转发
invocation.selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
if ([self respondsToSelector:originalForwardInvocationSEL]) {
((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
} else {
[self doesNotRecognizeSelector:invocation.selector];
}
}
...
}
依次处理 before/instead/after hook
以及真正函数实现。如果没有找到原始的函数实现,还需要进行转发操作。
4. 遇到的问题
以上就是 Apsects 的实现了,接下来会介绍在实际应用过程中遇到的一些问题以及我的解决方案。