作者: 闲鱼技术 - 尘萧
今天老板又问你怎么证明 Flutter 的性能比 Native 好? Flutter 线上的性能数据到底怎么收集? Flutter 高可用 SDK 在闲鱼上稳定运行了大半年, 我们终于要准备开源啦.
事出有因 - 我们为什么要做 Flutter 高可用 SDK
移动端 APM 其实已经是一个很成熟的命题了, 在 Native 世界这些年的发展中, 曾经诞生过很多用于监控线上性能数据的 SDK. 但是由于 Flutter 相对于 Native 做了很多革命性的改变, 导致 Native 的性能监控在 Flutter 页面上基本全部失效了. 基于这个背景, 我们在去年启动了名为 Flutter 高可用 SDK 的项目, 目的是让 Flutter 页面像 Native 页面一样可以被度量.
有的放矢 - 我们需要一个什么样的 SDK
性能监控既然是一个成熟的命题, 那么意味着我们有着充足的资源可以借鉴. 我们借鉴了包括手淘的 EMAS 高可用, 微信的 Martix, 美团的 Hertz 等性能监控 SDK, 并结合 Flutter 的实际情况我们确定了两个问题, 一个是需要收集什么性能指标, 一个是 SDK 需要有什么特性.
性能指标:
页面滑动流畅度: 传统体现滑动流畅度主要是通过 Fps, 但是 Fps 有个问题是无法区分大量的轻微卡顿和少量的严重卡顿, 但是对于用户来说显然体感差异是很大的, 所以我们同时引入了 Fps, 滑动时长, 掉帧时长来进行衡量是否流畅.
页面加载耗时: 页面加载耗时我们选了更能反映用户体感的可交互时长, 可交互时长是指从用户点击发起路由跳转行为开始, 到页面内容加载到可以发生交互结束的这一段时长.
Exception: 这个指标应该不需要多做解释.
SDK 特性:
准确性: 准确性是一个性能监控 SDK 的基础要求, 误报或者错报会导致开发者付出很多不必要的排查时间.
线上监控: 线上监控意味着收集数据时付出的代价不能太大, 不能让监控影响到 App 原本的性能.
易于拓展: 作为一个开源项目, 根本目标是希望大家都能参与进来为社区做贡献, 所以 SDK 本身要易于拓展, 同时需要一系列的规范来帮助大家进行开发.
见微知著 - 从单个指标看 SDK
2019 年 4 月 25 日, 我们曾经发表了一篇文章, 讲述通过数据驱动 Flutter 体验升级, 文章中详细介绍了 Flutter 的性能指标以及收集方式, 大家可以去翻阅一下之前的文章快速复习一下学过的知识. 我们这里就选择其中比较典型的收集瞬时 Fps 的实现来进行讲解, 并通过这样的形式带大家看一下 SDK 整体的设计.
首先需要实现一个 FpsRecorder, 并继承自 BaseRecorder. 这个类的目的是为了获取到业务层中页面 Pop/Push 的时机以及 FlutterBinding 提供的页面开始渲染, 结束渲染, 发生点击事件的时机, 并通过这些时机来计算出源数据. 对于瞬时 Fps 来说源数据即为每帧时长.
- class FpsRecorder extends BaseRecorder {
- ///...
- @override
- void onReceivedEvent(BaseEvent event) {
- if (event is RouterEvent) {
- ///...
- } else if (event is RenderEvent) {
- switch (event.eventType) {
- case RenderEventType.beginFrame:
- _frameStopwatch.reset();
- _frameStopwatch.start();
- break;
- case RenderEventType.endFrame:
- _frameStopwatch.stop();
- PerformanceDataCenter().push(FrameData(_frameStopwatch.elapsedMicroseconds));
- break;
- }
- } else if (event is UserInputEvent) {
- ///...
- }
- }
- @override
- List<Type> subscribedEventList() {
- return <Type>[RenderEvent, RouterEvent, UserInputEvent];
- }
- }
我们在 beginFrame 时打下开始点, 在 endFrame 时打下结束点, 即可得到每帧的时长. 可以看到我们收集到了每帧时长后, 将其封装为了一个 FrameData 并 push 到了 PerformanceDataCenter 中. PerformanceDataCenter 会将该数据分发给订阅了 FrameData 的 Processor 中, 所以我们需要新建一个 FpsProcessor 订阅并处理这些源数据.
- class FpsProcessor extends BaseProcessor {
- ///...
- @override
- void process(BaseData data) {
- if (data is FrameData) {
- ///...
- if (isFinishedWithSample(currentTime)) {
- /// 当时间间隔大于 1s, 则计算一次 FPS
- _startSampleTime = currentTime;
- collectSample(currentTime);
- }
- }
- }
- @override
- List<Type> subscribedDataList() {
- return [FrameData];
- }
- void collectSample(int finishSampleTime) {
- ///...
- PerformanceDataCenter().push(FpsUploadData(avgFps: fps));
- }
- ///...
- }
FpsProcessor 将获取到的每帧时长收集起来并计算 1s 内的瞬时 Fps 值 (具体的统计方法可以参考上文提到的前一篇文章的实现, 这里不过多的进行描述). 同样的在计算完 Fps 值后, 我们将其封装为了一个 FpsUploadData 并再一次 push 到了 PerformanceDataCenter 中. PerformanceDataCenter 会将 FpsUploadData 交给订阅了它的 Uploader 进行处理, 所以我们需要新建一个 MyUploader 订阅并处理这些数据.
- class MyUploader extends BaseUploader {
- @override
- List<Type> subscribedDataList() {
- return <Type>[
- FpsUploadData, //TimeUploadData, ScrollUploadData, ExceptionUploadData,
- ];
- }
- @override
- void upload(BaseUploadData data) {
- if (data is FpsUploadData) {
- _sendFPS(data.pageInfoData.pageName, data.avgFps);
- }
- ///...
- }
- }
Uploader 可以通过 subscribedDataList() 选择需要订阅的 UploadData, 并通过 upload() 接收 notify 并进行上报. 理论上一个 Uploader 对应一个上传渠道, 使用者可以按需实现如 LocalLogUploader,NetworkUploader 等将数据上报到不同的地方.
纵观全局 - SDK 整体结构设计
SDK 总体可以分为 4 层, 并大量的使用了发布 - 订阅模式利用 2 个 Center 进行连接, 这种模式的好处在于可以使得层与层之间做到完全的解耦, 使得对于数据的处理可以更加灵活多变.
API
这一层中主要是一些对外暴露的接口. 比如 init() 需要使用者在 runApp() 前进行调用, 以及业务层需要调用 pushEvent() 方法给 SDK 提供的一些时机.
Recorder
这一层的主要职责是用 Evnet 所提供的时机进行相应的源数据收集并交给订阅了该数据的 Processor 进行处理. 比如 FPS 采集中的每帧时长即为源数据. 这一层的设计主要是为了使得源数据可以被利用在不同的地方, 比如每帧时长除了用于计算 FPS, 还可以用来计算卡顿秒数.
使用时需要继承 BaseRecoder, 通过 subscribedEventList() 选择订阅的 Event, 在 onReceivedEvent() 中处理接收到的 Event
- abstract class BaseRecorder with TimingObserver {
- BaseRecorder() {
- PerformanceEventCenter().subscribe(this, subscribedEventList());
- }
- }
- mixin TimingObserver {
- void onReceivedEvent(BaseEvent event);
- List<Type> subscribedEventList();
- }
- Processor
这一层主要是将源数据加工为最终可以被上报的数据, 并交给订阅了该数据的 Uploader 进行上报. 比如 FPS 采集中根据收集到的每帧时长进行计算, 得到这一段时间内的 FPS 值.
使用时需要继承 BaseProcessor, 通过 subscribedDataList() 选择订阅的 Data 类型, 在 process() 中对接收到的 Data 进行处理.
- abstract class BaseProcessor{
- void process(BaseData data);
- List<Type> subscribedDataList();
- BaseProcessor(){
- PerformanceDataCenter().registerProcessor(this, subscribedDataList());
- }
- }
- Uploader
这一层主要是由使用者自己去实现, 因为每一位使用者希望将数据上报到的地方都不一样, 所以 SDK 内部会提供相应的基类, 只需要跟随着基类的规范来写, 即可获取到订阅的数据.
使用时需要继承 BaseUploader, 通过 subscribedDataList() 选择订阅的 Data 类型, 在 upload() 中对接收到的 UploadData 进行处理.
- abstract class BaseUploader{
- void upload(BaseUploadData data);
- List<Type> subscribedDataList();
- BaseUploader(){
- PerformanceDataCenter().registerUploader(this, subscribedDataList());
- }
- }
- PerformanceDataCenter
单例, 用于接收 BaseData(源数据) 以及 UploadData(加工后的数据), 并将这些时机分发给订阅了他们的 Processor 和 Uploader 进行处理.
在 BaseProcessor 和 BaseUploader 的构造函数中, 分别调用了 PerformanceDataCenter 的 register 方法进行订阅该操作会把对应的实例存在 PerformanceDataCenter 的两个 Map 中, 这样的数据结构使得一个 DataType 可以对应多个订阅者.
- final Map<Type, Set<BaseProcessor>> _processorMap = <Type, Set<BaseProcessor>>{
- };
- final Map<Type, Set<BaseUploader>> _uploaderMap = <Type, Set<BaseUploader>>{
- };
如下方图中所示, 当调用 PerformanceDataCenter.push() 方法 push 数据时, 会根据 Data 的类型进行分发, 交给所有订阅了该数据类型的 Proceesor/Uploader.
PerformanceEventCenter
单例, 设计思路和 PerformanceDataCenter 类似, 但这里是用于接收业务层提供的 Event(相应的时机), 并将这些时机分发给订阅了他们的 Recorder 进行处理. Event 的种类主要有:(其中业务状态需要使用者提供, 其它时机 SDK 内部已经完成收集)
App 状态: App 前后台切换
页面状态: 帧渲染开始, 帧渲染结束
业务状态: 页面发生 Pop/Push, 页面发生滑动, 业务中发生 Exception
见仁见智 - SDK 的打开方式
如果你是 SDK 的使用者, 那么你只需要关注 API 层以及 Uploader 层, 你只需要进行以下几步操作:
在 Pubspec 中引用高可用 SDK;
在 runApp() 方法被调用前, 调用 init() 方法将 SDK 初始化;
在你的业务代码中, 通过 pushEvent() 方法给 SDK 提供一些必要的时机, 比如路由的 Pop 以及 Push;
自定义一个 Uploader 类, 将数据以你希望的格式上报到你所使用的数据收集平台.
如果你希望能为高可用 SDK 贡献一份力量, 那么希望你遵守以下几点设计规范并向我们提出 Push Request, 我们会及时进行 Review 并将反馈给到你.
使用发布 - 订阅模式, 发布者先将数据交给对应的数据中心, 再由数据中心分发给相应的订阅者.
数据流向从 Recorder 到 Processor 再到 Uploader, 通过数据进行驱动, API 通过 Event 驱动 Recorder,Recorder 通过 BaseData 驱动 Processor,Processor 通过 UploadData 驱动 Uploader.
脚踏实地 - SDK 的落地情况
我们已经对 Flutter 高可用 SDK 进行了多次数据准确度方面的调优, 以及很多 BadCase 的解决, 甚至进行过一次颠覆性的重构. 至今, SDK 已经在闲鱼内稳定运行了将近半年, 从第一次接入至今从未出现过因为高可用 SDK 而引发的稳定性问题, 数据收集的准确度也在进行了多次调优后趋近于稳定.
我们利用了手淘 EMAS 的后台数据处理以及前台数据展示的能力, 将高可用 SDK 线上收集到的数据进行上报和展示, 使得 Flutter 页面可以与 Native 页面同场竞技.
仰望星空 - SDK 的发展计划
功能补充
目前 SDK 仍有两大问题社区中有需求但没有得到解决:
Flutter 内存分析
卡顿时堆栈的抓取
我们会继续这方面的研究, 同时也希望有想法的同学能加入我们并向我们提交代码.
开源计划
目前高可用 SDK 已经完成了集团内的开源, 根据集团内同学接入时的反馈我们对文档进行补充和修改, 但是仍然还不够全面, 同时测试用例也在紧张的编写当中. 在这些都完成之后我们将会进行开源, 预计会在两个月内和大家见面.
阅读原文
来源: http://www.jianshu.com/p/0d006fb3666d