preface

This time it took a long time to solve the problem, so I wanted to write it down. This article is mainly to share my thoughts on how to solve this problem.

Source of problem

After we upgraded Flutter2.5, the test found page freezes in the entire business process and gave me a BUG.

After several operations on xx page, the page is stuck, the page can still scroll but cannot jump, and clicking and pressing events are invalid.Copy the code

After many tests, I found that this problem did exist, and it also existed in older versions.

The difficulty problem

Repetition is difficult

Problem orientation

At first, I make sure of the failure cases, the event source have a right to send, so, first add the breakpoint on _dispatchPointerDataPacket method. It turned out to be normal. In fact, it is understandable that the page can scroll, which means that the engine layer must send events normally.

After a series of unusable breakpoint locations, it was found that the number of hitTestResult _paths for the normal event (the event hit all of the RenderObject nodes collected during the test phase that could respond to the event) and the number of hitTestResult _paths for the error page were different.

The normalhitTestResult The wronghitTestResult

RenderPointerListener (RenderPointerListener) {RenderPointerListener (RenderPointerListener); I went up the parent layer of the RenderObject node, An IgnorePointer is used in ScrollableState (ScrollableState is the Widget State used at the bottom of list components such as ListView, SingleChildScrollView, etc.)

/ /...
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),
        ),
      ),
    ),
  ),
);
/ /...
Copy the code

The _ignorePointerKey is used to mask events in the scroll region and its children. So when is _ignorePointerKey set to true?

Looking at the ScrollableState source code, _ignorePointerKey is set to true whenever the page is scrolling, and to false when the finger is lifted.

Through multiple breakpoints and log output, when I return from a later page to the target page, the first scroll triggers setIgnorePointer of ScrollableState to set _ignorePointerKey to true, However, there are no further events that set _ignorePointerKey to false, and the setIgnorePointer method cannot be triggered when scrolling the page again.

At this point, if you want to continue debugging, you need to be familiar with the principle of Flutter, because I just want to talk about my idea of how to solve this problem, so I don’t know much about the principle of Flutter. Back after a series of debugging, found that the problem was with OneSequenceGestureRecognizer this abstract class

abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
  / /...
  @protected
  void startTrackingPointer(int pointer, [Matrix4? transform]) {
    // Add the current pointer and the current handleEvent method to the global pointer recognizer for storage and cachingGestureBinding.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)) {
      // Remove the current pointer from the global pointerGestureBinding.instance! .pointerRouter.removeRoute(pointer, handleEvent); _trackedPointers.remove(pointer);// If _trackedPointers is empty
      if(_trackedPointers.isEmpty) didStopTrackingLastPointer(pointer); }}}Copy the code

OneSequenceGestureRecognizer of this class is used when there are multiple gestures, only responds to a gesture. For example, if I click a button with two fingers at the same time, the button click event will only be triggered once. As our common TapGestureRecognizer, VerticalDragGestureRecognizer, HorizontalDragGestureRecognizer are finally realize this class. In this class, the startTrackingPointer method adds the handleEvent of the current class to the global pointer recognizer after the finger is pressed, when a PointerDownEvent occurs. Add the pointer (see pointer ID) to the cache for _trackedPointers. This method is the start of a gesture.

When an event such as PointerUpEvent occurs, the stopTrackingPointer event is called to remove the gesture, which marks the end of the gesture.

One _trackedPointers isEmpty judgment, will call didStopTrackingLastPointer method, this method is generally the gesture recognizer to ready state. After checking the problem page breakpoints several times, I found that this method could not be called anyway, which means that a gesture pointer was never removed from _trackedPointers.

Here I will introduce VSCode a debugging method. Since I don’t know the root cause of the problem yet, I reproduce the problem by repeatedly clicking on the page and triggering a page redirect, only occasionally. So I couldn’t use the breakpoint to determine why there was a gesture event that didn’t call stopTrackingPointer, so I used VSCode’s LogPoint method to log the whole process.

In the recurring problem view log, pointer events are added to _trackedPointers before the jump page, but the new page is jumped without calling the stopTrackingPointer method.

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.onPushCopy the code

Problem determination

Since we are a mixed stack project, we wrote a set of mixed stack routing management by ourselves, which is similar to FlutterBoost. When a page jump is made, FlutterEngine will detach first and then jump. In the source code of the Flutter Android send event, FlutterEngine determines whether to attach and triggers the Flutter Framework to process the event.

@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);
  }
Copy the code

If there are still events being processed during the page jump (for example, the finger did not lift after the jump), the event that the finger lifted will not be received by the Flutter anymore, so _trackedPointers will not be removed correctly, resulting in an event exception. Because it is our own hybrid stack library, so it is easy to modify.

Problem solving

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); }}}Copy the code

We have an inheritance in itself FlutterView classes, in which the implementation of the parent class method onTouchEvent, remove the isAttachedToFlutterEngine judgment, because the androidTouchProcessor is private class, So I’m using reflection here.

The iOS solution is not quite the same. In the new version of Flutter, iOS provides forceTouchesCancelled for events in a Flutter, so iOS fixes this problem manually by calling this method before detach in the mixed stack.

conclusion

Due to my insufficient grasp of many details of the Flutter event, it took me almost a week to locate the problem and finally solve it, which also deepened my understanding of the Flutter event.