Aspects改进尝试

背景

一个库: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。最后继承链里只能一个类被修改,这个我没想通为什么这么做,因为我的方案在模拟器上实验,多个修改是没有问题的。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值