記一次RN Debug經(jīng)歷

bug是如果一個scrollview上有多個TextInput,那么一個TextInput處于focus狀態(tài)時點(diǎn)擊其它TextInput只會關(guān)閉鍵盤,沒有將另一個TextInput進(jìn)行focus,其實(shí)如果我之前了解React中事件傳遞順序的話并沒有必要這么麻煩,雖然花了很多時間,但是也許學(xué)習(xí)到了很多。

項(xiàng)目源碼: Reminders

首先iOS程序員的直覺之檢查canBecomeFirstResponder,打斷點(diǎn)

canBecomeFirstResponder檢查

發(fā)現(xiàn)正常情況該方法會被調(diào)用兩次(false, true),然而異常情況只會被調(diào)用一次(false)

正常情況第二次調(diào)用的調(diào)用棧

textField canBecomeFirstResponder調(diào)用棧

圖中的foucs方法暴露給了js, 猜測是由js調(diào)用的
native端 focus方法

搜索focus

focus( 在整個項(xiàng)目中的搜索結(jié)果


結(jié)果太多。。。猜肯定和TextInput有關(guān)

focus( 在整個TextInput中的搜索結(jié)果

TextField在_onPress里調(diào)用了focus (繼續(xù)找下去可以看到JS端最終調(diào)用了UIManager.focus(), 對應(yīng)上面native端focus方法)

接著搜索_onPress,看它是在哪里被調(diào)用的,可以看到這是個callback


_onPress搜索

然后在_onPress里設(shè)個斷點(diǎn),發(fā)現(xiàn)異常情況并不會調(diào)用_onPress
正常時調(diào)用_onPress的調(diào)用棧(講真我第一眼看到這個棧想掀桌子。。):

_onPress調(diào)用棧

從上到下分析:
touchableHandlePress只是簡單轉(zhuǎn)發(fā):


touchableHandlePress


在第三個_performsideEffectsForTransition打斷點(diǎn),發(fā)現(xiàn)無論怎樣都會被執(zhí)行多次。。慢慢分析會比較復(fù)雜。先嘗試換思路,我們先確定是事件沒有發(fā)出還是傳輸時丟失了, 我們需要先找到Js端event的源頭然后推出Native發(fā)送event的位置

根據(jù)TextInput的render函數(shù)實(shí)現(xiàn)可知onPress信號由TouchableWithoutFeedback接受:

TextInput::render()

由TouchableWithoutFeedback的實(shí)現(xiàn)可知TouchableWithoutFeedback只是將child的clone加上了一大堆方法處理的屬性然后直接返回child的clone


TouchableWithoutFeedback::render()


所以Js端event的接受者是child 即 {textContainer} ,對應(yīng)的是native端的RCTTextField:

現(xiàn)在在原生找RCTTextField(一開始那個類)
原生檢測touch事件無非兩種方法。要么實(shí)現(xiàn)UIResponder的方法,要么加GestureRecognizer,
RCTTextField的實(shí)現(xiàn)里沒有UIResponder的方法,所以確定是GestureRecognizer。
要添加GestureRecognizer必須要有RCTTextField的示例,所以必然會有RCTTextField的引用,搜索一下。


RCTTextField搜索結(jié)果

從圖看來一定是在RCTTextField自身或者RCTTextFieldManager里添加的GestureRecognizer了!一定是這樣沒錯!

看了下。。。。媽的沒有。。。。
想了想,還有一種可能:響應(yīng)的是parent view而不是自身。那么最有可能的就是RootView了。
看了下,真的有?。?em>現(xiàn)在想想這樣做最有道理,首先性能上肯定占優(yōu)勢,其次如果子view和parentView都有g(shù)estureRecoginzer不做處理的話同時都會響應(yīng),就麻煩了)

RCTRootView初始化方法

在處理touch的方法handleGestureUpdate:里打個斷點(diǎn)
看來是正常發(fā)出了。。。
看看JS調(diào)用棧有個
調(diào)用棧

設(shè)個斷點(diǎn)

在receiveTouches里設(shè)斷點(diǎn)

正常: topTouchStart,topTouchEnd,topFocus

異常:topTouchStart, topTouchEnd, topEndEditing,topBlur
(正常異常情況下topTouchStart和topTouchEnd的rootNodeID都相同)

我們都知道focus是touch的結(jié)果,所以推測是topTouchStart、topTouchEnd的后續(xù)異常處理導(dǎo)致的bug

繼續(xù)看調(diào)用棧:_receiveRootNodeIDEvent只做了簡單的轉(zhuǎn)發(fā)

_receiveRootNodeIDEvent

在handleTopLevel里打個斷點(diǎn):
handleTopLevel

發(fā)現(xiàn)正常異常情況在傳入topTouchEnd時傳給runEventQueueInBatch的參數(shù)不同
異常:event[1]. _dispatchListeners. __reactBoundMethod = function scrollResponderHandleResponderRelease(e)
正常:event[1]. _dispatchListeners. __reactBoundMethod = function touchableHandleResponderRelease(e)
我們有理由相信就是因?yàn)閠ouchableHandleResponderRelease沒被調(diào)用導(dǎo)致的bug

