1. 背景
Aspects 和 JSPatch 是 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 前后的變化

方法替換后原方法指向了
_objc_msgForward,同時(shí)添加一個(gè)方法PREFIXtest(JSPatch 是ORIGtest,Aspects 是aspects_test)指向了原來的實(shí)現(xiàn)。JSPatch新增了一個(gè)方法指向IMP(NEWtest),Aspects則保存block為關(guān)聯(lián)屬性
-forwardInvocation: 的變化也相似,原來的-forwardInvocation: 沒實(shí)現(xiàn)是這樣的
如果原來的
-forwardInvocation:有實(shí)現(xiàn),就新加一個(gè)-ORIGforwardInvocation:指向原IMP(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的變化


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


這時(shí)調(diào)用
-test同樣發(fā)生消息轉(zhuǎn)發(fā),進(jìn)入-forwardInvocation:執(zhí)行JSPatch的IMP(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:的變化同上一步Aspects先hook。
由于-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í)行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];
}
執(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ā)給ORIGINforwardInvocation即IMP(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)的情況。
JSPatch中super的實(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)解答了兩者兼容的大部分問題。通過以上分析,得出不兼容的四種情況:
-
Aspects先hook某一方法,JSPatch再hook同一方法且JSPatch調(diào)用了self.ORIGxxx(),結(jié)果是異常崩潰。 -
Aspects先hook父類某一方法,JSPatch再hook子類同一方法且JSPatch調(diào)用了self.super().xxx(),結(jié)果是異常崩潰。 -
JSPatch先hook某一方法,Aspects以After的方式hook同一方法,結(jié)果是執(zhí)行順序不對 -
Aspects先hook任何方法,JSPatch再hook另一方法,Aspects再hook和JSPatch相同的方法,結(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)注。
參考