Fix一个随机出现的键盘弹出的issue后的思考(ReactNative)
最近花了近一周fix了一個移動端的bug,是個很有趣的bug,大概是這樣的。這是一個比較長的故事,有興趣的可以一直看。
是一個什么樣的bug
bug的表現是在一款tablet端應用使用很久之后,第一,在輸入框內輸入一些內容后,點擊done/search,第二,然后點擊頁面的一些空白區域,軟鍵盤彈出,并且光標focus在最近輸入過的輸入框內。
此時應用對用戶行為的響應會讓用戶很疑惑和費解。
總結,它有如下幾個特點
如何去修復這個bug
第一步 試圖穩定重現
我們先是試圖去找一個最小的用戶journey去復現這個bug,當時運氣比較好,花了大概半天時間找到了一條最小的重現路徑。
不說業務背景,簡單介紹下應用的頁面邏輯。
我們的應用在登錄之后有一個home頁面,home頁面存在三個tab可以滑動或者點擊切換,
在tab頁面之上還存在一些功能菜單,其中某個功能菜單menuA可以點擊跳到另一個新的帶有一個輸入框的頁面。
頁面大概如下,不是專業ux很丑勿見怪。
我們發現的一條可以快速重現的路徑是
第二步 試圖從代碼部分找到為什么最小場景會出現問題
?找到一個最小重現路徑之后,我們可以從代碼里面找找為什么會出現這個問題。
因為這個bug在應用重啟后沒有,我們懷疑的方向就定位在render的問題,大概率是出在組件上。
我們中間有幾個猜測
最后發現貌似都不是,這個時候和組內另外一個同事pair,她發現在請求比較多的時候容易有問題,中間還懷疑過網絡請求處理導致的。這個懷疑其實不大對,但是確實為我們找到了一條路。
因為我們最后發現
我們所有的網絡請求都在請求結果返回之前,在頁面出現一層蒙版mask以及loading提示符號(在RN里面是ActivityIndicator),這個部分是會影響頁面render的。
而把這部分去掉(在請求到達之前不出現蒙層),這個bug就沒有了,這個發現當時還是讓人很震驚的以及疑惑的,因為似乎找到了一部分原因但我們還是沒搞清楚為什么。
第三步 嘗試修復(未弄清根本原因的情況下)
有了這個思路的提示,我們試圖嘗試修復。按照業務需求,我們不能取消ActivityIndicator的使用,因為給用戶適當的提示這個確實很有必要,所以我們試圖去修改mask的實現。
在老的mask里面
我們使用了一個第三方的RN組件react-native-root-siblings來幫助我們在root同級插入一個兄弟元素顯示我們的loading提示符號。
一般在發完請求請求結果未到達之前,我們就插入一個新的同級兄弟元素,請求完成后就刪除掉它。
當時懷疑因為這部分反復的修改頁面的元素結構,就把new-destory的邏輯換成了new-update的邏輯,減少了元素的修改。
update的時候只是去讓ActivityIndicator不出現似乎被hide了。
第四步 測試bug是否還能重現
我們希望通過減少頁面元素反復的刪除創建,來fix這個bug,結果怎么樣呢?
居然神奇的很難復現了,我們很開心,雖然還是沒弄懂原因。
后面QA說在真機上還是遇到了幾次,讓我們更是費解,費解的是出現的概率確實變少了,但為啥還會出現?
第五步 分析bug產生的根本原因
這個時候我們需要了解bug產生的真正原因了。
我們重新回到這個bug的表現,為什么點擊空白區域會觸發TextInput的focus方法?我們嘗試做了這樣的事情。
找出在會觸發TextInput的focus的地方,會不會是被錯誤的調用了。
除了在代碼邏輯里面少量的通過綁定ref然后觸發.focus方法(因為是少量出現,不符合我們這個bug一出現所有input都受影響的情景,快速排除不是這部分原因),我們發現在RN提供的TextInput組件里面也有很多地方會調用到focus方法。
大概查找的路徑是文件node_modules/react-native/Libraries/Components/TextInput/TextInput.js中發現多處this.focus()的調用,除了正常的onFocus事件的綁定以及autoFocus,有一個在_onPress里面的調用感覺很奇怪,暫時放著
_onFocus: function(event: Event) {if (this.props.onFocus) {this.props.onFocus(event);}if (this.props.selectionState) {this.props.selectionState.focus();}},//奇怪的地方_onPress: function(event: Event) {if (this.props.editable || this.props.editable === undefined) {console.log('------> _onPress',event);//logthis.focus();}},先打了一段log,發現點擊空白區域的時候,真的被觸發了呀,當然點擊輸入框也會觸發,二者的表現一樣一樣的。
結論
沒法確認是不是被錯誤的調用了,但確實是被調用了,我們去找找調用的地方看有什么線索。
看到target里面的ResponderSyntheticEvent了嗎,找到這個文件打幾行log 有驚喜。
在ResponderSyntheticEvent打日志獲取更多信息,并對比正常和有bug時候的異同
?
如下
你會發現你點擊頁面的任何一個區域都會在console出現這樣的記錄
并且任何一個點擊的響應一般都會有如下四個階段
- onResponderGrant
- onResponderStart
- onResponderEnd
- onResponderRelease
然后試圖重現bug,看看log有沒有什么不一樣,果然被逮住了。
其中綠色部分的log是正常的,紅色劃線是不正常的,發現是輸入框(1387)這個node grant了手勢響應但是后面手勢開始是空白區域(1398),最終空白區域(1398)影響了輸入框(1387)。
結論
正常情況下四個事件依次觸發,出現bug的情況下input的onResponderGrant被調用后面是空白區域的onResponderStart被調用,和其他對比之后,發現onResponderGrant不應該被調用。
了解手勢響應系統
還是很疑惑為什么最開始input框(1387)會grant呢?這部分涉及對手勢的響應,去rn的官網上面我們去了解一下手勢響應系統,看到提到
具體的實現在ResponderEventPlugin.js文件中,你可以在源碼中讀到更多細節和文檔。然后找到react/lib/ResponderEventPlugin.js文件,
在多個地方(主要是setResponderAndExtractTransfer方法內)找到ResponderSyntheticEvent(老朋友了,之前在ta那里打過log)的調用,比如
var grantEvent = ResponderSyntheticEvent. getPooled(eventTypes.responderGrant, wantsResponderInst, nativeEvent, nativeEventTarget);而 setResponderAndExtractTransfer 方法是否調用取決于canTriggerTransfer方法的返回值。
var extracted = canTriggerTransfer(topLevelType, targetInst, nativeEvent) ? setResponderAndExtractTransfer(topLevelType, targetInst, nativeEvent, nativeEventTarget) : null;細看canTriggerTransfer方法
function canTriggerTransfer(topLevelType, topLevelInst, nativeEvent) {console.log('-->response c3', trackedTouchCount, trackedTouchCount > 0);return topLevelInst && (// responderIgnoreScroll: We are trying to migrate away from specifically// tracking native scroll events here and responderIgnoreScroll indicates we// will send topTouchCancel to handle canceling touch events insteadtopLevelType === EventConstants.topLevelTypes.topScroll &&!nativeEvent.responderIgnoreScroll || trackedTouchCount > 0 &&topLevelType === EventConstants.topLevelTypes.topSelectionChange ||isStartish(topLevelType) || isMoveish(topLevelType)); }其實這個地方的log最開始打了好多,最好發現是trackedTouchCount值不一樣導致的。
同時去能夠影響trackedTouchCount值的地方加一些log
簡單描述下這條依賴關系,但其實并不確定是不是在有bug情況下trackedTouchCount值不一樣,先留一個假設。
- 變量trackedTouchCount
- 方法canTriggerTransfer返回值
- 方法setResponderAndExtractTransfer
- 影響grantEvent執行
?
在控制臺仔細觀察,隨便點擊幾下,得到如下的截圖,
這是在正常未出現bug的情況下,trackedTouchCount的值在0和1之間擺動,當tounchstart的時候+1,在touchend的時候-1。
我們再去重現bug,當我們去反復切換tab的時候,看看日志有什么區別。
簡單分析
有一條toucnStart的記錄987沒有對應的TouchEnd,導致trackedTouchCount沒法復位為0。
為什么在反復切換tab的時候,會出現這樣有toucnStart而沒有toucnEnd的情景,想了下發現是每次切換tab其實是做了這么幾件事情
- 點擊tab頁簽
- 頁面出現mask(new一個新的)
- 頁面請求數據
- 數據response到達(destroy mask)
但如果頻繁點動tab頁簽,其實某些邊界時刻,點到的是mask,對應mask的node的toucnStart被觸發,然后請求即將到達,mask被destroy了,toucnEnd永遠都不會被觸發了。
所以當我們把mask的實現從new-destroy改成new-update的時候,保證了toucnEnd最終能夠被觸發了。
歸納
什么情況下 會再次發生
這一次我們定位了這個issue的問題,并且使用了一些不是完全fix的方法,讓這個bug不會由于mask的頻繁使用而出現。
但有沒有可能在其他的業務場景或者寫代碼的過程中再次引入這個bug呢? 答案是肯定的。
后續在team 內我們再次fix過幾次類似的問題,簡單總結如下:
- 場景1 : 給某個業務實體(人 或 物) 添加備注標簽,在輸入欄輸入并按回車后就會生成新的備注標簽,備注標簽上會有一個小叉叉,點擊小叉叉可以刪除這個備注標簽。一旦刪除某個備注標簽,就會重現。
- 場景2: 在某個頁面,會展示一些實體(物)的詳細信息,因為信息比較多,我們做了一個flip的效果,點擊后會翻轉展示更多,在點一下就會回到之前的,就像一個撲克牌的兩面,一面是花紋,一面是具體的大小比如K。 如果連續多次翻轉,就會重現。
這兩個場景,以及我們最初遇到的mask的場景,看似沒有任何聯系,但是最終都會觸發軟鍵盤莫名顯示的問題,其根本原因和之前mask的一致,都是trackedTouchCount這個變量被改壞了。
那為什么這幾個場景都會改壞這個變量呢?
在排查的過程中,我們發現一旦出現某個頁面元素(或者在RN的語境下稱之為組件比較合適)被刪除,而頁面元素上的onPressOut沒來得及觸發,就會出現此類的問題。
這是RN事件響應系統的問題,一般很難去修改底層庫,我們目前的解決辦法基本上是
其他
另外一個在github上面報的因為trackedTouchCount變量不正確狀態導致的issue
[ListView]Scroll on ListView end with an error saying "Ended a touch event which was not counted in trackedTouchCount"
有興趣的可以看看。
總結
以上是生活随笔為你收集整理的Fix一个随机出现的键盘弹出的issue后的思考(ReactNative)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Class文件结构amp;字节码指令
- 下一篇: 全渠道java b2b b2c o2o平