背景
在我們實(shí)際開(kāi)發(fā)的ios項(xiàng)目中,使用了Jspatch和Aspect兩個(gè)第三方庫(kù)
先說(shuō)下這兩個(gè)是什么東東,能用來(lái)做什么,我們項(xiàng)目為何要引入這兩個(gè)庫(kù)
Aspect 源于切面編程的概念,可以在一個(gè)類的seletor的前后添加代碼,或替換一個(gè)selector的實(shí)現(xiàn)。應(yīng)用場(chǎng)景主要是處理UI表現(xiàn),修改系統(tǒng)控件滿足我們的需求。簡(jiǎn)易教程可參看aspect doc
Jspatch 在后期添加,appstore發(fā)布周期長(zhǎng),有線上問(wèn)題需要即時(shí)修復(fù),等不及發(fā)版本,需要做hotfix。到目前為止也幫助我們解決了很多線上問(wèn)題,貢獻(xiàn)很大
之前兩者從未遇到過(guò)沖突問(wèn)題,但在有一次用Jspatch在做hotfix的時(shí)候,修改了UIWebview的一個(gè)selector A,可偏偏Aspect也修改了UIWebview的一個(gè)selector B, 就是兩者修改了同一個(gè)類的不同方法!
代碼運(yùn)行起來(lái)
必掛在aspect的__ASPECTS_ARE_BEING_CALLED__
方法里,具體位置在
但當(dāng)時(shí)我們?nèi)孕枰鰄otfix,只能繞路了,不能hook UIWebview了!找其他方法。
為了找尋其原因,我們來(lái)脫離項(xiàng)目,做個(gè)demo,來(lái)完整看看Jspatch和Aspect的兼容問(wèn)題。
Demo
我們做的這個(gè)Demo分以下幾個(gè)步驟,正好不熟悉Jspatch的同學(xué)和Aspect的同學(xué)也可以熟悉一下。
- Jspatch修改Viewcontroller的viewWillAppear的方法,在方法里修改背景色為
黑色
- 屏蔽Jspatch,Aspect單獨(dú)修改的viewWillAppear的方法,在方法里修改背景色為
黃色
- Jspatch和Aspect,都替換viewWillAppear的方法,顏色仍修改為各自顏色
- Jspatch和Aspect,替換不同方法
- Jspatch替換類的實(shí)例方法,Aspect替換實(shí)例對(duì)象方法
步驟一(Jspatch修改背景色)
首先我們創(chuàng)建一個(gè)新的工程,xcode -> file -> new -> project -> Signal view。工程目錄下創(chuàng)建Podfile。
Podfile內(nèi)容如下, 將jspatch和aspect兩個(gè)庫(kù)引入進(jìn)來(lái)
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'JADemo' do
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
# Pods for JADemo
pod 'JSPatch'
pod 'Aspects'
end
本地添加一個(gè)
demo.js
的文件,js代碼替換viewWillAppear
, 設(shè)置背景色為黑色
require('UIColor')
defineClass('ViewController', {
viewWillAppear: function(animated) {
self.super().viewWillAppear(animated);
self.view().setBackgroundColor(UIColor.blackColor());
},
});
接下來(lái)Appdelegate
里加入
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[JPEngine startEngine];
NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
if (sourcePath) {
NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];
}
return YES;
}
運(yùn)行...
修改完成!
步驟二(Aspects修改背景色)
屏蔽Jspatch,將demo.js 重命名為demo1.js
加入aspects代碼
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[ViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, BOOL animated) {
[super viewWillAppear:animated];
self.view.backgroundColor = [UIColor yellowColor];
} error:nil];
}
步驟三(Jspatch和Aspects同時(shí)修改)
現(xiàn)在將demo1.js名字改回demo.js, 恢復(fù)Jspatch修改功效。
運(yùn)行...
結(jié)果是Aspects修改生效了
步驟四(Jspatch和Aspects同時(shí)修改不同方法)
我們這時(shí)要有兩種不同的表現(xiàn),那用Jspatch來(lái)修改背景色,Aspects來(lái)彈一個(gè)alert。看看是否都生效了吧。Aspects的修改方法,改為另一個(gè)viewDidAppear
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[ViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, BOOL animated) {
[super viewDidAppear:animated];
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"alert" message:@"content" preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:alertController animated:true completion:nil];
} error:nil];
}
運(yùn)行...
哎呀,沒(méi)有沖突啊,都生效了,是不是搞錯(cuò)了。注意,我們用Aspects替換的是類的實(shí)例方法,也就是ViewController創(chuàng)建出來(lái)對(duì)象,都會(huì)彈一個(gè)alert出來(lái)。而我們項(xiàng)目中遇到情況是用Aspects修改了具體實(shí)例。所以進(jìn)入最后一個(gè)步驟。
步驟五(Jspatch和Aspects同時(shí)修改不同方法, Aspects修改具體實(shí)例)
修改Aspects代碼, self
代替ViewController
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, BOOL animated) {
[super viewDidAppear:animated];
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"alert" message:@"content" preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:alertController animated:true completion:nil];
} error:nil];
}
運(yùn)行...
如果你加了exception斷點(diǎn)的話,就會(huì)重現(xiàn)項(xiàng)目中情況了。
Demo總結(jié)
這里先做個(gè)小總結(jié), Jspatch 和 Aspects 實(shí)際沖突的表現(xiàn)
- Jspatch 與 Aspects 在修改同一個(gè)類的同一方法時(shí), 是可行的,但只有一個(gè)生效,取決于先后順序
步驟三演示中,我只驗(yàn)證了Jspatch在前的情況,有興趣的同學(xué)可以試驗(yàn)一下反過(guò)來(lái)的情況
Jspatch 與 Aspects 在修改同一個(gè)類的不同方法時(shí),如果Aspects修改的是一個(gè)類的實(shí)例方法,而非具體實(shí)例,兩者可以同時(shí)生效
Jspatch 與 Aspects 在修改同一個(gè)類的不同方法時(shí),如果Aspects修改的是一個(gè)具體類的實(shí)例, Aspects assert,結(jié)果是Aspects生效
因此針對(duì)步驟五的情況是需要我們注意的。
現(xiàn)象研究
由表及里,先研究下我們最關(guān)心的步驟五。
forwardInvocation
沖突的原由是Jspatch和Aspects都使用了forwardInvocation,來(lái)重寫消息轉(zhuǎn)發(fā)。
這里做一下runtime的復(fù)習(xí)
我們知道對(duì)oc對(duì)象的方法調(diào)用,通過(guò)發(fā)送消息的方式,底層會(huì)調(diào)用void objc_msgSend(id self, SEL cmd, ...)
, 去對(duì)象的類中方法列表
中搜索,如果沒(méi)找到就去父類的方法列表
中去尋找,如果找不到就走消息轉(zhuǎn)發(fā)
流程
對(duì)象在收到未定義的方法的時(shí)候,會(huì)首先走 +(BOOL)resolveInstanceMethod:(SEL)selector
, 這里給你的機(jī)會(huì)去動(dòng)態(tài)添加一個(gè)方法
如果這里未處理,走到-(id)forwardingTargetForSelector:(SEL)selector
, 返回一個(gè)能處理此消息的對(duì)象
如果仍未處理走到最終的-(void)forwardInvocation:(NSInvocation*)invocation
, forwardInvocation叫做完整的消息轉(zhuǎn)發(fā),之所以叫完整消息轉(zhuǎn)發(fā), 它包含一次調(diào)用的所有信息,調(diào)用對(duì)象,調(diào)用方法名,實(shí)現(xiàn),參數(shù),返回值。
Jspatch 和 Aspects 為了自定義一個(gè)方法調(diào)用,直接將方法調(diào)用,轉(zhuǎn)發(fā)給forwardInvocation,在forwardInvocation中做一些復(fù)雜的工作。
如果你override一個(gè)forwardInvocation方法在一個(gè)類中,你發(fā)現(xiàn)你的方法并不會(huì)進(jìn)入,這是因?yàn)檫@個(gè)方法在類中可以找到,不出觸發(fā)消息轉(zhuǎn)發(fā)機(jī)制,上面所講。那如何強(qiáng)制進(jìn)入forwardInvocation呢
Jspatch 和 Aspects 都會(huì)找到這樣一個(gè)關(guān)鍵字,_objc_msgForward
,在Demo中搜索一下
拿Jspatch舉例,Jspatch會(huì)最終對(duì)類的某個(gè)selector調(diào)用class_replaceMethodclass_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
,Aspects也同樣有此操作。
這樣這個(gè)在調(diào)用這個(gè)類對(duì)象的selector,就直接進(jìn)入forwardInvocation了。用圖表示一下,工作原理
Jspatch 和 Aspects 為了保留原來(lái)的實(shí)現(xiàn),添加了自己forwardInvocation,并replace了原來(lái)的forwardInvocation,保留了原版,可恢復(fù)。
現(xiàn)象分析之步驟三
現(xiàn)在可以思考下,步驟三,當(dāng)替換了同一個(gè)方法時(shí),方法首先被Jspatch強(qiáng)制轉(zhuǎn)發(fā)給forwardInvocation,并替換了forwaInvocation的實(shí)現(xiàn),然后后來(lái)Aspects,也替換了forwardInvocation的實(shí)現(xiàn),就是把jspatch的實(shí)現(xiàn)給替換走了。
因此當(dāng)替換同一個(gè)方法時(shí),與順序有關(guān)。
現(xiàn)象分析之步驟四
回顧步驟四,替換不同方法(Jspatch替換了viewWillAppear,Aspects替換viewDidAppear),Aspects替換類的實(shí)例方法。
到這里可能有疑惑,如果按照現(xiàn)象分析之步驟三的圖
, Jspatch的實(shí)現(xiàn)被Aspects替換,那么Jspatch應(yīng)該沒(méi)有啟動(dòng)作用才對(duì),怎么會(huì)都奏效呢。
這就要進(jìn)入Aspects的forwardInvocation看看它是怎么處理的。Aspects重寫的forwardInvocation的函數(shù)是__ASPECTS_ARE_BEING_CALLED__
。
關(guān)注這個(gè)函數(shù)的最下面的代碼
斷點(diǎn)斷住的地方,就是去執(zhí)行Jspatch的地方。respondsToAlias代表,Aspects hook了這個(gè)selector(Aspects內(nèi)部會(huì)記錄下自己hook的所有selector)。這部分代碼意思是,當(dāng)Aspects沒(méi)hook這個(gè)selector的時(shí)候,就調(diào)用原來(lái)的forwardInvocation實(shí)現(xiàn)。大家注意我用紅圈圈起來(lái)的地方,就知道進(jìn)來(lái)的方法就是Jspatch hook的viewWillAppear。
那么originalForwardInvocationSEL
, 肯定保留了Jspatch的實(shí)現(xiàn),找一下源頭。搜索AspectsForwardInvocationSelectorName
就可以找到了。
很明顯,Aspects在替換原來(lái)forwardInvocation的時(shí)候,把原來(lái)的實(shí)現(xiàn)保存在AspectsForwardInvocationSelectorName
,這是一個(gè)新增方法。用圖表示下
現(xiàn)象分析之步驟五
看樣子,Aspects處理還是很完美,但是往往是看上去完美的地方,還是會(huì)出現(xiàn)問(wèn)題,該來(lái)的終究回來(lái)。
最后來(lái)解釋那個(gè)assert。步驟五與步驟四不同的地方在于,Aspects替換的是具體對(duì)象的方法。
經(jīng)過(guò)調(diào)試,發(fā)現(xiàn)在獲取的Jspatch原有實(shí)現(xiàn)是空! 注意紅框里的。AspectsForwardInvocationSelectorName
也不會(huì)添加成功。
問(wèn)題出在class_replaceMethod
這個(gè)方法上
文檔上表明,這個(gè)函數(shù)的返回值是,一個(gè)類定義了的方法的實(shí)現(xiàn),它不會(huì)去找super class。然而這個(gè)class_replaceMethod
的第一個(gè)參數(shù)klass
是Aspects動(dòng)態(tài)創(chuàng)建的一個(gè)Viewcontroller的子類(為何動(dòng)態(tài)創(chuàng)建子類也是為方便的回復(fù)原狀)。你控制臺(tái)上po klass,會(huì)輸出ViewController_Aspects_
。然而定義forwardInvocation
的類是Viewcontroller( Jspatch有重新實(shí)現(xiàn)過(guò)Viewcontroller的forwardInvocation
)
回到那個(gè)assert
由于origin forwardInvoca 為nil,AspectsForwardInvocationSelectorName這方法也就不復(fù)存在了。因此走到了那個(gè)assert。
解決方案
到目前為止,原因已經(jīng)全部查明,接下來(lái)看看有什么辦法來(lái)解決步驟五遇到的問(wèn)題,也就是我們項(xiàng)目中遇到的問(wèn)題。
我們首先任務(wù)就是解決,獲取的Jspatch原有實(shí)現(xiàn)為nil的問(wèn)題,既然class_replaceMethod
不能夠上訴super class去獲取實(shí)現(xiàn),我們使用另一個(gè)獲取實(shí)現(xiàn)的方法class_getInstanceMethod
, class_getInstanceMethod
文檔描述可以獲取父類實(shí)現(xiàn)。
修改代碼
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
NSCParameterAssert(klass);
- // If there is no method, replace will act like class_addMethod.
- IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
- if (originalImplementation) {
- class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
+ // get origin forwardInvocation impl, include superClass impl,not NSObject impl, and class method to kClass
+ Method originalMethod = class_getInstanceMethod(klass, @selector(forwardInvocation:));
+ if (originalMethod != class_getInstanceMethod([NSObject class], @selector(forwardInvocation:))) {
+ IMP originalImplementation = method_getImplementation(originalMethod);
+ if (originalImplementation) {
+ // If there is no method, replace will act like class_addMethod.
+ class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
+ }
}
+ class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
獲取原有實(shí)現(xiàn),排除[NSObject forwardInvocation], 因?yàn)樗阉鞲割悾瑫?huì)搜到[NSObject forwardInvocation],這個(gè)我們不需要addMethod,我們只關(guān)心,我們重寫的。
這樣Jspatch的實(shí)現(xiàn)就會(huì)找到,看圖中紅框,很明顯名字看出來(lái)那就是Jspatch的實(shí)現(xiàn),我們找到了
不過(guò)當(dāng)再次運(yùn)行的時(shí)候,assert還是會(huì)跳到,警報(bào)還沒(méi)有完全解除。
respondsToSelector仍不能認(rèn)識(shí)AspectsForwardInvocationSelectorName
這個(gè)方法。
查文檔發(fā)現(xiàn)
respondsToSelector實(shí)質(zhì)會(huì)去調(diào)用class獲取類是Viewcontroller(因此子類的class方法被Aspects重寫為基類的,為了動(dòng)態(tài)創(chuàng)建子類對(duì)用戶是透明的),然后查看這個(gè)類的方法列表。然而由于Aspects的動(dòng)態(tài)創(chuàng)建子類,AspectsForwardInvocationSelectorName
添加到了Aspects的動(dòng)態(tài)創(chuàng)建的子類ViewController_Aspects_
上!難怪找不到了!
我們?nèi)匀皇褂?code>class_getInstanceMethod
if (!respondsToAlias) {
invocation.selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
- if ([self respondsToSelector:originalForwardInvocationSEL]) {
- ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
- }else {
+ Method method = class_getInstanceMethod(object_getClass(self), originalForwardInvocationSEL);
+ if (method) {
+ typedef void (*FuncType)(id, SEL, NSInvocation *);
+ FuncType imp = (FuncType)method_getImplementation(method);
+ imp(self, selector, invocation);
+ } else {
[self doesNotRecognizeSelector:invocation.selector];
}
運(yùn)行...
沖突解決了!
總結(jié)
Jspatch和Aspects沖突問(wèn)題和解決方案調(diào)查完畢,如果你有遇到和我們一樣的問(wèn)題,可以考慮我的修復(fù)方案。Jspatch和Aspects都是hack的方案,大家在開(kāi)發(fā)過(guò)程中,還是盡量少用,我們目前項(xiàng)目中hack了很多系統(tǒng)控件,完成需求,這使得系統(tǒng)升級(jí)的時(shí)候,我們變得非常被動(dòng),因?yàn)槲覀兊膆ack不知道會(huì)出現(xiàn)什么問(wèn)題。
注意
我還嘗試過(guò)在jspatch的js里,寫aspects修改方法,這樣是不起作用的,jspatch會(huì)自己修改block參數(shù)個(gè)數(shù),Aspects發(fā)現(xiàn)參數(shù)不一致, 是不會(huì)執(zhí)行的,因此不要試圖這樣做