全面談?wù)凙spects和JSPatch兼容問題(更新建議)

1. 背景

AspectsJSPatch 是 iOS 開發(fā)中非常常見的兩個(gè)庫。Aspects 提供了方便簡單的方法進(jìn)行面向切片編程(AOP),JSPatch可以讓你用 JavaScript 書寫原生 iOS APP 和進(jìn)行熱修復(fù)。關(guān)于實(shí)現(xiàn)原理可以參考 面向切面編程之 Aspects 源碼解析及應(yīng)用JSPatch wiki。簡單地概括就是將原方法實(shí)現(xiàn)替換為_objc_msgForward(或_objc_msgForward_stret),當(dāng)執(zhí)行這個(gè)方法是直接進(jìn)入消息轉(zhuǎn)發(fā)過程,最后到達(dá)替換后的-forwardInvocation:,在-forwardInvocation:內(nèi)執(zhí)行新的方法,這是兩者的共同原理。最近項(xiàng)目開發(fā)中需要用 JSPatch 替換方法修復(fù)一個(gè) bug ,然而這個(gè)方法已經(jīng)使用 Aspects 進(jìn)行 hook 過了,那么兩者同時(shí)使用會不會有問題呢?關(guān)于這個(gè)問題,網(wǎng)上介紹比較詳細(xì)的是 面向切面編程之 Aspects 源碼解析及應(yīng)用有關(guān)Swizzling的一個(gè)問題,深入研究后發(fā)現(xiàn)這兩篇文章講得都不夠全面。本文基于 Aspects 1.4.1 和 JSPatch 1.1 介紹幾種測試結(jié)果和原因。

2. 測試

2.0. 源碼

這是本文使用的測試代碼,你可以clone下來,泡杯咖啡,找個(gè)安靜的地方跟著本文一步一步實(shí)踐。

2.1. 代碼說明

ViewController.m中首先定義一個(gè)簡單類MyClass,只有-test-test2方法,方法內(nèi)打印log


@interface MyClass : NSObject
- (void)test;
- (void)test2;
@end

@implementation MyClass
- (void)test {
    NSLog(@"MyClass origin log");
}
- (void)test2 {
    NSLog(@"MyClass test2 origin log");
}
@end

接著是三個(gè)hook方法,分別是對-test進(jìn)行hook-jp_hook、-aspects_hook和對-test2進(jìn)行hook-aspects_hook_test2

- (void)jp_hook {
    [JPEngine startEngine];
    NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
    NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
    [JPEngine evaluateScript:script];
}
- (void)aspects_hook {
    [MyClass aspect_hookSelector:@selector(test) withOptions:AspectPositionAfter usingBlock:^(id aspects) {
        NSLog(@"aspects log");
    } error:nil];
}
- (void)aspects_hook_test2 {
    [MyClass aspect_hookSelector:@selector(test2) withOptions:AspectPositionInstead usingBlock:^(id aspects) {
        NSLog(@"aspects test2 log");
    } error:nil];
}

demo.js代碼也非常簡單,對MyClass-test進(jìn)行替換

require('MyClass')
defineClass('MyClass', {
    test: function() {
//        self.ORIGtest();
        console.log("jspatch log")
    }
});

2.2. 具體測試

2.2.1. JSPatch 先 hook 、Aspects 采用 AspectPositionInstead (替換) hook

那么代碼就是下面這樣,注意把-aspects_hook方法設(shè)置為AspectPositionInstead

// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self jp_hook];
    [self aspects_hook];
    MyClass *a = [[MyClass alloc] init];
    [a test];
}

執(zhí)行結(jié)果:

JPAndAspects[2092:1554779] aspects log

結(jié)果是 Aspects 正確替換了方法

2.2.2. Aspects 先采用隨便一種Position hook,JSPatch再hook

那么代碼就是下面這樣

- (void)viewDidLoad {
    [super viewDidLoad];
    [self aspects_hook];
    [self jp_hook];
    MyClass *a = [[MyClass alloc] init];
    [a test];
}

執(zhí)行結(jié)果:

JPAndAspects[2774:1565702] JSPatch.log: jspatch log

結(jié)果是 JSPatch 正確替換了方法

Why?

前面說到,hook 會替換該方法和 -forwardInvocation:,我們先看看方法被 hook 前后的變化

原方法對應(yīng)關(guān)系

