Aspects改进尝试

作者: FindCrt | 来源:发表于2017-06-23 22:43 被阅读464次

    背景

    一个库:Aspects
    两篇文章:面向切面编程之 Aspects 源码解析及应用
    消息转发机制与Aspects源码解析

    Aspects库的作用就是可以通过一行代码在某个类的某个方法里插入代码。
    核心接口:

    + (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;
    

    但是它有几个比较明显的问题:

    1. 为什么用 forwardInvocation?这会导致没有返回值
    2. 为什么继承链里只能被修改一次?
    3. 为什么没有类方法修改?

    尝试解决

    看它的代码的时候,发现并没有想象的简单,在我的想法里,插入一段代码,就是:把原本的method和另一个method切换,然后在那个method里调用原来的method和插入的代码。就跟你想在一个方法里添加一段代码那样去写,我觉得这是最直观的了。可是它最后绕到了forwardInvocation里去了。

    简单说,就是把原来的method的实现搞没了去,然后利用OC的消息转发特性最后转发到了forwardInvocation方法。用这个方法有两个坏处:

    1. 没有返回值,forwardInvocation的返回值是void,所以如果你修改的方法原本是有返回值的,会被搞没有。
      2. 会和其他的swizzle库冲突,因为forwardInvocation方法只有一个,你搞一个自己的实现,它搞一个自己的实现。后一个就挤掉前面的想了下是有解决办法的,但是要所有的库都同时遵守,即调用完自己的实现都要调用原来的实现,如果同时有多个库,那么这个原来的实现可能就是别的库的实现,这样就可以实现一个链式调用,大家都会调用。

    反正我就尝试按直觉的那样去写, demo在此

    +(void)injectAspectsToSelector:(SEL)selector block:(id)block error:(NSError **)error{
        
        if (![self isInjectAvailableForSelector:selector error:error]) {
            return;
        }
        
        Method originMethod = class_getInstanceMethod(self, selector);
        IMP originalIMP = method_getImplementation(originMethod);
        const char *originalTypes = method_getTypeEncoding(originMethod);
        //位置1
        class_replaceMethod(self, selector, (IMP)injectedCommonFunc, "@@:");
        SEL injectedselector = NSSelectorFromString([injectedSelectorPrefix stringByAppendingFormat:@"_%@",NSStringFromSelector(selector)]);
        //位置2
        BOOL addSucceed = class_addMethod(self, injectedselector, originalIMP, originalTypes);
        if (!addSucceed) {
            NSLog(@"%@ add method %@ failed",TFClassDesc(self), NSStringFromSelector(injectedselector));
        }
        //位置3
        objc_setAssociatedObject(self, injectedselector, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    

    核心就是这个方法了,selector是想要修改的方法,block是想插入的代码。

    1. 把原来的方法的IMP切换成我定义的一个通用函数injectedCommonFunc(位置1)。
      这个函数定义得跟objc_msgSend一样:id injectedCommonFunc(id self, SEL selector, ...)。我的想法是使用变参函数来应对不确定的情况。定义两个这样的函数,一个有返回值一个没返回值就可以了,可以根据Method的typeEncoding获取返回值情况,然后决定使用哪个。

    2. 添加一个新方法指向原来的IMP,新方法名使用一个前缀加原来的方法名(位置2)。

    3. 把要插入的block和被修改的类使用objc_setAssociatedObject绑定,并且key使用新方法。

    转发到injectedCommonFunc

    经过上面的处理,调用原方法后,实际执行的是injectedCommonFunc

    • 获取要插入的block
    Class realClass = object_getClass(self);
        
        SEL injectedselector = NSSelectorFromString([injectedSelectorPrefix stringByAppendingFormat:@"_%@",NSStringFromSelector(selector)]);
        
        //find the first injected block along the class inheritance chain
        id injectBlock;
        Class injectedClass = realClass;
        do {
            injectBlock = objc_getAssociatedObject(injectedClass, injectedselector);
        } while (!injectBlock && (injectedClass = class_getSuperclass(injectedClass)));
    

    这个do-while循环的目的是:沿着继承链向上找到和类绑定的block。因为我想设计的效果是,代码插入效果是可以被子类继承的,所以插入的block可能会在某个父类里,而不是和当前调用者的class绑定。所以要追溯向上找到。

    那么接下来的问题就是怎么调用这个block?

    这里的关键问题是参数是未知的,而block只是id类型,不是变参函数。所以我借鉴了Aspects,使用NSInvocation

    • 构建blockInvocation
    Method injectedMethod = class_getInstanceMethod(realClass, injectedselector);
        const char *originalTypes = method_getTypeEncoding(injectedMethod);
        
        NSMethodSignature *originSignature = [NSMethodSignature signatureWithObjCTypes:originalTypes];
        
        char *blockTypes = malloc(sizeof(char)*(strlen(originalTypes)+1));
        strcat(blockTypes, [originSignature methodReturnType]);
        strcat(blockTypes, "@?");
        for (int i = 2; i<[originSignature numberOfArguments]; i++) {
            strcat(blockTypes, [originSignature getArgumentTypeAtIndex:i]);
        }
        NSMethodSignature *blockSignature = [NSMethodSignature signatureWithObjCTypes:blockTypes];
        
        NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:blockSignature];
        NSInvocation *originalInvocation = [NSInvocation invocationWithMethodSignature:originSignature];
        originalInvocation.selector = injectedselector;
        originalInvocation.target = self;
    

    这里默认的认知是,block的参数类型和被插入代码的方法类型是一样的,某则没法搞。

    • 获取原方法的签名originSignature,因为OC方法自带self和selector两个参数,所以实际参数从第三个开始。

    • 先把返回值类型赋值给blockTypes,然后从第三个参数开始,依次把参数类型拷贝过去。

    • 然后由类型字符串blockTypes构建签名blockSignature;由签名构建blockInvocation

    • 给blockInvocation设置参数

        va_list params;
        va_start(params, selector);
    
        .......
        .......
    
        void *argument = NULL;
        
        id object = nil;
        int num_int;
        
        for (int i = 1; i< blockSignature.numberOfArguments; i++) {
            const char argType = [blockSignature getArgumentTypeAtIndex:i][0];
            
            //TODO: other arg types
            if (argType == _C_ID) {
                object = va_arg(params, id);
                argument = &object;
            }else if (argType == _C_INT){
                num_int = va_arg(params, int);
                argument = &num_int;
            }
            [blockInvocation setArgument:argument atIndex:i];
            [originalInvocation setArgument:argument atIndex:i+1];
        }
        
        va_end(params);
    

    使用变参函数的性质,把参数一个个取出来,但是要直到类型才能取。但是因为有*block参数类型和原方法一致的设定,那么参数类型是直到的。所以对不同的argType,调用不同的类型取值。比如:@表示对象,即id,那就调用va_arg(params, id)取值。这些对应关系在Type Encodings里。

    原方法的调用也使用NSInvocation来调用,因为发现也没有办法传递参数。但它和blockInvocation类型,也不必多做多少处理。

    • 调用NSInvocation,拿到返回值
        [blockInvocation invokeWithTarget:injectBlock];
        
        [originalInvocation invoke];
        
        void *returnValue = nil;
        [originalInvocation getReturnValue:&returnValue];
        
        return (__bridge id)(returnValue);
    

    这里有个小坑:getReturnValue的结果是直接把内存赋值给returnValue,没有做任何内存管理相关的操作的,相当于没有retain,如果你用一个__strong类型的变量去接,后面用完了会release,这样就会堆出来一个release, 然后crash。所以先用一个__weak指针或void*指针去接,然后转到正确类型。

    转折

    一开始跑得都挺好的,直到我突然发现不行了,怎么会?我明明没有修改什么东西?然后我猛地意识到似乎之前都是在模拟器上跑!-_-

    关键点在变参函数取不到值了,而在模拟器上是可以的

    我仔细看了下变参函数获取参数的那几个宏:va_list,va_start,va_argva_end

    网上可以查到他们的定义,原理是依靠参数入栈的规律:参数由后往前逐个入栈,且地址从高到底一次排列。这样只要知道了其中某个参数的位置,其他参数都可以通过类型一次找出来。

    但可惜的是,经过观察,iOS和mac上都不是这样的!我看到的结论是:

    • 固定参数的位置和变参的位置是在不同的区域,并且不是紧贴这的。

    • 固定参数的位置是一次排列的,但是是前往后,地址逐渐降低,而不是升高

    • 使用va_start(ap, param)用来定位第一个变参函数的位置,这个在模拟和真机上有区别,正是这个导致了整个方案的失败

      • 在模拟器上,va_start得到的位置是根据函数自身来确定的,比如你有一个固定参数,那么定位的是第二个参数,如果你有固定参数,那么定位的就是第三个参数。
      • 在真机上,va_start定位似乎是根据内存分布来的,调用函数的时候,哪些是固定,哪些是变参就已经确定好了,跟函数定义没关系。
      • 举例:
      IMP unknownIMP = class_getMethodImplementation([TFPerson class], @selector(unknownParamsFunc:otherSome:));
        ((NSString *(*)(id self, SEL selector, ...))unknownIMP)(person,@selector(unknownParamsFunc:otherSome:),@"known_xx0",@"known_xx1",@"known_xx2",@"known_xx3");
      

      unknownParamsFunc:otherSome:这个方法实际是有两个参数的,在真机上,va_start永远定位第一个参数known_xx0,因为调用的时候,转成(NSString *(*)(id self, SEL selector, ...)类型来调用的,所有4个参数都是变参。如果改成(id self, SEL selector,id name, ...)就会是第二个参数known_xx1
      而在模拟就永远定位在第三个参数,因为函数有两个定参。

    • 所以在模拟器上,我把一个有n个固定参数的方法的IMP指向一个变参函数injectedCommonFunc,我还是可以去得到所有的参数值的。而在真机上,原本调用的时候就没有变参,va_start定位就是空,取不到固定参数。

    最后

    最后,我想到了objc_msgsend,我们调用函数都是通过它转发,它的参数类型也是(id self, SEL selector, ...),那么它又是怎么做到把固定参数和变参都取到的?

    然后就找到mikeash的一篇文章,翻译, 原文。关于参数的部分看了下,用的汇编。

    “整型数和指针参数会被传入寄存器 %rsi, %rdi, %rdx, %rcx, %r8 和 %r9。其他类型的参数会被传进栈(stack)中” 之类的处理,但明确的事,没有开放的函数/接口可以用来处理这些事,即使猜到了内部的处理,也是不稳定的,因为没有开放接口,那么内部的改变就不需要对外界负责。

    到此也明白了为什么要用forwardInvocation来做处理,而不是自定义的函数,因为forwardInvocation自带一个NSInvocation参数,包含了原方法所有的参数信息。至于类方法的修改,使用object_getClass(self)来做调用者,因为类方法放在metaClass里,object_getClass(self)当self本身就是Class是得到的就是它的metaClass。最后继承链里只能一个类被修改,这个我没想通为什么这么做,因为我的方案在模拟器上实验,多个修改是没有问题的。

    所以就到此结束了,当一次学习吧。

    相关文章

      网友评论

      本文标题:Aspects改进尝试

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