乐趣区

走近科学探究阿里闲鱼团队通过数据提升Flutter体验的真相

背景

闲鱼客户端的 flutter 页面已经服务上亿级用户,这个时候 Flutter 页面的用户体验尤其重要,完善 Flutter 性能稳定性监控体系,可以及早发现线上性能问题,也可以作为用户体验提升的衡量标准。那么 Flutter 的性能到底如何?是否像官方宣传的那么丝滑?Native 的性能指标是否可以用来检测 Flutter 页面?下面给大家分享我们在实践中总结出来的 Flutter 的性能稳定性监控方案。

目标

过度的丢帧从视觉上会出现卡顿现象,体现在用户滑动操作不流畅;页面加载耗时过长容易中断操作流程;Flutter 部分 exception 会导致发生异常代码后面的逻辑没有走到从而造成逻辑 bug 甚至白屏。这些问题很容易考验用户耐心,引起用户反感。

所以我们制定以下三个指标作为线上 Flutter 性能稳定性标准:

  1. 页面滑动流畅度
  2. 页面加载耗时(首屏时长 + 可交互时长)
  3. Exception 率

最终目标是让这些数据指标驱动 Flutter 用户体验升级。

页面滑动流畅度

我们先大概了解下屏幕渲染流程:CPU 先把 UI 对象转变 GPU 可以识别的信息存储进 displaylist 列表,GPU 执行绘图指令来执行 displaylist,取出相应的图元信息,进行栅格化渲染,显示到屏幕上,这样一个循环的过程实现屏幕刷新。

闲鱼客户端采用的 Native、Flutter 混合技术方案,Native 页面 FPS 监控采用集团高可用方案,Flutter 页面是否可以直接采用这套方案监控?

普遍的 FPS 检测方案 Android 端采用的是 Choreographer.FrameCallBack,IOS 采用的是 CADisplayLink 注册的回调,原理是类似的,在每次发出 Vsync 信号,并且 CPU 开始计算的时候执行到对应的回调,这个时候表示屏幕开始一次刷新,计算固定时间内屏幕渲染次数来得到 fps。(这种方式只能检测到 CPU 卡顿,对于 GPU 的卡顿是无法监控到的)。由于这两种方法都是在主线程做检测处理,而 flutter 的屏幕绘制是在 UI TaskRunner 中进行,真正的渲染操作是在 GPU TaskRunner 中,关于详细的 Flutter 线程问题可以参考闲鱼之前的文章:深入理解 Flutter 引擎线程模式。

这里我们得出结论:Native 的 FPS 检测方法并不适用于 Flutter。

Flutter 官方给我们提供了 Performance Overlay (具体参考 Flutter performance profiling) 作为检测帧率工具,可否直接拿来用?

上图显示了 Performance Overlay 模式下的帧率统计,可以看到,Flutter 分开计算 GPU 和 UI TaskRunner。UI Task Runner 被 Flutter Engine 用于执行 Dart root isolate 代码,GPU Task Runner 被用于执行设备 GPU 的相关调用。通过对 flutter engine 源码分析,UI frame time 是执行 window.onBeginFrame 所花费的总时间。GPU frame time 是处理 CPU 命令转换为 GPU 命令并发送给 GPU 所花费的时间。

这种方式只能在 debug 和 profile 模式下开启,没有办法作为线上版本的 fps 统计。但是我们可以通过这种方式获得启发,通过监听 Flutter 页面刷新回调方法 handleBeginFrame()、handleDrawFrame() 来计算实际 FPS。

具体实现方式:

注册 WidgetsFlutterBinding 监听页面刷新回调 handleBeginFrame()、handleDrawFrame()

handleBeginFrame: Called by the engine to prepare the framework to produce a new frame.
handleDrawFrame: Called by the engine to produce a new frame.

通过计算 handleBeginFrame 和 handleDrawFrame 之间的时间间隔计算帧率,主要流程如下图:

效果