方法替換后原方法指向了_objc_msgForward,同時(shí)添加一個(gè)方法PREFIXtest(JSPatchORIGtest,Aspectsaspects_test)指向了原來的實(shí)現(xiàn)。JSPatch新增了一個(gè)方法指向IMP(NEWtest),Aspects則保存block為關(guān)聯(lián)屬性
`-test`變化

-forwardInvocation: 的變化也相似,原來的-forwardInvocation: 沒實(shí)現(xiàn)是這樣的
`-forwardInvocation:`變化

如果原來的-forwardInvocation:有實(shí)現(xiàn),就新加一個(gè)-ORIGforwardInvocation:指向原IMP(forwardInvocation:)
`-forwardInvocation:`變化

由于-test方法指向了_objc_msgForward,這時(shí)調(diào)用-test方法就會進(jìn)入消息轉(zhuǎn)發(fā),消息轉(zhuǎn)發(fā)的第三步進(jìn)入-forwardInvocation:執(zhí)行新的IMP(NEWforwardInvocation),拿到invocation,invocation.selector拼上前綴,然后拼上其他信息直接invoke,最終執(zhí)行IMP(NEWtest)(Aspects是執(zhí)行替換的block)。


以上是只有一次hook的情況,我們看看兩者都hook的變化

JSPatch先hook,`-test`變化

JSPatch先hook,`-forwardInvocation:`變化

這時(shí)調(diào)用-test同樣發(fā)生消息轉(zhuǎn)發(fā),進(jìn)入-forwardInvocation:執(zhí)行AspectsIMP(AspectsforwardInvocation),上文提到Aspects把替換的block保存為關(guān)聯(lián)屬性了,到了-forwardInvocation:直接拿出來執(zhí)行,和原來的實(shí)現(xiàn)沒有任何關(guān)系,所以有了2.2.1 正確的結(jié)果。


Aspects先hook,`-test`變化

Aspects先hook,`-forwardInvocation:`變化

這時(shí)調(diào)用-test同樣發(fā)生消息轉(zhuǎn)發(fā),進(jìn)入-forwardInvocation:執(zhí)行JSPatchIMP(JSPatchforwardInvocation),執(zhí)行_JPtest,和原來的實(shí)現(xiàn)
沒有任何關(guān)系,所以有了2.2.2 正確的結(jié)果。
看到這里,如果細(xì)心的話會發(fā)現(xiàn)ORIGtest指向了_objc_msgForward,如果我們在JSPatch代碼里調(diào)用self.ORIGtest()會怎么樣呢?

2.2.3. Aspects 先采用隨便一種Position hook,JSPatch再hook,JSPatch代碼里調(diào)用self.ORIGtest()

代碼是下面這樣的

// demo.js
require('MyClass')
defineClass('MyClass', {
    test: function() {
        self.ORIGtest();
        console.log("jspatch log")
    }
});
// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self aspects_hook];
    [self jp_hook];
    MyClass *a = [[MyClass alloc] init];
    [a test];
}

執(zhí)行結(jié)果:

JPAndAspects[8668:1705052] -[MyClass ORIGtest]: unrecognized selector sent to instance 0x7ff592421a30
Why?

-test-forwardInvocation:的變化同上一步Aspectshook。
由于-ORIGtest指向了_objc_msgForward,調(diào)用方法時(shí)進(jìn)入-forwardInvocation:執(zhí)行IMP(JSPatchforwardInvocation),JSPatchforwardInvocation中有這樣一段代碼

static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation)
{
...
  JSValue *jsFunc = getJSFunctionInObjectHierachy(slf, JPSelectorName);
  if (!jsFunc) {
      JPExecuteORIGForwardInvocation(slf, selector, invocation);
      return;
  }
...
}

這個(gè)-ORIGtest在對象中找不到具體的實(shí)現(xiàn),因此轉(zhuǎn)發(fā)給了-ORIGINforwardInvocation:。注意:這里直接把-ORIGtest轉(zhuǎn)發(fā)出去了,很顯然IMP(AspectsforwardInvocation)也是處理不了這個(gè)消息的。因此,出現(xiàn)了unrecognized selector異常。
這里是兩者兼容出現(xiàn)的最大問題,如果JSPatch在轉(zhuǎn)發(fā)前判斷一下這個(gè)方法是自己添加的-ORIGxxx,把前綴ORIG去掉再轉(zhuǎn)發(fā),這個(gè)問題就解決了。

