全面談談Aspects和JSPatch兼容問題

1. 背景

AspectsJSPatch 是 iOS 開發中很是常見的兩個庫。Aspects 提供了方便簡單的方法進行面向切片編程(AOP),JSPatch可讓你用 JavaScript 書寫原生 iOS APP 和進行熱修復。關於實現原理能夠參考 面向切面編程之 Aspects 源碼解析及應用JSPatch wiki。簡單地歸納就是將原方法實現替換爲_objc_msgForward(或_objc_msgForward_stret),當執行這個方法是直接進入消息轉發過程,最後到達替換後的-forwardInvocation:,在-forwardInvocation:內執行新的方法,這是二者的共同原理。最近項目開發中須要用 JSPatch 替換方法修復一個 bug ,然而這個方法已經使用 Aspects 進行 hook 過了,那麼二者同時使用會不會有問題呢?關於這個問題,網上介紹比較詳細的是 面向切面編程之 Aspects 源碼解析及應用有關Swizzling的一個問題,深刻研究後發現這兩篇文章講得都不夠全面。本文基於 Aspects 1.4.1 和 JSPatch 1.1 介紹幾種測試結果和緣由。javascript

2. 測試

2.0. 源碼

這是本文使用的測試代碼,你能夠clone下來,泡杯咖啡,找個安靜的地方跟着本文一步一步實踐。php

2.1. 代碼說明

ViewController.m中首先定義一個簡單類MyClass,只有-test-test2方法,方法內打印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

接着是三個hook方法,分別是對-test進行hook-jp_hook-aspects_hook和對-test2進行hook-aspects_hook_test2java

- (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進行替換git

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

2.2. 具體測試

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

那麼代碼就是下面這樣,注意把-aspects_hook方法設置爲AspectPositionInstead程序員

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

執行結果:github

JPAndAspects[2092:1554779] aspects log

結果是 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]; }

執行結果:數組

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

結果是 JSPatch 正確替換了方法

Why?

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


原方法對應關係


方法替換後原方法指向了_objc_msgForward,同時添加一個方法PREFIXtest(JSPatchORIGtestAspectsaspects_test)指向了原來的實現。JSPatch新增了一個方法指向IMP(NEWtest)Aspects則保存block爲關聯屬性


-test變化


-forwardInvocation: 的變化也類似,原來的-forwardInvocation: 沒實現是這樣的


-forwardInvocation:變化


若是原來的-forwardInvocation:有實現,就新加一個-ORIGforwardInvocation:指向原IMP(forwardInvocation:)


-forwardInvocation:變化


因爲-test方法指向了_objc_msgForward,這時調用-test方法就會進入消息轉發,消息轉發的第三步進入-forwardInvocation:執行新的IMP(NEWforwardInvocation),拿到invocationinvocation.selector拼上前綴,而後拼上其餘信息直接invoke,最終執行IMP(NEWtest)(Aspects是執行替換的block)。


以上是隻有一次hook的狀況,咱們看看二者都hook的變化


JSPatch先hook, -test變化

JSPatch先hook, -forwardInvocation:變化


這時調用-test一樣發生消息轉發,進入-forwardInvocation:執行AspectsIMP(AspectsforwardInvocation),上文提到Aspects把替換的block保存爲關聯屬性了,到了-forwardInvocation:直接拿出來執行,和原來的實現沒有任何關係,因此有了2.2.1 正確的結果。



Aspects先hook, -test變化

Aspects先hook, -forwardInvocation:變化


這時調用-test一樣發生消息轉發,進入-forwardInvocation:執行JSPatchIMP(JSPatchforwardInvocation),執行_JPtest,和原來的實現
沒有任何關係,因此有了2.2.2 正確的結果。
看到這裏,若是細心的話會發現ORIGtest指向了_objc_msgForward,若是咱們在JSPatch代碼裏調用self.ORIGtest()會怎麼樣呢?

2.2.3. Aspects 先採用隨便一種Position hook,JSPatch再hook,JSPatch代碼裏調用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]; }

執行結果:

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

-test-forwardInvocation:的變化同上一步Aspectshook
因爲-ORIGtest指向了_objc_msgForward,調用方法時進入-forwardInvocation:執行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; } ... }

