全面談談iOS中的Aspects和JSPatch兼容問題
1. 背景
Aspects 和 JSPatch 是 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 介紹幾種測試結果和原因。
2. 測試
2.0. 源碼
這是本文使用的測試代碼 ,你可以clone下來,泡杯咖啡,找個安靜的地方跟著本文一步一步實踐。
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</code></pre>
接著是三個hook方法,分別是對 -test 進行 hook 的 -jp_hook 、 -aspects_hook 和對 -test2 進行 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];
}</code></pre>
demo.js 代碼也非常簡單,對 MyClass 的 -test 進行替換
require('MyClass')
defineClass('MyClass', {
test: function() {
// self.ORIGtest();
console.log("jspatch log")
}
});</code></pre>
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];
}</code></pre>
執行結果:
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];
}</code></pre>
執行結果:
JPAndAspects[2774:1565702] JSPatch.log: jspatch log
結果是 JSPatch 正確替換了方法
Why?
前面說到, hook 會替換該方法和 -forwardInvocation: ,我們先看看方法被 hook 前后的變化

原方法對應關系
方法替換后原方法指向了 _objc_msgForward ,同時添加一個方法 PREFIXtest ( JSPatch 是 ORIGtest , Aspects 是 aspects_test )指向了原來的實現。 JSPatch 新增了一個方法指向 IMP(NEWtest) , Aspects 則保存block為關聯屬性

-test 變化
-forwardInvocation: 的變化也相似,原來的 -forwardInvocation: 沒實現是這樣的
-forwardInvocation: 變化
如果原來的 -forwardInvocation: 有實現,就新加一個 -ORIGforwardInvocation: 指向原 IMP(forwardInvocation:)

-forwardInvocation: 變化
由于 -test 方法指向了 _objc_msgForward ,這時調用 -test 方法就會進入消息轉發,消息轉發的第三步進入 -forwardInvocation: 執行新的 IMP(NEWforwardInvocation) ,拿到 invocation , invocation.selector 拼上前綴,然后拼上其他信息直接invoke,最終執行 IMP(NEWtest) ( Aspects 是執行替換的 block )。
以上是只有一次hook的情況,我們看看兩者都hook的變化

JSPatch先hook, -test 變化

JSPatch先hook, -forwardInvocation: 變化
這時調用 -test 同樣發生消息轉發,進入 -forwardInvocation: 執行 Aspects 的 IMP(AspectsforwardInvocation) ,上文提到 Aspects 把替換的 block 保存為關聯屬性了,到了 -forwardInvocation: 直接拿出來執行,和原來的實現沒有任何關系,所以有了2.2.1 正確的結果。

Aspects先hook, -test 變化

Aspects先hook, -forwardInvocation: 變化
這時調用 -test 同樣發生消息轉發,進入 -forwardInvocation: 執行 JSPatch 的 IMP(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];
}</code></pre>
執行結果:
JPAndAspects[8668:1705052] -[MyClass ORIGtest]: unrecognized selector sent to instance 0x7ff592421a30
Why?
-test 和 -forwardInvocation: 的變化同上一步 Aspects 先 hook 。
由于 -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;
}
...
}</code></pre>
這個 -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 之后是什么樣的,都只執行 Aspects 的 block
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];
}</code></pre>
執行結果:
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];
}
}</code></pre>
首先執行 Before hooks ;接著查找是否有 Instead hooks ,如果有就執行,如果沒有就在類繼承鏈中查找父類能否響應 -aspects_test ,如果可以就invoke這個invocation,否則把 respondsToAlias 置為 NO ;接著執行 After hooks ;接著 if (!respondsToAlias) 把這個 -test 轉發給 ORIGINforwardInvocation 即 IMP(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];
}</code></pre>
代碼執行結果:
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);
}
objectsetClass(self, subclass);
return subclass;
}</code></pre>
這段代碼的作用是區分 self 的類型,進行不同的 swizzleForwardInvocation 。 self 本身可能是一個 Class ;或者self通過 -class 方法返回的self真正的 Class 不同,最典型的 KVO ,會創建一個子類加上 NSKVONotify
前綴,然后重寫class方法,看不懂的可以參考 Objective-C 對象模型 。這兩種情況都對self真正的Class進行 aspect_swizzleClassInPlace ;如果self是一個普通對象,則模仿KVO的實現方式,創建一個子類, swizzle 子類的 -forwardInvocation: ,通過 object_setClass 強行設置 Class 。</p>
再看 aspect_swizzleClassInPlace
static Class aspect_swizzleClassInPlace(Class klass) {
...
if (![swizzledClasses containsObject:className]) {
aspect_swizzleForwardInvocation(klass);
[swizzledClasses addObject:className];
}
...
}</code></pre>
問題就出在這個 aspect_swizzleClassInPlace ,它會判斷如果這個類的 -forwardInvocation: swizzle 過,就什么都不做,但是通過數組這種方式是會出問題,第二次 hook 的時候就不會 -forwardInvocation: 替換成 IMP(AspectsforwardInvocation) ,所以第二次 hook 不生效。相比, JSPatch 的實現就比較合理,判斷兩個IMP是否相等。
if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
}</code></pre>
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];
}</code></pre>
執行結果:
JPAndAspects[89642:1600226] -[MySubClass SUPER_test]: unrecognized selector sent to instance 0x7fa4cadabc70
Why?
父類 MyClass 的 -test 和 -forwardInvocation: 的變化同2.2.1中原 -forwardInvocation 沒有實現的情況。
JSPatch 中 super 的實現是新增加一個方法 -SUPER_test ,IMP指向了父類的IMP,由于 -test 指向了 _objc_msgForward ,調用方法時進入 -forwardInvocation: 執行 IMP(JSPatchforwardInvocation) ,執行 self.super().test() 時,實際執行了 -SUPER_test ,這個 -SUPER_test 在對象中找不到具體的實現,發生了 -ORIGtest 一樣的異常 。 這里是兩者兼容出現的第二個比較嚴重的問題。**
2.3 總結
寫到這里,除了 Aspects 對對象的 hook (這種情況很少見,你可以自己測試),可能已經解答了兩者兼容的大部分問題。通過以上分析,得出不兼容的四種情況:
-
Aspects 先 hook 某一方法, JSPatch 再 hook 同一方法且 JSPatch 調用了 self.ORIGxxx() ,結果是異常崩潰。
-
Aspects 先 hook 父類某一方法, JSPatch 再 hook 子類同一方法且 JSPatch 調用了 self.super().xxx() ,結果是異常崩潰。
-
JSPatch 先 hook 某一方法, Aspects 以 After 的方式 hook 同一方法,結果是執行順序不對
-
Aspects 先 hook 任何方法, JSPatch 再 hook 另一方法, Aspects 再 hook 和 JSPatch 相同的方法,結果是最后一次 hook 不生效
參考
來自:http://www.jianshu.com/p/dc1deaa1b28e