到这里,我们完成 Flutter 中页面帧率的统计,这种方式统计的是 UI TaskRunner 中的 CPU 操作耗时,GPU 操作在 Flutter 引擎内部实现,要修改引擎来监控完整的渲染耗时,我们目前大部分的场景没有复杂到 gpu 卡顿,问题主要还是集中在 CPU,所以说可以反应出大部分问题。从线上数据来看,release 模式下 Flutter 的流畅度还是蛮不错的,ios 的主要页面均值基本维持在 50fps 以上,android 相对 ios 略低。这里需要注意的是帧率的均值 fps 在反复滑动过程中会有一个稀释效果,导致一些卡顿问题没有暴露出来,所以除了 fps 均值,需要综合掉帧范围、卡顿秒数、滑动时长等数据才能反应出页面流畅度情况。

页面加载时长

集团内部高可用方案统计 Native 页面加载时长是通过容器初始化后开启定时器在容器 layout 的时候检查屏幕渲染程度,计算可见组件的屏幕覆盖率,满足条件水平 >60%,垂直 >80% 以上认为满足页面填充程度,再检查主线程心跳判断是否加载完成

再来看看 weex 页面加载流程和统计数据的定义

Weex 的页面刷新稳定定义:屏幕内 view 渲染完成且 view 树稳定的时间

具体实现:当屏幕内发生 view 的 add/rm 操作时,认为是可交互点, 记录数据。直到没有再发生为止。

在概念上 Flutter 和 weex 的首屏时长和可交互时长并不完全一致,Flutter 之所以选择从路由跳转开始计算时长主要是因为这种计算方式更贴近用户体验,可以获取更多的问题信息,比如路由跳转的时长问题等。

Flutter 的可交互时长 end 点采用的算法与 native 一致,可见组件满足页面填充程度并且完成心跳检查的情况下任务可交互,另外对于一些比较空的页面,组件面积小,无法达到水平 >60%,垂直 >80% 的条件,就用交互前最后一次 Frame 刷新时间点作为 end 点。

具体流程如下图:

效果

由于 debug 模式采用的 JIT 编译,debug 模式下体验加载时长偏长,但是 release 模式下的 AOT 编译时长明显缩短很多,整体页面加载时长还是要优于 weex。

Exception 率

Flutter 部分 exception/error 会导致代码后面的逻辑没有走到造成页面或逻辑 bug,所以 flutter 的 exception 需要作为稳定性的标准之一

定义

FlutterException 率 = exception 发生次数 / flutter 页面 PV

分子:exception 发生次数(已过滤掉白名单)

Flutter 内部 assert、try-catch 和一些异常逻辑的地方会统一调用 FlutterError.onError

通过重定向 FlutterError.onError 到自己的方法中监测 exception 发生次数,并上报 exception 信息

分母:flutter 页面 PV

具体实现如下:

Future<Null> main() async {FlutterError.onError = (FlutterErrorDetails details) async {Zone.current.handleUncaughtError(details.exception, details.stack);
  };

  runZoned<Future<Null>>(() async {runApp(new HomeApp());
  }, onError: (error, stackTrace) async {await _reportError(error, stackTrace);
  });
}

其中,FlutterError.onError 只会捕获 Flutter framework 层的 error 和 exception,官方建议将这个方法按照自己的 exception 捕获上报需求定制。在实践过程中,我们遇到很多不会对用户体验产生任何影响的 exception 会被频繁触发,这类没有改善意义的 exception 可以添加白名单过滤上报。

效果

有了线上 exception 的监控,可以及早发现隐患,获取问题堆栈信息,方便定位 bug,提示整体稳定性

总结

到这里,我们完成 Flutter 页面滑动流畅度、页面加载时长和 Exception 率的统计,对于 Flutter 的性能有一个具体的数字化标准,对以后的用户体验提升和性能问题排查提供基础。目前闲鱼客户端的商品详情页和主发布页已经全量 Flutter 化,感兴趣的同学可以体验下这两个页面和其他页面的性能差异,最后欢迎大家提供反馈和建议。


本文作者:闲鱼技术 - 三莅

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

退出移动版