问题来源
问题难点
问题定位
问题确定
问题解决
总结
问题来源在我们升级Flutter2.5后,测试在走整个业务流程中发现了有页面卡死现象,于是给我提了一个BUG。
在xx页面多次操作后,页面卡死,页面还可以滚动但是无法跳转,点击长按事件都失效了。
在我多次测试后发现,确实存在这个问题,而且老版本也都存在。
问题难点复现难
问题定位最开始,我先确定了失效情况下,事件源头有没有正确发送,所以,先在_dispatchPointerDataPacket
方法上添加了断点。结果发现都是正常。其实也好理解,页面可以滚动,说明引擎层发送事件肯定是正常的。
在进行一系列没有用的断点定位后发现,正常事件的hitTestResult
(事件中命中测试阶段收集的所有能够响应事件的RenderObject
节点)和错误页面的hitTestResult
的_path
数量不一样。
正常的hitTestResult
错误的hitTestResult
经过对比发现,错误的列表到RenderPointerListener
这个就停止了,我看这名字还挺熟悉,难道跟IgnorePointer
有啥关系?我通过这个RenderObject
节点的parent
一层一层往上找,发现是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
来把滚动区域及其子节点的事件都屏蔽了。那么什么时候_ignorePointerKey
会被置为true
呢。
通过了解ScrollableState
源码发现,只要页面在滚动过程中,_ignorePointerKey
就会被置为true
,当手指抬起时,才会将_ignorePointerKey
重新置为false
。
通过多次断点和日志输出发现,当我从后面的页面返回到目标页面时,第一次滚动时,就触发了ScrollableState
的setIgnorePointer
将_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
这个类的作用是当存在多个手势时,只响应一个手势。比如我同时两个手指点击一个按钮,按钮的点击事件也只会触发一次。像我们常见的TapGestureRecognizer
、VerticalDragGestureRecognizer
、HorizontalDragGestureRecognizer
等最终都是实现的这个类。
在这个类中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页面卡死的资料请关注易知道(ezd.cc)其它相关文章!