Aspects改進嘗試

背景

一個庫:Aspects
兩篇文章:面向切面編程之 Aspects 源碼解析及應(yīng)用
消息轉(zhuǎn)發(fā)機制與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?這會導(dǎo)致沒有返回值
  2. 為什么繼承鏈里只能被修改一次?
  3. 為什么沒有類方法修改?

嘗試解決

看它的代碼的時候,發(fā)現(xiàn)并沒有想象的簡單,在我的想法里,插入一段代碼,就是:把原本的method和另一個method切換,然后在那個method里調(diào)用原來的method和插入的代碼。就跟你想在一個方法里添加一段代碼那樣去寫,我覺得這是最直觀的了??墒撬詈罄@到了forwardInvocation里去了。

簡單說,就是把原來的method的實現(xiàn)搞沒了去,然后利用OC的消息轉(zhuǎn)發(fā)特性最后轉(zhuǎn)發(fā)到了forwardInvocation方法。用這個方法有兩個壞處:

  1. 沒有返回值,forwardInvocation的返回值是void,所以如果你修改的方法原本是有返回值的,會被搞沒有。
    2. 會和其他的swizzle庫沖突,因為forwardInvocation方法只有一個,你搞一個自己的實現(xiàn),它搞一個自己的實現(xiàn)。后一個就擠掉前面的想了下是有解決辦法的,但是要所有的庫都同時遵守,即調(diào)用完自己的實現(xiàn)都要調(diào)用原來的實現(xiàn),如果同時有多個庫,那么這個原來的實現(xiàn)可能就是別的庫的實現(xiàn),這樣就可以實現(xiàn)一個鏈?zhǔn)秸{(diào)用,大家都會調(diào)用。

反正我就嘗試按直覺的那樣去寫, 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切換成我定義的一個通用函數(shù)injectedCommonFunc(位置1)。
    這個函數(shù)定義得跟objc_msgSend一樣:id injectedCommonFunc(id self, SEL selector, ...)。我的想法是使用變參函數(shù)來應(yīng)對不確定的情況。定義兩個這樣的函數(shù),一個有返回值一個沒返回值就可以了,可以根據(jù)Method的typeEncoding獲取返回值情況,然后決定使用哪個。

  2. 添加一個新方法指向原來的IMP,新方法名使用一個前綴加原來的方法名(位置2)。

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

轉(zhuǎn)發(fā)到injectedCommonFunc

經(jīng)過上面的處理,調(diào)用原方法后,實際執(zhí)行的是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循環(huán)的目的是:沿著繼承鏈向上找到和類綁定的block。因為我想設(shè)計的效果是,代碼插入效果是可以被子類繼承的,所以插入的block可能會在某個父類里,而不是和當(dāng)前調(diào)用者的class綁定。所以要追溯向上找到。

那么接下來的問題就是怎么調(diào)用這個block?

這里的關(guān)鍵問題是參數(shù)是未知的,而block只是id類型,不是變參函數(shù)。所以我借鑒了Aspects,使用NSInvocation。

  • 構(gòu)建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;

這里默認(rèn)的認(rèn)知是,block的參數(shù)類型和被插入代碼的方法類型是一樣的,某則沒法搞。

  • 獲取原方法的簽名originSignature,因為OC方法自帶self和selector兩個參數(shù),所以實際參數(shù)從第三個開始。

  • 先把返回值類型賦值給blockTypes,然后從第三個參數(shù)開始,依次把參數(shù)類型拷貝過去。

  • 然后由類型字符串blockTypes構(gòu)建簽名blockSignature;由簽名構(gòu)建blockInvocation

  • 給blockInvocation設(shè)置參數(shù)

    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);

使用變參函數(shù)的性質(zhì),把參數(shù)一個個取出來,但是要直到類型才能取。但是因為有*block參數(shù)類型和原方法一致的設(shè)定,那么參數(shù)類型是直到的。所以對不同的argType,調(diào)用不同的類型取值。比如:@表示對象,即id,那就調(diào)用va_arg(params, id)取值。這些對應(yīng)關(guān)系在Type Encodings里。

原方法的調(diào)用也使用NSInvocation來調(diào)用,因為發(fā)現(xiàn)也沒有辦法傳遞參數(shù)。但它和blockInvocation類型,也不必多做多少處理。

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

