切面编程(AOP)
Aspect是切面编程的代表作之一,ios平台。AOP是Aspect Oriented Program的首字母缩写。当我们想在某个方法前后加入一个方法,比如打日志,又不想修改原来的代码,保留原有代码的结构和可读性,切面编程是个不错的选择。
Aspect 功能用法
/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (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;
- 第一个方法可以对一个指定类的selector,做切面,通过option可以指定在某个selector的前后执行或者替换
- 第二个方法可以对一个指定的实例的selector,做切面,通过option可以指定在某个selector的前后执行或者替换
- block的第一个参数是id<AspectInfo>(切面信息),后面参数与selector参数匹配,也可以不必写全
源码剖析
如果替换oc的一个方法,大家想到的runtime方式是
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
这个函数可以替换一个类的类方法或实例方法。
然而有时候我们只想替换某个实例的方法,而不是针对这个类。可惜的是,runtime没有提供方便的接口给我们,或者我们自己用runtime也想不到怎么才能实现只替换一个实例的方法。
我们来深入aspect源码,看看它究竟是怎么做的。过程中我会穿插介绍aspect所涉及的模块。
无论是替换类的方法,还是替换某个实例的方法,最终都进入aspect_add这个函数
static id aspect_add(id self, SEL selector, AspectOptions options, id block, const char* typeEncoding, NSError **error)
aspect_add 中
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);
}
这里大家看到Aspects的几个内置类
- AspectIdentifier
切面信息存储器,存储要切面的selector,切面的block,切面方式(替换,before,after),每一次添加切面都会生成一个
AspectIdentifier
,加入到AspectsContainer
切面容器
- AspectsContainer
与类本身或某个实例相关联的一个切面(AspectIdentifier)容器
到这里只是记录了一下切面,后面会用到!
我们接下来看下重要的hook部分
首先关注aspect_prepareClassAndHookSelector
中的aspect_hookClass
这里针对类对象和实例对象做了不同的处理,先说类对象
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);
}
statedClass和baseClass
statedClass和baseClass的区别就是object_getClass和class method的区别,简单说self如果是类对象,object_getClass拿到isa(baseClass)要么是元类要么是被kvo替换的kvo类, statedClass就是类对象本身,如果是实例对象,statedClass和baseClass就都是类对象。
如果是类对象就启用aspect_swizzleClassInPlace
->aspect_swizzleForwardInvocation
aspect_swizzleForwardInvocation所做的事情是
- 替换实例方法
forwardInvocation
为__ASPECTS_ARE_BEING_CALLED__
- 添加一个新的命名为AspectsForwardInvocationSelectorName的selector,保存
forwardInvocation
原来的实现
__ASPECTS_ARE_BEING_CALLED__
就是执行AspectsContainer
切面容器中记录的切面方法,这个稍后具体再介绍
AspectsForwardInvocationSelectorName
当没有任何切面的时候执行,相当于会直接走forwardInvocation
原来的实现
// Default case. Create dynamic subclass.
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;
}
aspect_swizzleForwardInvocation(subclass);
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
object_setClass(self, subclass);
对于实例,用了动态创建subclass的方式来做,这个类似于kvo的做法(作者的灵感可能来自与此),修改isa
- 替换实例方法
forwardInvocation
为__ASPECTS_ARE_BEING_CALLED__
,并在子类中添加 - 添加一个新的命名为AspectsForwardInvocationSelectorName的selector,保存
forwardInvocation
原来的实现 -
object_setClass(self, subclass)
将实例调用转移到子类
看样子是类对象的切面用一种实现法案,实例对象又用了另外一套实现方案,其实终归思想都是进入
__ASPECTS_ARE_BEING_CALLED__
,然后执行自己一开始记录的切面方法。基于这种思想,这两套设计,可以合并成一套,都可直接使用类对象的切面方式,不必动态创建子类,只要把aspect清理工作调整一下,他的单元测试还是可以跑的通的,大家有兴趣的话可以动手试一试。这里就不必纠结了,记住他的终归思想就可以了。
我们继续,hook之后
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));
}
这一步也是非常关键的,没有这一步,想跑到__ASPECTS_ARE_BEING_CALLED__
也是不可能的
- 针对调用的selector,添加一个aspect别名方法,实现用selector原有实现,目的是保存原有方法
- 将selector转移到forwardInvocation
当我们要还原selector的方法的时候,用aspect别名方法将forwardInvocation imp replace,并将forwardInvocation还原为AspectsForwardInvocationSelectorName(forwardInvocation)的原有实现.说白了就是自己挖了多少坑记得都要填平,aspect的remove就是这样做的, 可参看
aspect_cleanupHookedClassAndSelector
到此为止,准备工作都已经做好。当切面的方法被调用的时候,在__ASPECTS_ARE_BEING_CALLED__
打断点,就会进来了
小贴士:aspects 所有参数进入的地方都做了
ParameterAssert
值得我们再编码的时候学习
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;
// 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];
break;
}
}while (!respondsToAlias && (klass = class_getSuperclass(klass)));
}
// After hooks.
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
- 拿容器,使用了关联属性,aspect_add的时候
aspect_getContainerForObject
会创建AspectsContainer
与自己关联,类对象关联在类对象上,实例对象关联在实例对象上
// Loads or creates the aspect container.
static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) {
NSCParameterAssert(self);
SEL aliasSelector = aspect_aliasForSelector(selector);
AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);
if (!aspectContainer) {
aspectContainer = [AspectsContainer new];
objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);
}
return aspectContainer;
}
- 遍历并执行容器切面
apsect在一开始拿出了两组容器,都检查调用,beforeAspects
放在前面调用,afterAspects
放在后面调用, insteadAspects
放在中间调用,这些是根据add时候传入的aspect option来填充的
回到文章开始提出的问题,如何只替换实例的对象的方法,看到这里就解决了,实例对象会拿出自己的AspectsContainer
,将insteadAspects
执行就可以了
最后再聊两点
- AspectOptionAutomaticRemoval实现
对于option为AspectOptionAutomaticRemoval
, 在执行一次后就remove了
#define aspect_invoke(aspects, info) \
for (AspectIdentifier *aspect in aspects) {\
[aspect invokeWithInfo:info];\
if (aspect.options & AspectOptionAutomaticRemoval) { \
aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \
} \
}
// Remove any hooks that are queued for deregistration.
[aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
- block如何传
aspect的block,是id类型,那么用户传怎样的block进来呢,深入aspect invokeWithInfo
可以找到答案
for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
NSUInteger argSize;
NSGetSizeAndAlignment(type, &argSize, NULL);
if (!(argBuf = reallocf(argBuf, argSize))) {
AspectLogError(@"Failed to allocate memory for block invocation.");
return NO;
}
[originalInvocation getArgument:argBuf atIndex:idx];
[blockInvocation setArgument:argBuf atIndex:idx];
}
他使用你切面selector的参数,依次填入你的block,就是你的block的参数(除去第一个参数)可以和切面selector的参数相同,当然也允许你的block参数少于selector的参数个数,但不能多于。
jspatch原理分析
jspatch 也是自定义forwardInvocation。
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)KQForwardInvocation) {
IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)KQForwardInvocation, "v@:@");
class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
}
#pragma clang diagnostic pop
Jspatch动态添加方法ORIGforwardInvocation存储forwardInvocation
原来的实现,forwardInvocation
将执行其自定义的方法KQForwardInvocation
。KQForwardInvocation
将执行js代码(依赖于JavaScriptCore
)
aspects和jspatch的兼容
问题描述
如果项目同时用到这两个库,同时hook一个class的时候就会出现问题!
在我们的项目中遇到这样一种情况。举例示范一下, js代码如下
defineClass('XXXViewController', {
getBackBarItemTitle: function(nextVC) {
return "";
},
});
由于Jspatch由于项目代码先执行,会将forwardInvocation
的impl
替换为KQForwardInvocation
的impl
可XXXViewController
的一个selector(不是getBackBarItemTitle),被aspect也hook了,这就又发生了一次替换:会将forwardInvocation
的impl
替换为__ASPECTS_ARE_BEING_CALLED__
的impl
这样一来Jspatch的代码就被换掉了,不能被执行了,而且遇到一个assert
解释是,aspect并没有hook这样的getBackBarItemTitle
的方法, 因此respondsToAlias
== NO, 之后originalForwardInvocationSEL
为nil走到了else里,但originalForwardInvocationSEL
为什么为空呢?应该为KQForwardInvocation
的impl
才对。originalForwardInvocationSEL
来自于下面的代码
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));
}
经验证class_replaceMethod
返回为nil,
class_replaceMethod
解释是
The previous implementation of the method identified by name for the class identified by cls.
replace返回值不会返回基类方法实现,只会在本类中搜索,因此XXXViewController
中没有定义forwardInvocation
,就会返回nil。
那么我们如何得到Jspatch的KQForwardInvocation
的impl
呢?还有一个方法class_getInstanceMethod
, class_getInstanceMethod
会去搜索基类的方法
兼容处理
- 将
class_replaceMethod
改为class_getInstanceMethod
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@:@");
+ // get origin forwardInvocation impl, include superClass impl,not NSObject impl, and class method to kClass
+ Method originalMethod = class_getInstanceMethod(klass, @selector(forwardInvocation:));
+ if (originalMethod != class_getInstanceMethod([NSObject class], @selector(forwardInvocation:))) {
+ IMP originalImplementation = method_getImplementation(originalMethod);
+ if (originalImplementation) {
+ // If there is no method, replace will act like class_addMethod.
+ class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
+ }
}
+ class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
但这时仍遇到之前的assert,这个原因比较绕,因为XXXViewController
被动态创建了子类XXXViewController_aspect_
,AspectsForwardInvocationSelectorName
addmethod到了子类XXXViewController_aspect_
中,XXXViewController_aspect_
的class方法在aspect_hookedGetClass
本替换为基类XXXViewController
,respondsToSelector是通过调用class,来找对应class的方法,自然就找不到AspectsForwardInvocationSelectorName
了。
- 替换respondsToSelector部分
仍然使用class_getInstanceMethod
,object_getClass(self)
会拿isa, 也就是XXXViewController_aspect_
,不会走class方法
if (!respondsToAlias) {
invocation.selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
- if ([self respondsToSelector:originalForwardInvocationSEL]) {
- ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
- }else {
+ Method method = class_getInstanceMethod(object_getClass(self), originalForwardInvocationSEL);
+ if (method) {
+ typedef void (*FuncType)(id, SEL, NSInvocation *);
+ FuncType imp = (FuncType)method_getImplementation(method);
+ imp(self, selector, invocation);
+ } else {
[self doesNotRecognizeSelector:invocation.selector];
}
总结
如此以来就可以做到,jspatch和aspect可以共同hook一个class了,两者都能起到作用,但目前不能同时hook一个class的seletor
!
网友评论