现象

前段时间,忽然注意到在做的 Flutter 项目的日志收集系统中出现了几个异常的用户,他们的日志中出现了大量错误,报错数量多达几千甚至上万:
debugger_log.webp

而且是同一个错误在一段时间内频繁触发,间隔不过十几毫秒:
log.webp

其具体的错误堆栈信息为:

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
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# @Filename : flutter_patch.py
# @Date : 2021/2/3 下午3:51
# @Author : DebuggerX

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()
# 只在 SDK 中源码还没有修复时进行 patch
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__':
# 利用 command -v flutter 命令找到当前环境中 flutter sdk所在的路径
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 的版本了:

patched.webp