哈喽,我是老刘
前段时间有朋友咨询我在树莓派上开发的Flutter程序如何优化性能的问题。
老刘写了6年多的Flutter代码,树莓派这种平台还真是头一次碰到。
不过我听他说完他们的场景,我就知道他们大概率是碰到性能问题了。
那么今天就来说说这种极端场景下的性能优化如何进行。
场景描述
我们先来说明一下整个应用的运行场景。
首先,他的应用是运行在树莓派硬件平台上的ubuntu系统中的。
我们知道树莓派的处理器是arm系列处理器,因此其实本质上就是使用Flutter开发一个运行在arm架构cpu的ubuntu平台的应用。
其次,处理器的性能比较差。
树莓派的设计目标不是pc端的应用场景,因此采用的cpu和gpu都是低功耗低性能的型号。
所以其主要特点就是处理器性能比较差。
其实gpu也差,但是这里我们碰到的主要问题还是cpu的问题。
渲染原理
其实在这个场景中,如果是一些逻辑比较简单的页面,比如只是静态的展示一些信息。
即使信息比较多,使用体验也还能够接受。
就是当碰到页面中有大量图表,而且图表还有一定的动态交互的时候,就会有非常明显的卡顿。
我们从Flutter的基本渲染原理来分析卡顿的原因。
其实不仅仅是Flutter,客户端上的非游戏类App的渲染原理是类似的。
UI线程
对Flutter来说,当系统需要渲染一帧图像,就会通知到Flutter框架。
框架就会调用当前页面的build方法。
我们程序员通过Dart语言编写的build方法会计算出当前页面需要渲染的内容。
比如有几个文本、几个按钮、几个图片,它们的位置在哪里,我们可以把这些内容统称为页面布局。
framework将页面布局和其它一些需要渲染的内容整理成layer tree。
上面的步骤都是在UI线程完成的,也就是Dart代码完成的,全部依赖CPU运行。
GPU线程
Layer tree最终会交给引擎进行光栅化。
也就是在GPU线程中将layer tree的每一个layer中的布局信息计算成像素信息。
这部分主要是由skia完成,虽然不是Dart代码了,但是仍然由CPU完成。
GPU
最终Skia生成的像素信息会交给GPU,由GPU形成最终的展示数据并交给显示器展示。
好了,基本的渲染原理讲完了,接下来我们基于渲染原理分析一下卡顿的原因。
性能问题分析
从我们常见的非游戏类APP来看,其实每一帧需要GPU绘制的内容差异都不是很大。
那为什么有的页面卡顿,有的页面流畅呢?
其实主要就是需要CPU计算的这块不同了。
更具体一点分析,如果一个静态页面的内容非常复杂,那么交给skia的layer tree可能比一个动态页面的layer tree还要复杂一些。
那为什么卡顿总在动态页面出现呢?
其实最主要的原因就出在我们写的Dart代码上。
换句话说,大多数常见的卡顿都是UI线程内的优化不到位引起的。
具体来说,当展示一个静态页面时。
第一帧的时候,我们程序员编写的build方法需要大量计算,最终得到整个页面的布局。
所以第一帧会有些延迟。
后面的帧只需要简单判断一下页面是否需要rebuild,发现不需要就没有任何计算工作了。
所以UI线程的工作量很少,也就不会觉得卡顿。
当展示一个动态页面时,页面展示的内容会随着状态不停的变化。
也就是说每一帧都有可能需要rebuild,需要CPU重新计算页面布局。
回到文章开头的例子,当展示一个包含大量图表的动态页面时。
每一帧都需要根据当前鼠标的位置,重新计算图表的内容,比如鼠标指向的位置线条需要高亮等等。
这时如果CPU的运算能力比较差,例如树莓派,就会导致每一帧都因为计算会有延迟,甚至会有丢帧等情况出现。
这就是动态页面卡顿的根本原因。
知道了卡顿原因,接下来我们就可以分析一下优化方案了。
优化方案
1. 减少UI渲染的复杂性
分批加载数据 :
数据和页面元素的懒加载是老生常谈了,也是大家都会用的常规手段。
但是之所以会成为常规手段,就是因为好用啊。
所以别看没啥技术含量,一定先用起来。
简化图表元素 :
尽量减少每个图表中的元素数量(如线条、点、标签等)。
特别是如果页面中同时显示多个图表时,可以尝试让每个图表只呈现关键数据。
对于低性能平台,从产品功能设计上减少页面复杂性是一种取舍。
这是开发和产品的一种博弈,但是低性能平台的成本控制是一个好的切入点。
取消动画 :
图表动画虽然提升视觉效果,但也会消耗大量资源。如果发现卡顿严重,可以减少或禁用复杂动画效果。
避免过度使用透明层 :
透明层次的叠加会显著增加GPU负载,尽量减少不必要的透明图层或阴影效果。
2. 使用低级别的图形API
绘制优化 :
可以使用自定义绘制(例如CustomPainter)来直接绘制需要的图形,这样可以绕过一些性能瓶颈。
SDK提供的各种组件为了其通用性,在功能上是会有一点复杂的。
所以通过底层API来自定义绘制,在一定程度上能减少通用组件一些不必要的损耗。
当然,这是开发成本和运行效率间的一种取舍,也不能简单的算是技术问题了。
考虑使用图表库的轻量级替代品 :
如果你在使用某个第三方图表库,确认该库是否支持较低性能的设备,或者选择一些性能更高、适合嵌入式设备的轻量级图表库。
例如,可以考虑通过平台通道使用Raspberry Pi上更高效的图表库或绘图工具,并通过与Flutter交互来显示结果。
3. 分离计算和UI渲染
后台处理数据 :
复杂的图表通常涉及大量数据计算。
为了避免阻塞主线程的UI渲染,可以将数据计算放到后台(例如使用compute函数或Isolate),这样主线程只负责图表的渲染,数据处理则由独立的线程完成。
其实在低性能平台上即使把计算任务放到后台仍然会有卡顿,因为真的可能是算不过来啊。
但是仍然需要把计算任务放到后台运行,这样可以有效的避免主线程不响应。
也就是说虽然算不过来,会出现内容更新的不及时,但是至少不会整个UI没有响应了。
4. 其它Flutter优化常用技巧
使用RepaintBoundary :
对于不需要频繁刷新的图表,可以将其包裹在RepaintBoundary中,这样可以防止Flutter对这些部分的重复重绘,从而减少GPU的负担。
减少重建组件的频率 :
避免不必要的setState调用,确保只有需要刷新的部分组件才会重新构建,尽量减少无关组件的重建。
比如使用Bloc、Provider这样的状态管理方案,尽可能减少需要重建的组件范围。
避免不必要的ListView/ScrollView嵌套 :
如果页面中有大量数据需要滚动,尽量避免在一个大ListView中嵌套多个小ScrollView,这会增加渲染负担。
总结
前面我们从渲染原理的角度分析了在树莓派这种低性能平台上Flutter程序卡顿的原因。
然后提供了一些优化建议。
这些建议中大部分是相对通用的,比如懒加载、限制重绘范围等,也有一些是针对低性能平台特定的,比如通过底层API自定义绘制、取消动画等。
总的来说 Flutter 已经在各种大型项目中展示出足够的支撑能力。
但是在树莓派这样的低性能平台上还有很大的优化空间。
当然这不仅仅是框架自身的问题,也有包括三方库在内的生态系统需要共同完成的挑战。
如果看到这里的同学有学习Flutter的兴趣,欢迎联系老刘,我们互相学习。
点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
可以作为Flutter学习的知识地图。
覆盖90%开发场景的《Flutter开发手册》