现象
前段时间,忽然注意到在做的 Flutter 项目的日志收集系统中出现了几个异常的用户,他们的日志中出现了大量错误,报错数量多达几千甚至上万:
而且是同一个错误在一段时间内频繁触发,间隔不过十几毫秒:
其具体的错误堆栈信息为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| │ ⛔ Null check operator used on a null value │ ⛔ #0 _AndroidMotionEventConverter.toAndroidMotionEvent.<anonymous closure> (package:flutter/src/services/platform_views.dart:601) │ ⛔ #1 MappedIterable.elementAt (dart:_internal/iterable.dart:374) │ ⛔ #2 ListIterator.moveNext (dart:_internal/iterable.dart:343) │ ⛔ #3 new List.from (dart:core-patch/array_patch.dart:38) │ ⛔ #4 new List.of (dart:core-patch/array_patch.dart:68) │ ⛔ #5 SetMixin.toList (dart:collection/set.dart:102) │ ⛔ #6 _AndroidMotionEventConverter.toAndroidMotionEvent (package:flutter/src/services/platform_views.dart:602) │ ⛔ #7 AndroidViewController.dispatchPointerEvent (package:flutter/src/services/platform_views.dart:864) │ ⛔ #8 _PlatformViewGestureRecognizer.handleEvent (package:flutter/src/rendering/platform_view.dart:535) │ ⛔ #9 PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:77) │ ⛔ #10 PointerRouter._dispatchEventToRoutes.<anonymous closure> (package:flutter/src/gestures/pointer_router.dart:122) │ ⛔ #11 _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:377) │ ⛔ #12 PointerRouter._dispatchEventToRoutes (package:flutter/src/gestures/pointer_router.dart:120) │ ⛔ #13 PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:106) │ ⛔ #14 GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:358) │ ⛔ #15 GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:338) │ ⛔ #16 RendererBinding.dispatchEvent (package:flutter/src/rendering/binding.dart:267) │ ⛔ #17 GestureBinding._handlePointerEvent (package:flutter/src/gestures/binding.dart:295) │ ⛔ #18 GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:240) │ ⛔ #19 GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:213) │ ⛔ #20 _rootRunUnary (dart:async/zone.dart:1206) │ ⛔ #21 _CustomZone.runUnary (dart:async/zone.dart:1100) │ ⛔ #22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1005) │ ⛔ #23 _invoke1 (dart:ui/hooks.dart:265) │ ⛔ #24 _dispatchPointerDataPacket (dart:ui/hooks.dart:174) │ ⛔
|
可以看到错误的调用栈完全在 SDK 层,不包含我写的业务逻辑代码,而且凭我对 Flutter 架构的了解,看到错误来自 /services/platform_views.dart
,基本可以确定这是项目里引入的 WebView 插件抛出的异常。
但奇怪的是,与 WebView 相关的代码已经很久没有动过了,简单回忆一下最近的项目的所有更新,也不像是会有导致这样问题的更改,感觉很是懵逼……
分析
好在这个问题并不难查,直接以错误堆栈第一行的 _AndroidMotionEventConverter.toAndroidMotionEvent
作为关键词在 flutter 的 GitHub issue 区搜索,就查到了这两个讨论: [google_maps_flutter] Unhandled Exception: Null check operator used on a null value [google_maps_flutter] Three (or more) finger gestures make the app unusable。
虽然这个讨论里说的是 google_maps_flutter 这个包,但是因为 google_maps_flutter 和 webview_flutter 这两个包在实现 Android 端的平台显示时都引用了 SDK 中的 AndroidView
,所以本质上是同一个问题。大致问题就是,当用户用三指或更多的多点触摸操作诸如 google_maps_flutter 、webview_flutter 这样使用了 AndroidView 的组件,就会引发程序异常,而且这个异常一旦抛出,之后正常的单指和双指操作也会持续引发错误,直到程序重启,这个描述与我错误日志的表现完全一致。
然后我用自己手机试了一下,在 APP 中使用了 WebView 的页面尝试三指操作,果然也触发了异常,而且整个页面也完全卡住了,可以想象,用户操作此时肯定是要对着屏幕一通点按划,希望应用能有点反应,结果就是抛出了一大堆的错误 😂。
问题几乎可以确认了,但是为什么这个问题会忽然大量出现呢?看 Issue 讨论和 SDK 中代码的提交记录,这个官方 BUG 存在至少一年以上了,根据讨论 Sunbreak 的评论,他认为是 MIUI 上有一个三指下滑截屏的系统手势,可能会让很多人由此触发本 BUG,但我总觉得可能不是这个原因……
又看了看日志中异常的用户,大部分用户的昵称都像是女性,以我钢铁直 对非男性的认识,猜测会不会是这些用户化妆之后用沾了化妆品的手去操作手机,然后手机屏幕很油,导致的屏幕触摸识别出错?虽然这个猜测一样解释不了为何最近异常数突增的问题,但结果来说居然猜得八九不离十 😹
问询
异常用户中有一些是作为内部测试的公司同事,虽然因为岗位不同平时不怎么有机会能碰得到,几天后终于碰巧碰到一位,我就问了下她的情况 ——
Hi,xxx,请问一下,最近你用我们的 APP 的时候是不是经常出现页面完全卡死,怎么操作都没反应的情况啊?
—— 嗯,是啊,好几次了,我都不知道怎么突然就卡死了,我怎么试都不行,最后关了应用重开才能再用&×%¥&×(#%@……
emmm,其实我这边能看到那些错误的,根据错误提示,可能是因为你用三根以上的手指操作手机了?
—— 没有没有!不可能的!!我都是正常使用的!!!
额……我不是说你肯定是那样操作才……
—— 真的没有!我都是一根手指这样点~~连滑动都不敢划……&¥……&×(……%&@……!@#&×
………………
………………
行行行我知道了,让我看下你的手机吧 😑
然后,她就递给我一个油腻得疑似是手机的物体 😱
然后又询问了一下,原来是她们团队那边最近准备做一款护手霜的代理推广,所以内部同事都在测试效果,经常是涂了护手霜后又来用 APP,而这款护手霜看上去就是这么的“油”…… 我想我大概是已经破案了。。。
然后我问她们要来了一点同款护手霜,简单做了下测试:
解决
参看:PR: Fix crash when do three finger gesture,这个问题在 2020 年年底就有人尝试解决了,我根据他的修改 packages/flutter/lib/src/services/platform_views.dart 手动更新了一下本机的 Flutter SDK,确实解决了问题 👏 但是这个 PR 却迟迟没能合并进主线分支,因为这个 PR 的作者不太清楚怎么给新加的代码写测试,所以没能通过 Flutter 官方设置的自动测试(但是看测试的失败报告,是只在 MacOS 平台上的 build 失败了,那其实并不影响 Android 和 iOS 的),而且即使合并进了主线,以 Flutter 的版本发布策略和频率,这个修复应用到稳定分支可能最少还要数月的时间,所以目前只能自己处理。
由于要修改的代码在 SDK 源码中,而不是项目仓库中的代码,而且我的项目接入了基于 docker 的 GitLab CI,每次构建时编译环境都会重置,所以必须把这个修复用自动化的方式实现。
所以我写了 python 脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
import os
def patch_platform_views(flutter_sdk_path): origin_content = r''' if (event.platformData == kPointerDataFlagBatched || (isSinglePointerAction(event) && pointerIdx < numPointers - 1)) { return null; }
int action; '''.strip()
replace_content = r''' if (event.platformData == kPointerDataFlagBatched || (isSinglePointerAction(event) && pointerIdx < numPointers - 1)) { return null; }
if (pointers.length != pointerProperties.length || pointers.length != pointerPositions.length) { return null; }
int action; '''.strip()
file_path = os.path.join(flutter_sdk_path, 'packages/flutter/lib/src/services/platform_views.dart') with open(file_path, mode='r') as inp: content = inp.read() if origin_content in content: with open(file_path, mode='w') as out: out.write(content.replace(origin_content, replace_content)) print('Patched !!!\n') else: print('No change !!!\n')
if __name__ == '__main__': flutter_path = os.popen('command -v flutter').read().strip() if flutter_path.endswith('bin/flutter'): flutter_path = flutter_path[0: flutter_path.index('bin/flutter')] patch_platform_views(flutter_path)
|
然后加入到 GitLab CI/CD (一) :自动打包部署Flutter项目 中所示的 .gitlab-ci.yml
中:
1 2 3 4
| …… - python3 flutter_patch.py - flutter -v build apk --no-shrink --target-platform=android-arm ……
|
这样,每次发布时通过 GitLab CI 编译产生的 apk 就是修复过 SDK 的版本了: