混合栈跳转导致Flutter页面事件卡死问题解决

目录
  • 问题来源
  • 问题难点
  • 问题定位
  • 问题确定
  • 问题解决
  • 总结

问题来源

在我们升级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页面卡死的资料请关注我们其它相关文章!

(0)

相关推荐

  • Flutter页面传值的几种方式

    今天来聊聊Flutter页面传值的几种方式: InheritWidget Notification Eventbus (当前Flutter版本:2.0.4) InheritWidget 如果看过Provider的源码的同学都知道,Provider跨组件传值的原理就是根据系统提供的InheritWidget实现的,让我们来看一下这个组件. InheritWidget是一个抽象类,我们写一个保存用户信息的类UserInfoInheritWidget继承于InheritWidget: class Us

  • Flutter app页面路由以及路由拦截的实现

    为什么要使用路由 在之前我们的代码中,页面跳转使用的代码如下所示: Navigator.of(context).push( MaterialPageRoute(builder: (context) => LoginPage()), ); 在开发过程中,随着页面的增加,如果继续使用这种方式会有如下缺陷: 代码耦合严重:涉及到页面跳转的地方就需要插入页面的构造函数,意味着需要知道其他页面的构建方式. 不易维护:一旦某个页面发生了变化,需要将涉及到该页面的跳转全部改变. 权限控制不方便:假设某些页面需

  • Flutter 用自定义转场动画实现页面切换

    fluro 转场动画源码 在使用自定义转场动画前,先扒一扒 fluro 的源码,通过源码可以发现这么一个标准的转场方法: RouteTransitionsBuilder _standardTransitionsBuilder( TransitionType? transitionType) { return (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation

  • Flutter 使用fluro的转场动画进行页面切换

    在 fluro 中,定义路由处理器 Handler 时可以指定该页面的默认转场形式,或者在使用 navigateTo 方法是可以设置页面跳转transition参数来设定个性化的转场形式.本篇演示了fluro 内置的转场动画,效果如下图所示. 转场形式 fluro 的转场形式通过 TransitionType枚举定义,如下所示: enum TransitionType { native, //原生形式,和原生的保持一致,默认 nativeModal, //原生模态跳转 inFromLeft, /

  • Flutter 使用Navigator进行局部跳转页面的方法

    Navigator组件使用的频率不是很高,但在一些场景下非常适用,比如局部表单多页填写.底部导航一直存在,每个tab各自导航场景. Navigator 是管理路由的控件,通常情况下直接使用Navigator.of(context)的方法来跳转页面,之所以可以直接使用Navigator.of(context)是因为在WidgetsApp中使用了此控件,应用程序的根控件通常是MaterialApp,MaterialApp包含WidgetsApp,所以可以直接使用Navigator的相关属性. Nav

  • 混合栈跳转导致Flutter页面事件卡死问题解决

    目录 问题来源 问题难点 问题定位 问题确定 问题解决 总结 问题来源 在我们升级Flutter2.5后,测试在走整个业务流程中发现了有页面卡死现象,于是给我提了一个BUG. 在xx页面多次操作后,页面卡死,页面还可以滚动但是无法跳转,点击长按事件都失效了. 在我多次测试后发现,确实存在这个问题,而且老版本也都存在. 问题难点 复现难 问题定位 最开始,我先确定了失效情况下,事件源头有没有正确发送,所以,先在_dispatchPointerDataPacket方法上添加了断点.结果发现都是正常.

  • Session过期后自动跳转到登录页面的实例代码

    最近做了一个项目其中有需求,要实现自动登录功能,通过查阅相关资料,打算用session监听来做,下面给大家列出了配置监听器的方法: 1.在项目的web.xml文件中添加如下代码: <!--添加Session监听器--> <listener> <listener-class> 监听器路径 </listener-class> </listener> 2.编写java类. public class SessionListener implements

  • JS时间分片技术解决长任务导致的页面卡顿

    目录 起因 处理办法 事件循环 浏览器渲染时机 原始代码 代码 效果 函数改造 代码 效果 优化时间分片 代码 效果 对比优化前后 代码 效果 最后 起因 同事遇到一个动画展示的问题,就是下面要执行一个运算量很大的函数,他要加载一个 loading,但他发现把 loading 的元素 display: block; 页面中也不会立刻出现 loading 动画,出现动画的时候是运算函数执行完毕之后. 处理办法 有两种方法去处理这种耗时任务,第一种就是 webWorker,但是一些 dom 的操作做

  • Flutter EventBus事件总线的应用详解

    目录 前言 EventBus的简介 EventBus的实际应用 总结 前言 flutter项目中,有许多可以实现跨组件通讯的方案,其中包括InheritedWidget,Notification,EventBus等.本文主要探讨的是EventBus事件总线实现跨组件通讯的方法. EventBus的简介 EventBus的核心是基于Streams.它允许侦听器订阅事件并允许发布者触发事件,使得不同组件的数据不需要一层层传递,可以直接通过EventBus实现跨组件通讯. EventBus最主要是通过

  • Vue实现路由跳转至外界页面

    目录 Vue路由跳转至外界页面 解决办法 Vue路由跳转页面的几种方式 总结 Vue路由跳转至外界页面 用法 如果使用路由是在 vue 页面中来回跳转,可以使用 this.$router.push() 实现,但是如果想用这种方法跳转到外部链接就会报错,因为外部页面中是存在 HTTP 等前缀的. 解决办法 1. 在 data 中定义好要跳转的外部链接 data() {     return {         url: 'http://www.baidu.com'     } } 2. 按钮中创建

  • Vue3如何根据搜索框内容跳转至本页面指定位置

    目录 需求 思路 实现过程 补充内容 总结 需求 需求:根据搜索框内容跳转至本页面指定位置 搜索框是我们在开发各类项目中出现率很高的一个"组件",在element-plus中名为"自动补全输入框",即我们可以根据输入的内容去检索列表或者表格或者其他本页面出现的元素,那我们应该如何去实现这个行为呢? 思路 整体过程是这样的: 点击输入框的内容,页面跳转至指定的内容位置 实现过程 ①首先我们必须要在页面中引入自动补全输入框组件 template部分 <el-aut

  • Vue-router路由判断页面未登录跳转到登录页面的实例

    如下所示: router.beforeEach((to, from, next) => { if (to.matched.some(record => record.meta.requireAuth)){ // 判断该路由是否需要登录权限 if (token) { // 判断当前的token是否存在 next(); } else { next({ path: '/login', query: {redirect: to.fullPath} // 将跳转的路由path作为参数,登录成功后跳转到该

  • Spring boot 跳转到jsp页面的实现方法

    本人正在学习Spring boot,搜索了很多关于Spring boot 跳转到jsp页面的实现方法介绍,下面我来记录一下,有需要了解的朋友可参考.希望此文章对各位有所帮助. @Controller注解 1.application.properties文件中配置 # 配置jsp文件的位置,默认位置为:src/main/webapp spring.mvc.view.prefix=/pages/ # 配置jsp文件的后缀 spring.mvc.view.suffix=.jsp 2.Controlle

  • iOS 点击推送消息跳到应用指定页面的实例

    iOS 点击推送消息跳到应用指定页面 现在的推送用的越来越频繁,几乎每个应用都开始用到了.其实又有几个用户会去看推送消息呢?没办法,产品经理最大啊,只是苦了我们这一帮程序员啊!闲话少说,进入正题.兄弟我用的是极光推送,自然是以极光推送为例了.   现在点击推送消息,有两种跳转方式:1.打开应用,跳转到应用首页:2.打开应用,跳转到指定页面. ​第一种,你什么都不用设置,只要注册极光应用就可以.这里就不写怎么注册极光应用了,可以参考官方文档,写的很详细. ​ 第二种,重头戏来了. // APP未运

随机推荐