混合棧跳轉(zhuǎn)導(dǎo)致Flutter頁面事件卡死問題解決
問題來源
在我們升級Flutter2.5后,測試在走整個業(yè)務(wù)流程中發(fā)現(xiàn)了有頁面卡死現(xiàn)象,于是給我提了一個BUG。
在xx頁面多次操作后,頁面卡死,頁面還可以滾動但是無法跳轉(zhuǎn),點擊長按事件都失效了。
在我多次測試后發(fā)現(xiàn),確實存在這個問題,而且老版本也都存在。
問題難點
復(fù)現(xiàn)難
問題定位
最開始,我先確定了失效情況下,事件源頭有沒有正確發(fā)送,所以,先在_dispatchPointerDataPacket
方法上添加了斷點。結(jié)果發(fā)現(xiàn)都是正常。其實也好理解,頁面可以滾動,說明引擎層發(fā)送事件肯定是正常的。
在進行一系列沒有用的斷點定位后發(fā)現(xiàn),正常事件的hitTestResult
(事件中命中測試階段收集的所有能夠響應(yīng)事件的RenderObject
節(jié)點)和錯誤頁面的hitTestResult
的_path
數(shù)量不一樣。
正常的hitTestResult
錯誤的hitTestResult
經(jīng)過對比發(fā)現(xiàn),錯誤的列表到RenderPointerListener
這個就停止了,我看這名字還挺熟悉,難道跟IgnorePointer
有啥關(guān)系?我通過這個RenderObject
節(jié)點的parent
一層一層往上找,發(fā)現(xiàn)是ScrollableState
中使用了IgnorePointer
(ScrollableState
是列表組件如ListView
、SingleChildScrollView
等底層使用的Widget State)
//... Widget result = _ScrollableScope( scrollable: this, position: position, child: Listener( onPointerSignal: _receivedPointerSignal, child: RawGestureDetector( key: _gestureDetectorKey, gestures: _gestureRecognizers, behavior: HitTestBehavior.opaque, excludeFromSemantics: widget.excludeFromSemantics, child: Semantics( explicitChildNodes: !widget.excludeFromSemantics, child: IgnorePointer( key: _ignorePointerKey, ignoring: _shouldIgnorePointer, ignoringSemantics: false, child: widget.viewportBuilder(context, position), ), ), ), ), ); //...
這里會通過_ignorePointerKey
來把滾動區(qū)域及其子節(jié)點的事件都屏蔽了。那么什么時候_ignorePointerKey
會被置為true
呢。
通過了解ScrollableState
源碼發(fā)現(xiàn),只要頁面在滾動過程中,_ignorePointerKey
就會被置為true
,當(dāng)手指抬起時,才會將_ignorePointerKey
重新置為false
。
通過多次斷點和日志輸出發(fā)現(xiàn),當(dāng)我從后面的頁面返回到目標(biāo)頁面時,第一次滾動時,就觸發(fā)了ScrollableState
的setIgnorePointer
將_ignorePointerKey
置為true
了,但是后面再無事件將_ignorePointerKey
置為false
了,此后,再滾動頁面時,也無法觸發(fā)setIgnorePointer
方法。
到這里,想繼續(xù)調(diào)試,就需要比較熟悉Flutter的事件原理了,因為這里我只想講一下我解決這個問題的思路,所以Flutter原理的知識不多講。后面經(jīng)過一系列調(diào)試發(fā)現(xiàn),問題出在OneSequenceGestureRecognizer
這個抽象類中
abstract class OneSequenceGestureRecognizer extends GestureRecognizer { //... @protected void startTrackingPointer(int pointer, [Matrix4? transform]) { // 將當(dāng)前指針和當(dāng)前的handleEvent方法添加到全局指針識別器中存儲緩存起來 GestureBinding.instance!.pointerRouter.addRoute(pointer, handleEvent, transform); _trackedPointers.add(pointer); assert(!_entries.containsValue(pointer)); _entries[pointer] = _addPointerToArena(pointer); } @protected void stopTrackingPointer(int pointer) { if (_trackedPointers.contains(pointer)) { // 從全局指針中移出當(dāng)前指針 GestureBinding.instance!.pointerRouter.removeRoute(pointer, handleEvent); _trackedPointers.remove(pointer); // 如果_trackedPointers是空的 if (_trackedPointers.isEmpty) didStopTrackingLastPointer(pointer); } } }
OneSequenceGestureRecognizer
這個類的作用是當(dāng)存在多個手勢時,只響應(yīng)一個手勢。比如我同時兩個手指點擊一個按鈕,按鈕的點擊事件也只會觸發(fā)一次。像我們常見的TapGestureRecognizer
、VerticalDragGestureRecognizer
、HorizontalDragGestureRecognizer
等最終都是實現(xiàn)的這個類。
在這個類中startTrackingPointer
方法會在手指按下后,也就是發(fā)生PointerDownEvent
時將當(dāng)前類的handleEvent
添加到全局指針識別器中,并且將這個pointer
(可以看做指針id)添加到_trackedPointers
中緩存起來,可以這樣理解,這個方法就是一次手勢的開始。
當(dāng)發(fā)生PointerUpEvent
等事件時,會調(diào)用stopTrackingPointer
事件,將手勢移除,這就標(biāo)志著手勢的結(jié)束。
其中有個_trackedPointers.isEmpty
判斷,會調(diào)用didStopTrackingLastPointer
方法,這個方法一般是將手勢識別器的狀態(tài)置為ready
。經(jīng)過我多次對問題頁斷點發(fā)現(xiàn),無論如何都調(diào)不到這個方法,也就是說_trackedPointers
里面一直有個手勢指針沒有被移除。
這里我要介紹一下VSCode一個調(diào)試方法。因為我還不知道問題的根源,所以我復(fù)現(xiàn)問題是通過不斷點擊頁面同時觸發(fā)頁面跳轉(zhuǎn)來達(dá)到的,而且只是有幾率復(fù)現(xiàn)。所以我無法通過斷點來確定這里為何有手勢事件沒有調(diào)用stopTrackingPointer
,所以我使用了VSCode的LogPoint
方式來對整個過程進行日志輸出。
在不斷復(fù)現(xiàn)問題查看日志中發(fā)現(xiàn),在跳轉(zhuǎn)頁面前,會有指針事件被添加進_trackedPointers
,但是卻沒有調(diào)用stopTrackingPointer
方法就跳轉(zhuǎn)到新頁面了。
tap 4. addAllowedPointer (tap.dart) _down != null = true 637436658 tap 5. _trackedPointers add 195 502831342 handleEvent: 931478062 tap 5. _trackedPointers add 195 21393736 handleEvent: 790157058 tap 5. _trackedPointers add 195 126324365 handleEvent: 160402385 onNativeRouteEvent: (9): NativeRouteEvent.onCreate onNativeRouteEvent: (8): NativeRouteEvent.onPause onFlutterRouteEvent: (9): FlutterRouteEvent.onPush
問題確定
由于我們是混合棧項目,我們是自己寫的一套混合棧路由管理,類似FlutterBoost,在進行頁面跳轉(zhuǎn)時,會將FlutterEngine
先detach,然后再跳轉(zhuǎn)。在Flutter的Android發(fā)送事件源碼里面,會對FlutterEngine
是否attach
進行判斷,然后觸發(fā)Flutter Framework一系列處理。
@Override public boolean onTouchEvent(@NonNull MotionEvent event) { // 這里判斷是否attach if (!isAttachedToFlutterEngine()) { return super.onTouchEvent(event); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { requestUnbufferedDispatch(event); } return androidTouchProcessor.onTouchEvent(event); }
這里由于頁面跳轉(zhuǎn)時如果還有事件在處理(比如手指按下并沒有抬起),那么跳轉(zhuǎn)后,F(xiàn)lutter再也接收不到手指抬起的事件了,所以_trackedPointers
就一直不被正確移除,導(dǎo)致了事件異常。由于是我們自己寫的混合棧庫,所以修改起來也簡單。
問題解決
Android
public class XXXFlutterView extends FlutterView { // ... @Override public boolean onTouchEvent(@NonNull MotionEvent event) { try { AndroidTouchProcessor androidTouchProcessor; Field field = this.getClass().getSuperclass().getDeclaredField("androidTouchProcessor"); field.setAccessible(true); androidTouchProcessor = (AndroidTouchProcessor)field.get(this); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { requestUnbufferedDispatch(event); } return androidTouchProcessor.onTouchEvent(event); } catch (Exception e) { e.printStackTrace(); return super.onTouchEvent(event); } } }
我們本身有一個繼承于FlutterView
的類,在其中實現(xiàn)一下父類的onTouchEvent
方法,把isAttachedToFlutterEngine
的判斷去掉即可,由于androidTouchProcessor
是私有類,所以這里我使用了反射。
iOS解決思路還不太一樣,在新的Flutter版本中,iOS提供了forceTouchesCancelled
方法來取消Flutter中的事件,所以iOS是通過在混合棧中detach前,手動調(diào)用一下這個方法來解決這個問題的。
總結(jié)
由于對Flutter事件很多細(xì)節(jié)掌握的不夠到位,所以這個問題從定位問題到最終解決差不多花了一周時間,解決過程中也加深了我對Flutter事件的理解。
以上就是混合棧跳轉(zhuǎn)導(dǎo)致Flutter頁面事件卡死問題解決的詳細(xì)內(nèi)容,更多關(guān)于混合棧Flutter頁面卡死的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
iOS Webview自適應(yīng)實際內(nèi)容高度的4種方法詳解
這篇文章主要介紹了iOS Webview自適應(yīng)實際內(nèi)容高度的4種方法詳解,本文介紹的非常詳細(xì),具有參考借鑒價值,需要的朋友可以參考下2016-09-09C++ 中exit(),_exit(),return,abort()函數(shù)的區(qū)別
這篇文章主要介紹了C++ 中exit(),_exit(),return,abort()函數(shù)的區(qū)別的相關(guān)資料,需要的朋友可以參考下2016-12-12