美文网首页AspectRumtimeiOS Developer
Aspect 源码剖析, 以及与jspatch兼容分析处理

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

作者: 咖啡兑水 | 来源:发表于2016-12-02 13:29 被阅读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;
    
    • 第一个方法可以对一个指定类的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将执行其自定义的方法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会去搜索基类的方法

    兼容处理

    • 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_AspectsForwardInvocationSelectorNameaddmethod到了子类XXXViewController_aspect_中,XXXViewController_aspect_的class方法在aspect_hookedGetClass本替换为基类XXXViewController,respondsToSelector是通过调用class,来找对应class的方法,自然就找不到AspectsForwardInvocationSelectorName了。

    • 替换respondsToSelector部分

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

    相关文章

      网友评论

        本文标题:Aspect 源码剖析, 以及与jspatch兼容分析处理

        本文链接:https://www.haomeiwen.com/subject/yutqmttx.html