現(xiàn)在可以確定bug在EventPluginHub.extraceEvents里

來看下實(shí)現(xiàn):

EventPluginHub.extraceEvents

關(guān)于plugin是啥,一開始我也不知道。后來去專門看了下初始化的源碼才知道?,F(xiàn)在就當(dāng)未知數(shù)(每個plugin負(fù)責(zé)監(jiān)聽一套事件,現(xiàn)在的RN雖然有兩個默認(rèn)plugin,但是大多數(shù)組件都依賴于原有的React自帶plugin,由native端定義的plugin幾乎沒用上)。
我們知道的:

  1. 正常異常情況下EventPluginRegistry.plugins返回的值都是一個長度為2的數(shù)組
  2. 異常情況下接受“topTouchEnd”時第一個plugin產(chǎn)生的extractedEvents[1]的listener是scrollResponderHandleResponderRelease(e) 而正常情況下是touchableHandleResponderRelease(e)
  3. extractedEvents在for plugin循環(huán)里調(diào)用(一般不會去更改plugin)
    推斷:
    EventPluginRegistry.plugins兩次返回的都是一樣的數(shù)組
    bug在possiblePlugin.extractEvents里
    進(jìn)去看看。。
    possiblePlugin.extractEvents

    extracted的_dispatchListeners在這一行前是null
    執(zhí)行完這一行變?yōu)楹衒unction scrollResponderHandleResponderRelease(e)的回調(diào)
    根據(jù)accumulate這個參數(shù)名大概知道是把finalEvent的內(nèi)容放進(jìn)extracted里了
    看下finalEvent的內(nèi)容驗(yàn)證一下
    finalEvent

    finalEvent在一開始初始化
    var finalEvent = ResponderSyntheticEvent.getPooled(finalTouch, responderID, nativeEvent, nativeEventTarget);
    后大概就是這樣子沒變過
    然后我們看一下用來初始化finalEvent的ResponderSyntheticEvent.getPooled的參數(shù)
    正常:{
    finalTouch: "onResponderRelease"
    responderID: ".r[1]{TOP_LEVEL}[0].$1.0.1.0.$scene_0.0.1.$1.1.0.1:$r_s1_1.1"
    nativeEvent: …
    nativeEventTarget: …
    }
    異常:{
    finalTouch: "onResponderRelease"
    responderID: ".r[1]{TOP_LEVEL}[0].$1.0.1.0.$scene_0.0.1.$1.1"
    nativeEvent: …
    nativeEventTarget: …
    }

可以看到responderID不同。。自然respond的方法也不同
于是我們watch responderID,找到它是在什么時候開始不同的
發(fā)現(xiàn)responderID從ResponderSyntheticEvent.getPooled一開始就是不同的
想想既然touchEnd觸發(fā)事件,那么touchStart理論上就沒意義了,個人能想到唯一有可能的功能就是確定responderId
于是在touchStart時打個斷點(diǎn)

在touchStart時打個斷點(diǎn)

這行結(jié)束前后console輸出一下possiblePlugin.getResponderID()
possiblePlugin.getResponderID()

假設(shè)正確

所以問題其實(shí)是出在接收touchStart時調(diào)用的extractEvents方法
在該方法里watch responderId:
發(fā)現(xiàn)responderId在
var extracted = canTriggerTransfer(topLevelType, topLevelTargetID, nativeEvent) ? setResponderAndExtractTransfer(topLevelType, topLevelTargetID, nativeEvent, nativeEventTarget) : null;
里被改變。。。。
從名字里也能看出來是setResponderAndExtractTransfer干的。。。進(jìn)去看看

setResponderAndExtractTransfer

在這changeResponder方法里repsonderId被更改
更改結(jié)果為wantsResponderID
所以是wantsResponderID出錯了
該執(zhí)行路徑下(異常時執(zhí)行路徑)wantsResponderID只在聲明時賦值了
wantsResponderID聲明

有兩種可能:
executeDispatchsInOrderStopAtTrue出錯
參數(shù)shouldSetEvent不對

進(jìn)executeDispatchsInOrderStopAtTrue看看:

executeDispatchsInOrderStopAtTrue

再進(jìn)executeDispatchesInOrderStopAtTrueImpl
核心代碼如下

