AspectRumtimeiOS Developer

Aspect 源码剖析, 以及与jspatch兼容分析处理

2016-12-02  本文已影响0人  咖啡兑水

切面编程(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;

源码剖析

如果替换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的几个内置类

切面信息存储器,存储要切面的selector,切面的block,切面方式(替换,before,after),每一次添加切面都会生成一个AspectIdentifier,加入到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所做的事情是

__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

看样子是类对象的切面用一种实现法案,实例对象又用了另外一套实现方案,其实终归思想都是进入__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别名方法将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);
// 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执行就可以了

最后再聊两点

对于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)];

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将执行其自定义的方法KQForwardInvocationKQForwardInvocation将执行js代码(依赖于JavaScriptCore

aspects和jspatch的兼容

问题描述

如果项目同时用到这两个库,同时hook一个class的时候就会出现问题!

在我们的项目中遇到这样一种情况。举例示范一下, js代码如下

defineClass('XXXViewController', {
    getBackBarItemTitle: function(nextVC) {
        return "";
    },
});

由于Jspatch由于项目代码先执行,会将forwardInvocationimpl替换为KQForwardInvocationimpl

XXXViewController的一个selector(不是getBackBarItemTitle),被aspect也hook了,这就又发生了一次替换:会将forwardInvocationimpl替换为__ASPECTS_ARE_BEING_CALLED__impl

这样一来Jspatch的代码就被换掉了,不能被执行了,而且遇到一个assert

解释是,aspect并没有hook这样的getBackBarItemTitle的方法, 因此respondsToAlias == NO, 之后originalForwardInvocationSEL为nil走到了else里,但originalForwardInvocationSEL为什么为空呢?应该为KQForwardInvocationimpl才对。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的KQForwardInvocationimpl呢?还有一个方法class_getInstanceMethod, 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_AspectsForwardInvocationSelectorNameaddmethod到了子类XXXViewController_aspect_中,XXXViewController_aspect_的class方法在aspect_hookedGetClass本替换为基类XXXViewController,respondsToSelector是通过调用class,来找对应class的方法,自然就找不到AspectsForwardInvocationSelectorName了。

仍然使用class_getInstanceMethodobject_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

上一篇下一篇

猜你喜欢

热点阅读