這個-ORIGtest在對象中找不到具體的實現,所以轉發給了-ORIGINforwardInvocation:注意:這裏直接把-ORIGtest轉發出去了,很顯然IMP(AspectsforwardInvocation)也是處理不了這個消息的。所以,出現了unrecognized selector異常。
這裏是二者兼容出現的最大問題,若是JSPatch在轉發前判斷一下這個方法是本身添加的-ORIGxxx,把前綴ORIG去掉再轉發,這個問題就解決了。

2.2.4. JSPatch先hook, Aspects 再採用AspectPositionInstead(替換)hook,JSPatch代碼裏調用self.ORIGtest()

和2.2.1 相同,無論JSPatch hook以後是什麼樣的,都只執行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]; }

執行結果:

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

執行結果如期是正確的。
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]; } }

首先執行Before hooks;接着查找是否有Instead hooks,若是有就執行,若是沒有就在類繼承鏈中查找父類可否響應-aspects_test,若是能夠就invoke這個invocation,不然把respondsToAlias置爲NO;接着執行After hooks;接着if (!respondsToAlias)把這個-test轉發給ORIGINforwardInvocationIMP(JSPatchforwardInvocation)處理了這個消息。注意這裏是把-test轉發

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

結果都輸出了,可是順序不對。
IMP(AspectsforwardInvocation)代碼中不難看出,After hooks先執行了,再將這個消息轉發。這也能夠說是Aspects的不足。

2.2.7. Aspects隨便一種Position hook方法-test2,JSPatch再hook -test,JSPatch代碼裏調用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]; }

代碼執行結果:

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

結果是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; }

這段代碼的做用是區分self的類型,進行不一樣的swizzleForwardInvocationself自己多是一個Class;或者self經過-class方法返回的self真正的Class不一樣,最典型的KVO,會建立一個子類加上NSKVONotify_前綴,而後重寫class方法,看不懂的能夠參考Objective-C 對象模型。這兩種狀況都對self真正的Class進行aspect_swizzleClassInPlace;若是self是一個普通對象,則模仿KVO的實現方式,建立一個子類,swizzle子類的-forwardInvocation:,經過object_setClass強行設置Class


再看aspect_swizzleClassInPlace

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

問題就出在這個aspect_swizzleClassInPlace,它會判斷若是這個類的-forwardInvocation: swizzle過,就什麼都不作,可是經過數組這種方式是會出問題,第二次hook的時候就不會-forwardInvocation:替換成IMP(AspectsforwardInvocation),因此第二次hook不生效。相比,JSPatch的實現就比較合理,判斷兩個IMP是否相等。

if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) { }
2.2.8. Aspects 先採用隨便一種Position hook父類,JSPatch再hook子類,JSPatch代碼裏調用self.super().xxx()

代碼是下面這樣的

// demo.js require('MySubClass') defineClass('MySubClass', { test: function() { self.super().test(); console.log("jspatch log") } }); // ViewController.m // 增長一個子類 @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]; }

執行結果:

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

父類MyClass-test-forwardInvocation:的變化同2.2.1中原-forwardInvocation沒有實現的狀況。
JSPatchsuper的實現是新增長一個方法-SUPER_test,IMP指向了父類的IMP,因爲-test指向了_objc_msgForward,調用方法時進入-forwardInvocation:執行IMP(JSPatchforwardInvocation),執行self.super().test()時,實際執行了-SUPER_test,這個-SUPER_test在對象中找不到具體的實現,發生了-ORIGtest同樣的異常。
這裏是二者兼容出現的第二個比較嚴重的問題。

2.3 總結

寫到這裏,除了Aspects對對象的hook(這種狀況不多見,你能夠本身測試),可能已經解答了二者兼容的大部分問題。經過以上分析,得出不兼容的四種狀況:

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

3. 寫在最後

簡書做爲一個優質原創內容社區,擁有大量優質原創內容,提供了極佳的閱讀和書寫體驗,吸引了大量文字愛好者和程序員。簡書技術團隊在這裏分享技術心得體會,是但願拋磚引玉,吸引更多的程序員大神來簡書記錄、分享、交流本身的心得體會。這個專題之後會不按期更新簡書技術團隊的文章,包括Android、iOS、前端、後端等等,歡迎你們關注。

參考



文/zhao0(簡書做者) 原文連接:http://www.jianshu.com/p/dc1deaa1b28e 著做權歸做者全部,轉載請聯繫做者得到受權,並標註「簡書做者」。
相關文章
相關標籤/搜索