executeDispatchesInOrderStopAtTrueImpl

// 省略一些無關(guān)代碼
executeDispatchesInOrderStopAtTrueImpl續(xù)

這段代碼大概就是找到第一個響應(yīng)事件的listener然后返回listener所屬的object的id
該循環(huán)在i=3時跳出
dispatchListeners[3] 里是scrollResponderHandleStartShouldSetResponderCapture
dispatchListeners[4] 里是touchableHandleStartShouldSetResponder
也就是說scrollResponderHandleStartShouldSetResponderCapture ”偷走”了事件
搜索下scrollResponderHandleStartShouldSetResponderCapture, 找到他的實(shí)現(xiàn):
scrollResponderHandleStartShouldSetResponderCapture

注意那行注釋。。。如果鍵盤打開他就會“eat taps”。。。。。。
其實(shí)bug就出在這。。。下面是因?yàn)槔斫忮e誤而白白多走得幾步(React系統(tǒng)中parentView有一個方法專門“偷”子view事件)

下面是之前想錯了多想的幾步,沒有考慮到原生中點(diǎn)擊事件也有可能在hitTest里就被截取了=。=

所以bug出在這?其實(shí)仔細(xì)想想并不是。。。這個只是說了如果鍵盤打開他就會接收事件
在iOS中如果parent view 和 sub view 都能響應(yīng)點(diǎn)擊事件(UIResponder),那么subView會得到事件的優(yōu)先處置權(quán)
所以bug出在parentView的優(yōu)先級高于subView,也就是event._dispatchListeners順序錯誤
回到setResponderAndExtractTransfer方法。發(fā)現(xiàn)異常情況下shouldSetEvent._dispatchListeners在這一行被賦值:

setResponderAndExtractTransfer

進(jìn)去看看:
accumulateTwoPhaseDispatches

再進(jìn)
forEachAccmulated

上三步可以簡化為
accumulateTwoPhaseDispatchesSingle(shouldSetEvent)

EventPluginHub.injection.getInstanceHandle().traverseTwoPhase(event.dispatchMarker,accumulateDirectionalDispatches, event);
大概做的事是從rootView遍歷到標(biāo)記為event.dispatchMarker的view,然后反向遍歷(event.dispatchMarker會被訪問兩次,一次正向,一次反向),每個節(jié)點(diǎn)調(diào)用accumulateDirectionalDispatches,參數(shù)為(節(jié)點(diǎn)id,遍歷方向,event)

accumulateDirectionalDispatches

大概就是問一下每個view:你接不接受這個event,接受的話就把你放到event的候選目標(biāo)里
但是為啥會需要兩個遍歷階段?

capturephase是啥?
查了一下文檔:

Gesture Responder System:http://facebook.github.io/react-native/releases/0.26/docs/gesture-responder-system.html#capture-shouldset-handlers
里面有這么一句話:

However, sometimes a parent will want to make sure that it becomes responder. This can be handled by using the capture phase. Before the responder system bubbles up from the deepest component, it will do a capture phase, firing on*ShouldSetResponderCapture. So if a parent View wants to prevent the child from becoming responder on a touch start, it should have a onStartShouldSetResponderCapture handler which returns true.

。。。

嗯。。。我現(xiàn)在就想抽自己。。。當(dāng)初為啥不好好看文檔。。。不過至少對RN了解深入了很多。。。
其實(shí)這么說來bug原因就是scrollView不應(yīng)該capture event。而是如果子view都不處理才去處理。
打開scrollResponder把

scrollResponderHandleStartShouldSetResponderCapture和scrollResponderHandleStartShouldSetResponder的實(shí)現(xiàn)互換就解決了。

這樣還有個問題,如果打開鍵盤后直接點(diǎn)擊返回鍵盤不消失,好辦,在Touchable的響應(yīng)方法里加一句這個就搞定了
TextInputState.blurTextInput(TextInputState.currentlyFocusedField())
當(dāng)然這樣textInput被點(diǎn)擊時也會觸發(fā)這個事件,但是因?yàn)閕OS如果在一個時間周期內(nèi)收到關(guān)、開鍵盤。他就會忽視前一個信號。所以一切正常啦~

update:
在RN的repo里提了這個問題,有人說可以組合keyboardDismissMode和keyboardShouldPersistent兩個屬性,來解決。但是個人感覺這依舊是個workaround呀。。。因?yàn)槲矣X得通過capture事件來隱藏鍵盤還是不符合正常事件傳遞順序。而且這樣點(diǎn)擊返回按鈕依然不會隱藏鍵盤。然而沒人繼續(xù)回我了。。。大概600多個issue確實(shí)太忙了。??磥碛植荒茉诖笮晚?xiàng)目中提交PR了。。。桑心。。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容