首頁 > 軟體

混合棧跳轉導致Flutter頁面事件卡死問題解決

2022-08-09 22:02:48

問題來源

在我們升級Flutter2.5後,測試在走整個業務流程中發現了有頁面卡死現象,於是給我提了一個BUG。

在xx頁面多次操作後,頁面卡死,頁面還可以捲動但是無法跳轉,點選長按事件都失效了。

在我多次測試後發現,確實存在這個問題,而且老版本也都存在。

問題難點

復現難

問題定位

最開始,我先確定了失效情況下,事件源頭有沒有正確傳送,所以,先在_dispatchPointerDataPacket方法上新增了斷點。結果發現都是正常。其實也好理解,頁面可以捲動,說明引擎層傳送事件肯定是正常的。

在進行一系列沒有用的斷點定位後發現,正常事件的hitTestResult(事件中命中測試階段收集的所有能夠響應事件的RenderObject節點)和錯誤頁面的hitTestResult_path數量不一樣。

正常的hitTestResult

錯誤的hitTestResult 

經過對比發現,錯誤的列表到RenderPointerListener這個就停止了,我看這名字還挺熟悉,難道跟IgnorePointer有啥關係?我通過這個RenderObject節點的parent一層一層往上找,發現是ScrollableState中使用了IgnorePointerScrollableState是列表元件如ListViewSingleChildScrollView等底層使用的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來把卷動區域及其子節點的事件都遮蔽了。那麼什麼時候_ignorePointerKey會被置為true呢。

通過了解ScrollableState原始碼發現,只要頁面在捲動過程中,_ignorePointerKey就會被置為true,當手指擡起時,才會將_ignorePointerKey重新置為false

通過多次斷點和紀錄檔輸出發現,當我從後面的頁面返回到目標頁面時,第一次捲動時,就觸發了ScrollableStatesetIgnorePointer_ignorePointerKey置為true了,但是後面再無事件將_ignorePointerKey置為false了,此後,再捲動頁面時,也無法觸發setIgnorePointer方法。

到這裡,想繼續偵錯,就需要比較熟悉Flutter的事件原理了,因為這裡我只想講一下我解決這個問題的思路,所以Flutter原理的知識不多講。後面經過一系列偵錯發現,問題出在OneSequenceGestureRecognizer這個抽象類中

abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
  //...
  @protected
  void startTrackingPointer(int pointer, [Matrix4? transform]) {
    // 將當前指標和當前的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)) {
      // 從全域性指標中移出當前指標
      GestureBinding.instance!.pointerRouter.removeRoute(pointer, handleEvent);
      _trackedPointers.remove(pointer);
      // 如果_trackedPointers是空的
      if (_trackedPointers.isEmpty)
        didStopTrackingLastPointer(pointer);
    }
  }
}

OneSequenceGestureRecognizer這個類的作用是當存在多個手勢時,只響應一個手勢。比如我同時兩個手指點選一個按鈕,按鈕的點選事件也只會觸發一次。像我們常見的TapGestureRecognizerVerticalDragGestureRecognizerHorizontalDragGestureRecognizer等最終都是實現的這個類。

在這個類中startTrackingPointer方法會在手指按下後,也就是發生PointerDownEvent時將當前類的handleEvent新增到全域性指標識別器中,並且將這個pointer(可以看做指標id)新增到_trackedPointers中快取起來,可以這樣理解,這個方法就是一次手勢的開始。

當發生PointerUpEvent等事件時,會呼叫stopTrackingPointer事件,將手勢移除,這就標誌著手勢的結束。

其中有個_trackedPointers.isEmpty判斷,會呼叫didStopTrackingLastPointer方法,這個方法一般是將手勢識別器的狀態置為ready。經過我多次對問題頁斷點發現,無論如何都調不到這個方法,也就是說_trackedPointers裡面一直有個手勢指標沒有被移除。

這裡我要介紹一下VSCode一個偵錯方法。因為我還不知道問題的根源,所以我復現問題是通過不斷點選頁面同時觸發頁面跳轉來達到的,而且只是有機率復現。所以我無法通過斷點來確定這裡為何有手勢事件沒有呼叫stopTrackingPointer,所以我使用了VSCode的LogPoint方式來對整個過程進行紀錄檔輸出。

在不斷復現問題檢視紀錄檔中發現,在跳轉頁面前,會有指標事件被新增進_trackedPointers,但是卻沒有呼叫stopTrackingPointer方法就跳轉到新頁面了。

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,在進行頁面跳轉時,會將FlutterEngine先detach,然後再跳轉。在Flutter的Android傳送事件原始碼裡面,會對FlutterEngine是否attach進行判斷,然後觸發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);
  }

這裡由於頁面跳轉時如果還有事件在處理(比如手指按下並沒有擡起),那麼跳轉後,Flutter再也接收不到手指擡起的事件了,所以_trackedPointers就一直不被正確移除,導致了事件異常。由於是我們自己寫的混合棧庫,所以修改起來也簡單。

問題解決

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的類,在其中實現一下父類別的onTouchEvent方法,把isAttachedToFlutterEngine的判斷去掉即可,由於androidTouchProcessor是私有類,所以這裡我使用了反射。

iOS解決思路還不太一樣,在新的Flutter版本中,iOS提供了forceTouchesCancelled方法來取消Flutter中的事件,所以iOS是通過在混合棧中detach前,手動呼叫一下這個方法來解決這個問題的。

總結

由於對Flutter事件很多細節掌握的不夠到位,所以這個問題從定位問題到最終解決差不多花了一週時間,解決過程中也加深了我對Flutter事件的理解。

以上就是混合棧跳轉導致Flutter頁面事件卡死問題解決的詳細內容,更多關於混合棧Flutter頁面卡死的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com