ios項(xiàng)目,對(duì)jspatch 和 aspect 兼容問(wèn)題研究

背景

在我們實(shí)際開(kāi)發(fā)的ios項(xiàng)目中,使用了JspatchAspect兩個(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é)也可以熟悉一下。

  1. Jspatch修改Viewcontroller的viewWillAppear的方法,在方法里修改背景色為黑色
  2. 屏蔽Jspatch,Aspect單獨(dú)修改的viewWillAppear的方法,在方法里修改背景色為黃色
  3. Jspatch和Aspect,都替換viewWillAppear的方法,顏色仍修改為各自顏色
  4. Jspatch和Aspect,替換不同方法
  5. 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í)行的,因此不要試圖這樣做

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 176,505評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 63,017評(píng)論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,219評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,438評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,918評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,697評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容