2.2.4. JSPatch先hook, Aspects 再采用AspectPositionInstead(替換)hook,JSPatch代碼里調(diào)用self.ORIGtest()

和2.2.1 相同,不管JSPatch hook之后是什么樣的,都只執(zhí)行Aspectsblock

2.2.5. JSPatch先hook, Aspects 再采用AspectPositionBefore(替換)hook

代碼如下,注意把AspectPositionInstead替換為AspectPositionBefore

// demo.js
require('MyClass')
defineClass('MyClass', {
    test: function() {
        console.log("jspatch log")
    }
});
// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self jp_hook];
    [self aspects_hook];
    MyClass *a = [[MyClass alloc] init];
    [a test];
}

執(zhí)行結(jié)果:

JPAndAspects[10943:1756624] aspects log
JPAndAspects[10943:1756624] JSPatch.log: jspatch log

執(zhí)行結(jié)果如期是正確的。
IMP(AspectsforwardInvocation)的部分代碼如下

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

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

    // If no hooks are installed, call original implementation (usually to throw an exception)
    if (!respondsToAlias) {
        invocation.selector = originalSelector;
        SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
        if ([self respondsToSelector:originalForwardInvocationSEL]) {
            ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
        }else {
            [self doesNotRecognizeSelector:invocation.selector];
        }
    }

首先執(zhí)行Before hooks;接著查找是否有Instead hooks,如果有就執(zhí)行,如果沒有就在類繼承鏈中查找父類能否響應(yīng)-aspects_test,如果可以就invoke這個(gè)invocation,否則把respondsToAlias置為NO;接著執(zhí)行After hooks;接著if (!respondsToAlias)把這個(gè)-test轉(zhuǎn)發(fā)給ORIGINforwardInvocationIMP(JSPatchforwardInvocation)處理了這個(gè)消息。注意這里是把-test轉(zhuǎn)發(fā)

2.2.6. JSPatch先hook, Aspects 再采用AspectPositionAfter hook

代碼同2.2.5,注意把AspectPositionBefore替換為AspectPositionAfter

JPAndAspects[11706:1776713] aspects log
JPAndAspects[11706:1776713] JSPatch.log: jspatch log

結(jié)果都輸出了,但是順序不對。
IMP(AspectsforwardInvocation)代碼中不難看出,After hooks先執(zhí)行了,再將這個(gè)消息轉(zhuǎn)發(fā)。這也可以說是Aspects的不足。

2.2.7. Aspects隨便一種Position hook方法-test2,JSPatch再hook -test,JSPatch代碼里調(diào)用self.ORIGtest(), Aspects 以隨便一種Position hook方法-test

同2.2.5和2.2.6很像,不過前面多了對-test2的hook,代碼如下:

// demo.js
require('MyClass')
defineClass('MyClass', {
    test: function() {
        self.ORIGtest();
        console.log("jspatch log")
    }
});
// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self aspects_hook_test2];
    [self jp_hook];
    [self aspects_hook];
    MyClass *a = [[MyClass alloc] init];
    [a test];
}

代碼執(zhí)行結(jié)果:

JPAndAspects[12597:1797663] MyClass origin log
JPAndAspects[12597:1797663] JSPatch.log: jspatch log

結(jié)果是Aspects對-test的hook沒有生效。

Why?

不廢話,直接看Aspects代碼:

static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
    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);
    }

    // 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);
    return subclass;
}

這段代碼的作用是區(qū)分self的類型,進(jìn)行不同的swizzleForwardInvocation。self本身可能是一個(gè)Class;或者self通過-class方法返回的self真正的Class不同,最典型的KVO,會創(chuàng)建一個(gè)子類加上NSKVONotify_前綴,然后重寫class方法,看不懂的可以參考Objective-C 對象模型。這兩種情況都對self真正的Class進(jìn)行aspect_swizzleClassInPlace;如果self是一個(gè)普通對象,則模仿KVO的實(shí)現(xiàn)方式,創(chuàng)建一個(gè)子類,swizzle子類的-forwardInvocation:,通過object_setClass強(qiáng)行設(shè)置Class。


再看aspect_swizzleClassInPlace

static Class aspect_swizzleClassInPlace(Class klass) {
    ...
        if (![swizzledClasses containsObject:className]) {
            aspect_swizzleForwardInvocation(klass);
            [swizzledClasses addObject:className];
        }
    ...
}

