即将开源|Flutter页面线上性能数据不再是谜

简介: 我要举报这个flutter性能监控太优秀

作者:闲鱼技术-尘萧

今天老板又问你怎么证明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需要有什么特性。

性能指标:
  1. 页面滑动流畅度:传统体现滑动流畅度主要是通过Fps,但是Fps有个问题是无法区分大量的轻微卡顿和少量的严重卡顿,但是对于用户来说显然体感差异是很大的,所以我们同时引入了Fps、滑动时长、掉帧时长来进行衡量是否流畅。
  2. 页面加载耗时:页面加载耗时我们选了更能反映用户体感的可交互时长,可交互时长是指从用户点击发起路由跳转行为开始,到页面内容加载到可以发生交互结束的这一段时长。
  3. Exception:这个指标应该不需要多做解释。
SDK特性:
  1. 准确性:准确性是一个性能监控SDK的基础要求,误报或者错报会导致开发者付出很多不必要的排查时间。
  2. 线上监控:线上监控意味着收集数据时付出的代价不能太大,不能让监控影响到App原本的性能。
  3. 易于拓展:作为一个开源项目,根本目标是希望大家都能参与进来为社区做贡献,所以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。

PerformanceDataCenter

PerformanceEventCenter

单例,设计思路和PerformanceDataCenter类似,但这里是用于接收业务层提供的Event(相应的时机),并将这些时机分发给订阅了他们的Recorder进行处理。Event的种类主要有:(其中业务状态需要使用者提供,其它时机SDK内部已经完成收集)

  • App状态:App前后台切换
  • 页面状态:帧渲染开始、帧渲染结束
  • 业务状态:页面发生Pop/Push、页面发生滑动、业务中发生Exception

见仁见智 - SDK的打开方式

如果你是SDK的使用者,那么你只需要关注API层以及Uploader层,你只需要进行以下几步操作:

  1. 在Pubspec中引用高可用SDK;
  2. 在runApp()方法被调用前,调用init()方法将SDK初始化;
  3. 在你的业务代码中,通过pushEvent()方法给SDK提供一些必要的时机,比如路由的Pop以及Push;
  4. 自定义一个Uploader类,将数据以你希望的格式上报到你所使用的数据收集平台。

如果你希望能为高可用SDK贡献一份力量,那么希望你遵守以下几点设计规范并向我们提出Push Request,我们会及时进行Review并将反馈给到你。

  1. 使用发布-订阅模式,发布者先将数据交给对应的数据中心,再由数据中心分发给相应的订阅者。
  2. 数据流向从Recorder到Processor再到Uploader,通过数据进行驱动,API通过Event驱动Recorder,Recorder通过BaseData驱动Processor,Processor通过UploadData驱动Uploader。

脚踏实地 - SDK的落地情况

我们已经对Flutter高可用SDK进行了多次数据准确度方面的调优,以及很多BadCase的解决,甚至进行过一次颠覆性的重构。至今,SDK已经在闲鱼内稳定运行了将近半年,从第一次接入至今从未出现过因为高可用SDK而引发的稳定性问题,数据收集的准确度也在进行了多次调优后趋近于稳定。

我们利用了手淘EMAS的后台数据处理以及前台数据展示的能力,将高可用SDK线上收集到的数据进行上报和展示,使得Flutter页面可以与Native页面同场竞技。

仰望星空 - SDK的发展计划

功能补充

目前SDK仍有两大问题社区中有需求但没有得到解决:

  1. Flutter内存分析
  2. 卡顿时堆栈的抓取

我们会继续这方面的研究,同时也希望有想法的同学能加入我们并向我们提交代码。

开源计划

目前高可用SDK已经完成了集团内的开源,根据集团内同学接入时的反馈我们对文档进行补充和修改,但是仍然还不够全面,同时测试用例也在紧张的编写当中。在这些都完成之后我们将会进行开源,预计会在两个月内和大家见面。

相关文章
|
3月前
Flutter笔记:使用Flutter构建响应式PC客户端/Web页面-案例
Flutter笔记:使用Flutter构建响应式PC客户端/Web页面-案例
58 0
|
4月前
Flutter StatefulWidget传递数据,多级控件传递数据
Flutter StatefulWidget传递数据,多级控件传递数据 在Flutter中,StatefulWidget可以通过构造函数将数据传递给其子控件,这种方式适用于一些简单的场景。但是,当存在多级嵌套控件时,将数据从祖先传递到后代可能会变得困难。在这种情况下,可以使用Flutter提供的InheritedWidget类来传递数据。
|
10月前
|
UED 索引
Flutter仿写微信导航栏快速实现页面导航
Flutter仿写微信导航栏快速实现页面导航
Flutter实时动态UI刷新、数据交互
Flutter实时动态UI刷新、数据交互
|
11月前
|
缓存 Dart iOS开发
|
11月前
|
缓存 Dart 定位技术
闲鱼技术2022年度白皮书-Flutter主题-节日献礼:Flutter 图片库重磅开源!(中)
闲鱼技术2022年度白皮书-Flutter主题-节日献礼:Flutter 图片库重磅开源!
145 0
|
11月前
|
Swift Kotlin
闲鱼技术2022年度白皮书-Flutter主题-节日献礼:Flutter 图片库重磅开源!(下)
闲鱼技术2022年度白皮书-Flutter主题-节日献礼:Flutter 图片库重磅开源!
121 0
|
缓存 开发者
Flutter 游戏优化性能
提高帧率和减少资源占用是优化 Flutter 游戏性能的重要手段。以下是一些常见的优化技巧以及相关代码实现
Flutter 游戏优化性能
Flutter特别耗性能的组建以及解决方案
Flutter 中使用起来耗性能的组件主要有以下几个: 1. 频繁重绘的组件,如 AnimatedBuilder、AnimatedContainer、AnimatedOpacity 和 AnimatedPositioned 等。 2. 布局复杂的组件,如 Table、Wrap 和 Flow 等,因为它们需要进行大量计算来确定子控件的位置和大小。
Flutter特别耗性能的组建以及解决方案