這里有個小坑:getReturnValue的結(jié)果是直接把內(nèi)存賦值給returnValue,沒有做任何內(nèi)存管理相關(guān)的操作的,相當(dāng)于沒有retain,如果你用一個__strong類型的變量去接,后面用完了會release,這樣就會堆出來一個release, 然后crash。所以先用一個__weak指針或void*指針去接,然后轉(zhuǎn)到正確類型。

轉(zhuǎn)折

一開始跑得都挺好的,直到我突然發(fā)現(xiàn)不行了,怎么會?我明明沒有修改什么東西?然后我猛地意識到似乎之前都是在模擬器上跑!-_-

關(guān)鍵點在變參函數(shù)取不到值了,而在模擬器上是可以的。

我仔細(xì)看了下變參函數(shù)獲取參數(shù)的那幾個宏:va_list,va_start,va_argva_end

網(wǎng)上可以查到他們的定義,原理是依靠參數(shù)入棧的規(guī)律:參數(shù)由后往前逐個入棧,且地址從高到底一次排列。這樣只要知道了其中某個參數(shù)的位置,其他參數(shù)都可以通過類型一次找出來。

但可惜的是,經(jīng)過觀察,iOS和mac上都不是這樣的!我看到的結(jié)論是:

  • 固定參數(shù)的位置和變參的位置是在不同的區(qū)域,并且不是緊貼這的。

  • 固定參數(shù)的位置是一次排列的,但是是前往后,地址逐漸降低,而不是升高

  • 使用va_start(ap, param)用來定位第一個變參函數(shù)的位置,這個在模擬和真機上有區(qū)別,正是這個導(dǎo)致了整個方案的失敗。

    • 在模擬器上,va_start得到的位置是根據(jù)函數(shù)自身來確定的,比如你有一個固定參數(shù),那么定位的是第二個參數(shù),如果你有固定參數(shù),那么定位的就是第三個參數(shù)。
    • 在真機上,va_start定位似乎是根據(jù)內(nèi)存分布來的,調(diào)用函數(shù)的時候,哪些是固定,哪些是變參就已經(jīng)確定好了,跟函數(shù)定義沒關(guān)系。
    • 舉例:
    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:這個方法實際是有兩個參數(shù)的,在真機上,va_start永遠(yuǎn)定位第一個參數(shù)known_xx0,因為調(diào)用的時候,轉(zhuǎn)成(NSString *(*)(id self, SEL selector, ...)類型來調(diào)用的,所有4個參數(shù)都是變參。如果改成(id self, SEL selector,id name, ...)就會是第二個參數(shù)known_xx1。
    而在模擬就永遠(yuǎn)定位在第三個參數(shù),因為函數(shù)有兩個定參。

  • 所以在模擬器上,我把一個有n個固定參數(shù)的方法的IMP指向一個變參函數(shù)injectedCommonFunc,我還是可以去得到所有的參數(shù)值的。而在真機上,原本調(diào)用的時候就沒有變參,va_start定位就是空,取不到固定參數(shù)。

最后

最后,我想到了objc_msgsend,我們調(diào)用函數(shù)都是通過它轉(zhuǎn)發(fā),它的參數(shù)類型也是(id self, SEL selector, ...),那么它又是怎么做到把固定參數(shù)和變參都取到的?

然后就找到mikeash的一篇文章,翻譯, 原文。關(guān)于參數(shù)的部分看了下,用的匯編。

“整型數(shù)和指針參數(shù)會被傳入寄存器 %rsi, %rdi, %rdx, %rcx, %r8 和 %r9。其他類型的參數(shù)會被傳進棧(stack)中” 之類的處理,但明確的事,沒有開放的函數(shù)/接口可以用來處理這些事,即使猜到了內(nèi)部的處理,也是不穩(wěn)定的,因為沒有開放接口,那么內(nèi)部的改變就不需要對外界負(fù)責(zé)。

到此也明白了為什么要用forwardInvocation來做處理,而不是自定義的函數(shù),因為forwardInvocation自帶一個NSInvocation參數(shù),包含了原方法所有的參數(shù)信息。至于類方法的修改,使用object_getClass(self)來做調(diào)用者,因為類方法放在metaClass里,object_getClass(self)當(dāng)self本身就是Class是得到的就是它的metaClass。最后繼承鏈里只能一個類被修改,這個我沒想通為什么這么做,因為我的方案在模擬器上實驗,多個修改是沒有問題的。

所以就到此結(jié)束了,當(dāng)一次學(xué)習(xí)吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容