問題就出在這個(gè)aspect_swizzleClassInPlace,它會判斷如果這個(gè)類的-forwardInvocation: swizzle過,就什么都不做,但是通過數(shù)組這種方式是會出問題,第二次hook的時(shí)候就不會-forwardInvocation:替換成IMP(AspectsforwardInvocation),所以第二次hook不生效。相比,JSPatch的實(shí)現(xiàn)就比較合理,判斷兩個(gè)IMP是否相等。

if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {

}
2.2.8. Aspects 先采用隨便一種Position hook父類,JSPatch再hook子類,JSPatch代碼里調(diào)用self.super().xxx()

代碼是下面這樣的

// demo.js
require('MySubClass')
defineClass('MySubClass', {
    test: function() {
        self.super().test();
        console.log("jspatch log")
    }
});
// ViewController.m

// 增加一個(gè)子類
@interface MySubClass : MyClass
@end

@implementation MySubClass
- (void)test {
    NSLog(@"MySubClass origin log");
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    [self aspects_hook];
    [self jp_hook];
    MySubClass *a = [[MySubClass alloc] init];
    [a test];
}

執(zhí)行結(jié)果:

JPAndAspects[89642:1600226] -[MySubClass SUPER_test]: unrecognized selector sent to instance 0x7fa4cadabc70
Why?

父類MyClass-test-forwardInvocation:的變化同2.2.1中原-forwardInvocation沒有實(shí)現(xiàn)的情況。
JSPatchsuper的實(shí)現(xiàn)是新增加一個(gè)方法-SUPER_test,IMP指向了父類的IMP,由于-test指向了_objc_msgForward,調(diào)用方法時(shí)進(jìn)入-forwardInvocation:執(zhí)行IMP(JSPatchforwardInvocation),執(zhí)行self.super().test()時(shí),實(shí)際執(zhí)行了-SUPER_test,這個(gè)-SUPER_test在對象中找不到具體的實(shí)現(xiàn),發(fā)生了-ORIGtest一樣的異常。
這里是兩者兼容出現(xiàn)的第二個(gè)比較嚴(yán)重的問題。

2.3 總結(jié)

寫到這里,除了Aspects對對象的hook(這種情況很少見,你可以自己測試),可能已經(jīng)解答了兩者兼容的大部分問題。通過以上分析,得出不兼容的四種情況:

  • Aspectshook某一方法,JSPatchhook同一方法且JSPatch調(diào)用了self.ORIGxxx(),結(jié)果是異常崩潰。
  • Aspectshook父類某一方法,JSPatchhook子類同一方法且JSPatch調(diào)用了self.super().xxx(),結(jié)果是異常崩潰。
  • JSPatchhook某一方法,AspectsAfter的方式hook同一方法,結(jié)果是執(zhí)行順序不對
  • Aspectshook任何方法,JSPatchhook另一方法,AspectshookJSPatch相同的方法,結(jié)果是最后一次hook不生效

2.4 建議

以我們的觀點(diǎn),JSPatch優(yōu)先級更高,使用JSPatch一定是出現(xiàn)了比較嚴(yán)重的線上bug(敲黑板,線上)需要修復(fù)。而AOP是在開發(fā)階段,開發(fā)階段所有東西都是可控的,AOP完全可以通過method_swizzling搞定,開發(fā)階段麻煩一點(diǎn)是可以接受的。我們最近已經(jīng)把Aspect拿掉了。另一方面,開發(fā)App難免要接入各種各樣的SDK,有些不是那么良心的SDK是閉源的,它在背后偷偷干了什么都不知道,對于這樣的SDK,將來App有什么差錯(cuò),你們是要負(fù)責(zé)任滴。

3. 寫在最后

簡書作為一個(gè)優(yōu)質(zhì)原創(chuàng)內(nèi)容社區(qū),擁有大量優(yōu)質(zhì)原創(chuàng)內(nèi)容,提供了極佳的閱讀和書寫體驗(yàn),吸引了大量文字愛好者和程序員。簡書技術(shù)團(tuán)隊(duì)在這里分享技術(shù)心得體會,是希望拋磚引玉,吸引更多的程序員大神來簡書記錄、分享、交流自己的心得體會。這個(gè)專題以后會不定期更新簡書技術(shù)團(tuán)隊(duì)的文章,包括Android、iOS、前端、后端等等,歡迎大家關(guān)注。

參考

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

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

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