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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容