前言
Flutter 作为目前最火爆的移动端跨平台框架,能够帮助开发者通过一套代码库高效地构建多平台的精美应用,并支持移动、Web、桌面和嵌入式平台。对于 Android 来说,Flutter 能够创作媲美原生的高性能应用,但是,在较为复杂的 App 中,使用 Flutter 开发也很难避免产生各种各样的性能问题。在这篇文章中,我将和你一起全方位地深入探索 Flutter 性能优化的疆域。
一、检测手段
准备
以 profile 模式启动应用,如果是混合 Flutter 应用,在 flutter/packages/flutter_tools/gradle/flutter.gradle 的 buildModeFor 方法中将 debug 模式改为 profile即可。
为什么要在分析模式下来调试应用性能?
分析模式在发布模式的基础之上,为分析工具提供了少量必要的应用追踪信息。
那,为什么要在发布模式的基础上来调试应用性能?
与调试代码可以在调试模式下检测 Bug 不同,性能问题需要在发布模式下使用真机进行检测。这是因为,相比发布模式而言,调试模式增加了很多额外的检查(比如断言),这些检查可能会耗费很多资源,而更重要的是,调试模式使用 JIT 模式运行应用,代码执行效率较低。这就使得调试模式运行的应用,无法真实反映出它的性能问题。
而另一方面,模拟器使用的指令集为 x86,而真机使用的指令集是 ARM。这两种方式的二进制代码执行行为完全不同,因此,模拟器与真机的性能差异较大,例如,针对一些 x86 指令集擅长的操作,模拟器会比真机快,而另一些操作则会比真机慢。这也同时意味着,你无法使用模拟器来评估真机才能出现的性能问题。
1、Flutter Inspector
Flutter Inspector有很多功能,但你应该把注意力花在更有用的功能学习上,例如:“Select Widget Mode” 和 “Repaint Rainbow”。
Select Widget Mode
点击 “Select Widget Mode” 图标,可以在手机上查看当前页面的布局框架与容器类型。
作用
快速查看陌生页面的布局实现方式。
Repaint Rainbow
点击 “Repaint Rainbow” 图标,它会 为所有 RenderBox 绘制一层外框,并在它们重绘时会改变颜色。
作用
帮你找到 App 中频繁重绘导致性能消耗过大的部分。
例如:一个小动画可能会导致整个页面重绘,这个时候使用 RepaintBoundary Widget 包裹它,可以将重绘范围缩小至本身所占用的区域,这样就可以减少绘制消耗。
使用场景
例如 页面的进度条动画刷新时会导致整个布局频繁重绘。
缺点
使用 RepaintBoundary Widget 会创建额外的绘制画布,这将会增加一定的内存消耗。
2、性能图层
性能图层会在当前应用的最上层,以 Flutter 引擎自绘的方式展示 Raster 与 UI 线程的执行图表,而其中每一张图表都代表当前线程最近 300 帧的表现,如果 UI 产生了卡顿(跳帧),这些图表可以帮助你分析并找到原因。
蓝色垂直的线条表示已执行的正常帧,绿色的线条代表的是当前帧,如果其中有一帧处理时间过长,就会导致界面卡顿,图表中就会展示出一个红色竖条。
**如果红色竖条出现在 GPU 线程图表,意味着渲染的图形太复杂,导致无法快速渲染;而如果是出现在了 UI 线程图表,则表示 Dart 代码消耗了大量资源,需要优化代码的执行时间。**如下图所示:
3、Raster 线程问题定位
它定位的是 渲染引擎底层渲染的异常。
解决方案是 把需要静态缓存的图像加入到 RepaintBoundary。而 RepaintBoundary 可以确定 Widget 树的重绘边界,如果图像足够复杂,Flutter 引擎会自动将其缓存,避免重复刷新。当然,因为缓存资源有限,如果引擎认为图像不够复杂,也可能会忽略 RepaintBoundary。
4、UI 线程问题定位
问题场景
在视图构建时,在 build 方法中使用了一些复杂的运算,或是在主 Isolate 中进行了同步的 I/O 操作。
使用 Performance 进行检测
点击 Android Studio 底部工具栏中的 “Open DevTools” 按钮,然后在打开的 Dart DevTools 网页中将顶部的 tab 切换到 Performance。
与性能图层能够自动记录应用执行的情况不同,使用 Performance 来分析代码执行轨迹,你需要手动点击 “Record” 按钮去主动触发,在完成信息的抽样采集后,点击 “Stop” 按钮结束录制。这时,你就可以得到在这期间应用的执行情况了。
使用 Performance 记录应用的执行情况,即 CPU 帧图,又被称为火焰图。火焰图是基于记录代码执行结果所产生的图片,用来展示 CPU 的调用栈,表示的是 CPU 的繁忙程度。
其中:
y 轴:表示调用栈,其每一层都是一个函数。调用栈越深,火焰就越高,底部就是正在执行的函数,上方都是它的父函数。
x 轴:表示单位时间,一个函数在 x 轴占据的宽度越宽,就表示它被采样到的次数越多,即执行时间越长。
所以,我们要 检测 CPU 耗时问题,皆可以查看火焰图底部的哪个函数占据的宽度最大。只要有 “平顶”,就表示该函数可能存在性能问题。如下图所示:
一般的耗时问题,我们通常可以 使用 Isolate(或 compute)将这些耗时的操作挪到并发主 Isolate 之外去完成。
dart 的单线程执行异步任务是怎么实现的?
网络调用的执行是由操作系统提供的另外的底层线程做的,而在 event queue 里只会放一个网络调用的最终执行结果(成功或失败)和响应执行结果的处理回调。
5、使用 checkerboardOffscreenLayers 检查多视图叠加的视图渲染
只要在 MaterialApp 的初始化方法中,将 checkerboardOffscreenLayers 开关设置为 true,分析工具就会自动帮你检测多视图叠加的情况。
这时,使用了 saveLayer 的 Widget 会自动显示为棋盘格式,并随着页面刷新而闪烁。
而 saveLayer 一般会通过一些功能性 Widget,在涉及需要剪切或半透明蒙层的场景中间接地使用。
6、使用 checkerboardRasterCacheImages 检查缓存的图像
它也是用来检测在界面重绘时频繁闪烁的图像(即没有静态缓存)。解决方案是把需要静态缓存的图像加入到 RepaintBoundary。
二、关键优化指标
1、页面异常率
页面异常率,即 页面渲染过程中出现异常的概率。
它度量的是页面维度下功能不可用的情况,其统计公式为:
页面异常率 = 异常发生次数 / 整体页面 PV 数。
统计异常发生次数
利用 Zone 与 FlutterError 这两个方法,然后在异常拦截的方法中,去累计异常的发生次数。
统计整体页面 PV 数
继承自 NavigatorObserver 的观察者,并在其 didPush 方法中,去累加页面的打开次数。
2、页面帧率
Flutter 在全局 Window 对象上提供了帧回调机制。我们可以在 Window 对象上注册 onReportTimings 方法,将最近绘制帧耗费的时间(即 FrameTiming),以回调的形式告诉我们。
有了每一帧的绘制时间后,我们就可以计算 FPS 了。
为了让 FPS 的计算更加平滑,我们需要保留最近 25 个 FrameTiming 用于求和计算。
由于帧的渲染是依靠 VSync 信号驱动的,如果帧绘制的时间没有超过 16.67 ms,我们也需要把它当成 16.67 ms 来算,因为绘制完成的帧必须要等到下一次 VSync 信号来了之后才能渲染。而如果帧绘制时间超过了 16.67 ms,则会占用后续 VSync 的信号周期,从而打乱后续的绘制次序,产生卡顿现象。
那么,页面帧率的统计公式就是:
FPS = 60 * 实际渲染的帧数 / 本来应该在这个时间内渲染完成的帧数。
首先,定义一个容量为 25 的列表,用于存储最近的帧绘制耗时 FrameTiming。
然后,在 FPS 的计算函数中,你再将列表中每帧绘制时间与 VSync 周期 frameInterval 进行比较,得出本来应该绘制的帧数。
最后,两者相除就得到了 FPS 指标。
3、页面加载时长
页面加载时长 = 页面可见的时间 - 页面创建的时间(包括网络加载时长)
统计页面可见的时间
WidgetsBinding 提供了单次 Frame 回调的 addPostFrameCallback 方法,它会在当前 Frame 绘制完成之后进行回调,并且只会回调一次。一旦监听到 Frame 绘制完成回调后,我们就可以确认页面已经被渲染出来了,因此我们可以借助这个方法去获取页面的渲染完成时间 endTime。
统计页面创建的时间
获取页面创建的时间比较容易,我们只需要在页面的初始化函数 initState() 里记录页面的创建时间 startTime。
最后,再将这两个时间做减法,你就能得到页面的加载时长。
需要注意的是,正常的页面加载时长一般都不应该超过2秒。如果超过了,则意味着有严重的性能问题。
三、布局加载优化
Flutter 为什么要使用声明书 UI 的编写方式?
为了减轻开发人员的负担,无需编写如何在不同的 UI 状态之间进行切换的代码,Flutter 使用了声明式的 UI 编写方式,而不是 Android 和 iOS 中的命令式编写方式。
这样的话,当用户界面发生变化时,Flutter 不会修改旧的 Widget 实例,而是会构造新的 Widget 实例。
Fluuter 框架使用 RenderObjects 管理传统 UI 对象的职责(比如维护布局的状态)。 RenderObjects 在帧之间保持不变, Flutter 的轻量级 Widget 通知框架在状态之间修改 RenderObjects, 而 Flutter Framework 则负责处理其余部分。
1、常规优化
常规优化即针对 build() 进行优化,build() 方法中的性能问题一般有两种:耗时操作和 Widget 堆叠。
1)、在 build() 方法中执行了耗时操作
我们应该尽量避免在 build() 中执行耗时操作,因为 build() 会被频繁地调用,尤其是当 Widget 重建的时候。
此外,我们不要在代码中进行阻塞式操作,可以将文件读取、数据库操作、网络请求等通过 Future 来转换成异步方式来完成。
最后,对于 CPU 计算频繁的操作,例如图片压缩,可以使用 isolate 来充分利用多核心 CPU。
**isolate 作为 Flutter 中的多线程实现方式,之所以被称之为 isolate(隔离),是因为每一个 isolate 都有一份单独的内存。
Flutter 会运行一个事件循环,它会从事件队列中取得最旧的事件,处理它,然后再返回下一个事件进行处理,依此类推,直到事件队列清空为止。每当动作中断时,线程就会等待下一个事件。*
实质上,不仅仅是 isolate,所有的高级 API 都能够应用于异步编程,例如 Futures、Streams、async 和 await,它们全部都是构建在这个简单的事件循环之上。
而,async 和 await 实际上只是使用 futures 和 streams 的替代语法,它将代码编写形式从异步变为同步,主要用来帮助你编写更清晰、简洁的代码。
此外,async 和 await 也能使用 try on catch finally 来进行异常处理,这能够帮助你处理一些数据解析方面的异常。
2)、build() 方法中堆砌了大量的 Widget
这将会导致三个问题:
1、代码可读性差:画界面时需要一个 Widget 嵌套一个 Widget,但如果 Widget 嵌套太深,就会导致代码的可读性变差,也不利于后期的维护和扩展。
2、复用难:由于所有的代码都在一个 build(),会导致无法将公共的 UI 代码复用到其它的页面或模块。
3、影响性能:我们在 State 上调用 setState() 时,所有 build() 中的 Widget 都将被重建,因此 build() 中返回的 Widget 树越大,那么需要重建的 Widget 就越多,也就会对性能越不利。
所以,你需要 控制 build 方法耗时,将 Widget 拆小,避免直接返回一个巨大的 Widget,这样 Widget 会享有更细粒度的重建和复用。
3)、使用 Widget 而不是函数
如果一个函数可以做同样的事情,Flutter 就不会有 StatelessWidget ,使用 StatelessWidget 的最大好处在于:能尽量避免不必要的重建。总的来说,它的优势有:
1)、允许性能优化:const 构造函数,更细粒度的重建等等。
2)、确保在两个不同的布局之间切换时,能够正确地处理资源(因为函数可能重用某些先前的状态)。
3)、确保热重载正常工作,使用函数可能会破坏热重载。
4)、在 flutter 自带的 Widget 显示工具中能看到 Widget 的状态和参数。
5)、发生错误时,有更清晰的提示:此时,Flutter 框架将为你提供当前构建的 Widget 名称,更容易排查问题。
6)、可以定义 key 和方便使用 context 的 API。
4)、尽可能地使用 const
如果某一个实例已经用 const 定义好了,那么其它地方再次使用 const 定义时,则会直接从常量池里取,这样便能够节省 RAM。
5)、尽可能地使用 const 构造器
当构建你自己的 Widget 或者使用 Flutter 的 Widget 时,这将会帮助 Flutter 仅仅去 rebuild 那些应当被更新的 Widget。
因此,你应该尽量多用 const 组件,这样即使父组件更新了,子组件也不会重新进行 rebuild 操作。特别是针对一些长期不修改的组件,例如通用报错组件和通用 loading 组件等。
6)、使用 nil 去替代 Container() 和 SizedBox()
首先,你需要明白 nil 仅仅是一个基础的 Widget 元素 ,它的构建成本几乎没有。
在某些情况下,如果你不想显示任何内容,且不能返回 null 的时候,你可能会返回类似 const SizedBox/Container 的 Widget,但是 SizedBox 会创建 RenderObject,而渲染树中的 RenderObject 会带来多余的生命周期控制和额外的计算消耗,即便你没有给 SizedBox 指定任何的参数。
下面,是我平时使用 nil 的一套方式:
// BEST
text != null ? Text(text) : nil
or
if (text != null) Text(text)
text != null ? Text(text) : const Container()/SizedBox()
7)、列表优化
在构建大型网格或列表的时候,我们要尽量避免使用 ListView(children: [],) 或 GridView(children: [],)。
因为,在这种场景下,不管列表内容是否可见,会导致列表中所有的数据都会被一次性绘制出来,这种用法类似于 Android 的 ScrollView。
如果我们列表数据比较大的时候,建议使用 ListView 和 GridView 的 builder 方法,它们只会绘制可见的列表内容,类似于 Android 的 RecyclerView。
其实,本质上,就是对列表采用了懒加载而不是直接一次性创建所有的子 Widget,这样视图的初始化时间就减少了。
8)、针对于长列表,记得在 ListView 中使用 itemExtent。
有时候当我们有一个很长的列表,想要用滚动条来大跳时,使用 itemExtent 就很重要了,它会帮助 Flutter 去计算 ListView 的滚动位置而不是计算每一个 Widget 的高度,与此同时,它能够使滚动动画有更好的性能。
9)、减少可折叠 ListView 的构建时间
针对于可折叠的 ListView,未展开状态时,设置其 itemCount 为 0,这样 item 只会在展开状态下才进行构建,以减少页面第一次的打开构建时间。
10)、尽量避免saveLayer操作
调用 saveLayer() 会开辟一片离屏缓冲区。将内容绘制到离屏缓冲区可能会触发渲染目标切换,这些切换在较早期的 GPU 中特别慢。
有两个方法可以检查页面是否使用saveLayer:
1.可以使用在MeterialApp中时使用checkerboardOffscreenLayers属性开关来检查当前界面是否使用了saveLayer,打开开关之后,运行应用并检查是否有图像的轮廓闪烁。如果有新的帧渲染的话,容器就会闪烁;
2.使用flutter screenshot --type=skia --observatory-url=这里填timeline的观察台地址,生成skp文件,再上传到 debugger.skia.org/,就可详细分析页面中的saveLayer的调用,推荐该方法,第一种方法有的saveLayer无法检查出来。
在官网中可以看到,以下组件会触发saveLayer,应尽量避免使用,寻找其他代替。透明度(Opacity)、裁剪(clipping)、阴影(shadows)以及文字(Text)
2、深入优化
1)、优化光栅线程
所有的 Flutter 应用至少都会运行在两个并行的线程上:UI 线程和 Raster 线程。
UI 线程是你构建 Widgets 和运行应用逻辑的地方。 Raster 线程是 Flutter 用来栅格化你的应用的。它从 UI 线程获取指令并将它们转换为可以发送到图形卡的内容。
在光栅线程中,会获取图片的字节,调整图像的大小,应用透明度、混合模式、模糊等等,直到产生最后的图形像素。然后,光栅线程会将其发送到图形卡,继而发送到屏幕上显示。
使用 Flutter DevTools-Performance 进行检测,步骤如下:
1、在 Performance Overlay 中,查看光栅线程和 UI 线程哪个负载过重。
2、在 Timeline Events 中,找到那些耗费时间最长的事件,例如常见的 SkCanvas::Flush,它负责解决所有待处理的 GPU 操作。
3、找到对应的代码区域,通过删除 Widgets 或方法的方式来看对性能的影响。
2)、用 key 加速 Flutter 的性能优化光栅线程
一个 element 是由 Widget 内部创建的,它的主要目的是,知道对应的 Widget 在 Widget 树中所处的位置。但是元素的创建是非常昂贵的,通过 Keys(ValueKeys 和 GlobalKeys),我们可以去重复使用它们。
GlobalKey 与 ValueKey 的区别?
GlobalKey 是全局使用的 key,在跨小部件的场景时,你就可以使用它去刷新其它小部件。但,它是很昂贵的,如果你不需要访问 BuildContext、Element 和 State,应该尽量使用 LocalKey。
而 ValueKey 和 ObjectKey、UniqueKey 一样都归属于局部使用的 LocalKey,无法跨容器使用,ValueKey 比较的是 Widget 的值,而 ObjectKey 比较的是对象的 key,UniqueKey 则每次都会生成一个不同的值。
元素的生命周期
Mount:挂载,当元素第一次被添加到树上的时候调用。
Active:当需要激活之前失活的元素时被调用。
Update:用新数据去更新 RenderObject。
Deactive:当元素从 Widget 树中被移除或移动时被调用。如果一个元素在同一帧期间被移动了且它有 GlobalKey,那么它仍然能够被激活。
UnMount:卸载,如果一个元素在一帧期间没有被激活,它将会被卸载,并且再也不会被复用。
优化方式
**为了去改善性能,你需要去尽可能让 Widget 使用 Activie 和 Update 操作,并且尽量避免让 Widget触发 UnMount 和 Mount。**而使用 GlobayKeys 和 ValueKey 则能做到这一点:
/// 1、给 MaterialApp 指定 GlobalKeys
MaterialApp(key: global, home: child,);
/// 2、通过把 ValueKey 分配到正在被卸载的根 Widget,你就能够
/// 减少 Widget 的平均构建时间。
Widget build(BuildContext context) {
return Column(
children: [
value
? const SizedBox(key: ValueKey('SizedBox'))
: const Placeholder(key: ValueKey('Placeholder')),
GestureDetector(
key: ValueKey('GestureDetector'),
onTap: () {
setState(() {
value = !value;
});
},
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
!value
? const SizedBox(key: ValueKey('SizedBox'))
: const Placeholder(key: ValueKey('Placeholder')),
],
);
}
如何知道哪些 Widget 会被 Update,哪些 Widget会被 UnMount?
只有 build 直接 return 的那个根 Widget 会自动更新,其它都有可能被 UnMount,因此都需要给其分配 ValueKey。
为什么没有给 Container 分配 ValueKey?
因为 Container 是 GestureDetector 的一个子 Widget,所以当给 GestureDetector 使用 ValueKey 去实现复用更新时,Container 也能被自动更新。
优化效果
优化前:
优化后:
可以看到,平均构建时间 由 5.5ms 减少到 1.6ms,优化效果还是很明显的。
优势
大幅度减少 Widget的平均构建时间。
缺点
**过多使用 ValueKey 会让你的代码变得更冗余。
如果你的根 Widget 是 MaterialApp 时,则需要使用 GlobalKey,但当你去重复使用 GlobalKey 时可能会导致一些错误,所以一定要避免滥用 Key。
注意📢:在大部分场景下,Flutter 的性能都是足够的,不需要这么细致的优化,只有当产生了视觉上的问题,例如卡顿时才需要去分析优化。
四、启动速度优化
1、Flutter 引擎预加载
使用它可以达到页面秒开的一个效果,具体实现为:
在 HIFlutterCacheManager 类中定义一个 preLoad 方法,使用 Looper.myQueue().addIdleHandler 添加一个 idelHandler,当 CPU 空闲时会回调 queueIdle 方法,在这个方法里,你就可以去初始化 FlutterEngine,并把它缓存到集合中。
预加载完成之后,你就可以通过 HIFlutterCacheManager 类的 getCachedFlutterEngine 方法从集合中获取到缓存好的引擎。
2、Dart VM 预热
对于 Native + Flutter 的混合场景,如果不想使用引擎预加载的方式,那么要提升 Flutter 的启动速度也可以通 过Dart VM 预热来完成,这种方式会提升一定的 Flutter 引擎加载速度,但整体对启动速度的提升没有预加载引擎提升的那么多。
无论是引擎预加载还是 Dart VM 预热都是有一定的内存成本的,如果 App 内存压力不大,并且预判用户接下来会访问 Flutter 业务,那么使用这个优化就能带来很好的价值;反之,则可能造成资源浪费,意义不大。
五、内存优化
1、const 实例化
优势
**const 对象只会创建一个编译时的常量值。在代码被加载进 Dart Vm 时,在编译时会存储在一个特殊的查询表里,由于 flutter 采用了 AoT 编译,const + values 的方式会提供一些小的性能优势。**例如:const Color() 仅仅只分配一次内存给当前实例。
应用场景
Color()、GlobayKey() 等等。
2、识别出消耗多余内存的图片
Flutter Inspector:点击 “Invert Oversized Images”,它会识别出那些解码大小超过展示大小的图片,并且系统会将其倒置,这些你就能更容易在 App 页面中找到它。
针对这些图片,你可以指定 cacheWidth 和 cacheHeight 为展示大小,这样可以让 flutter 引擎以指定大小解析图片,减少内存消耗。
3、针对 ListView item 中有 image 的情况来优化内存
ListView 不能够杀死那些在屏幕可视范围之外的那些 item,如果 item 使用了高分辨率的图片,那么它将会消耗非常多的内存。
换言之,ListView 在默认情况下会在整个滑动/不滑动的过程中让子 Widget 保持活动状态,这一点是通过 AutomaticKeepAlive 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,以使被包裹的子 Widget 保持活跃。
其次,如果用户向后滚动,则不会再次重新绘制子 Widget,这一点是通过 RepaintBoundaries 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,它会让被包裹的子 Widget 仅仅绘制一次,以此获得更高的性能。
但,这样的问题在于,如果加载大量的图片,则会消耗大量的内存,最终可能使 App 崩溃。
解决方案
通过将这两个选项置为 false 来禁用它们,这样不可见的子元素就会被自动处理和 GC。
ListView.builder(
...
addAutomaticKeepAlives: false (true by default)
addRepaintBoundaries: false (true by default)
);
由于重新绘制子元素和管理状态等操作会占用更多的 CPU 和 GPU 资源,但是它能够解决你 App 的内存问题,并且会得到一个高性能的视图列表。
六、包体积优化
1、图片优化
对图片压缩或使用在线的网络图片。
2、移除冗余的二三库
随着业务的增加,项目中会引入越来越多的二三方库,其中有不少是功能重复的,甚至是已经不再使用的。移除不再使用的和将相同功能的库进行合并可以进一步减少包体积。
3、启用代码缩减和资源缩减
打开 minifyEnabled 和 shrinkResources,构建出来的 release 包会减少 10% 左右的大小,甚至更多。
4、构建单 ABI 架构的包
目前手机市场上,x86 / x86_64/armeabi/mips / mips6 的占有量很少,arm64-v8a 作为最新一代架构,是目前的主流,而 armeabi-v7a 只存在少部分的老旧手机中。
所以,为了进一步优化包大小,你可以构建出单一架构的安装包,在 Flutter 中可以通过以下方式来构建出单一架构的安装包:
cd <flutter应用的android目录>
flutter build apk --split-per-abi
如果想进一步压缩包体积可将 so 进行动态下发,将 so 放在远端进行动态加载,不仅能进一步减少包体积也可以实现代码的热修复和动态加载。
七、总结
在本篇文章中,我主要从以下 六个方面 讲解了 Flutter 性能优化相关的知识:
1)、检测手段:Flutter Inspector、性能图层、Raster 和 UI 线程问题的定位 使用 checkerboardOffscreenLayers 检查多视图叠加的视图渲染 、使用 checkerboardRasterCacheImages 检查缓存的图像。
2)、关键优化指标:包括页面异常率、页面帧率、页面加载时长。
3)、布局加载优化:十大常规优化、优化光栅线程、用 key 加速 Flutter 的性能。
4)、启动速度优化:引擎预加载和 Dart VM 预热。
5)、内存优化:const 实例化、识别出消耗多余内存的图片、针对 ListView item 中有 image 的情况来优化内存。
6)、包体积优化:图片优化、移除冗余的二三库、启用代码缩减和资源缩减、构建单 ABI 架构的包。
在近一年实践 Flutter 的过程中,越发发现一个人真正应该具备的核心能力应该是你的思考能力。
flutter性能优化大全
Flutter是一款高性能、高质量的移动开发框架,但在实际开发中,为了保证应用的流畅性和响应速度,仍然需要进行一些性能优化。下面是一些常见的Flutter性能优化技巧:
- 减少Widget重建:Widget重建是Flutter应用中的一个常见性能问题,因为它会导致不必要的渲染和绘制。减少Widget重建的方法包括使用const构造函数、使用Key标识Widget、使用StatefulWidget等。
- 避免过多的UI重绘:避免过多的UI重绘可以提高应用的性能。可以使用shouldRepaint方法来判断是否需要重绘。
- 优化图片加载:在Flutter中,图片加载是一个常见的性能问题。可以使用缓存或预加载技术来优化图片加载,以减少不必要的网络请求。
- 避免过多的网络请求:过多的网络请求会导致应用响应速度变慢。可以使用缓存或者减少请求次数的方法来减少网络请求,从而提高应用的性能。
- 优化布局:布局是应用性能的重要因素之一。可以使用Flex布局或者使用CustomMultiChildLayout等方法来优化布局,以提高应用的性能。
- 使用异步操作:在应用中使用异步操作可以避免UI卡顿的问题。可以使用Future、Stream等异步操作来优化应用的性能。
- 避免过多的内存使用:过多的内存使用会导致应用响应速度变慢。可以使用Flutter自带的内存分析工具来查找内存泄漏问题,从而避免过多的内存使用。
- 使用热重载:热重载是Flutter的一个重要特性,它可以快速预览和调试应用。使用热重载可以提高开发效率,从而加速应用的开发过程。
Flutter的性能优化技巧包括减少Widget重建、避免过多的UI重绘、优化图片加载、避免过多的网络请求、优化布局、使用异步操作、避免过多的内存使用、使用热重载等。需要根据实际应用场景进行选择和应用。
减少Widget重建
1. 减少Widget重建是Flutter中的一个重要性能优化技巧,以下是一些例子:
使用const构造函数创建常量Widget:
class MyWidget extends StatelessWidget {
final String text;
const MyWidget({Key key, this.text}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(text);
}
}
在上面的例子中,使用了const构造函数来创建常量Widget,这样可以避免不必要的Widget重建。
2. 使用Key标识Widget:
class MyWidget extends StatelessWidget {
final String text;
const MyWidget({Key key, this.text}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(text, key: key);
}
}
使用Key标识Widget,这样可以避免在Widget重建时被误认为是不同的Widget。
3. 使用StatefulWidget管理状态:
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Text('Count: $_count');
}
}
使用StatefulWidget来管理计数器的状态,这样可以避免整个Widget重建。
4. 使用Builder构建子Widget:
class MyWidget extends StatelessWidget {
final int count;
const MyWidget({Key key, this.count}) : super(key: key);
@override
Widget build(BuildContext context) {
return Builder(builder: (context) {
return Text('Count: $count');
});
}
}
使用Builder构建子Widget,这样可以避免整个Widget重建。
减少Widget重建是Flutter中的一个重要性能优化技巧,可以通过使用const构造函数、Key标识Widget、StatefulWidget、Builder等方法来实现。需要根据实际应用场景进行选择和应用。
避免过多的UI重绘
避免过多的UI重绘是Flutter中的一个重要性能优化技巧,以下是一些常用的优化例子:
2. 使用shouldRepaint方法:
class MyPainter extends CustomPainter {
int count;
MyPainter(this.count);
@override
void paint(Canvas canvas, Size size) {
// 绘制操作
}
@override
bool shouldRepaint(MyPainter oldDelegate) {
return count != oldDelegate.count;
}
}
使用shouldRepaint方法来判断是否需要重绘。当count发生变化时,重绘Canvas。
2. 使用ClipRect避免不必要的绘制:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ClipRect(
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
);
}
}
使用ClipRect来限制Container的绘制范围,以避免不必要的绘制。
3. 使用Offstage避免不必要的布局计算:
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool _visible = true;
@override
Widget build(BuildContext context) {
return Column(
children: [
RaisedButton(
child: Text(_visible ? 'Hide' : 'Show'),
onPressed: () {
setState(() {
_visible = !_visible;
});
},
),
Offstage(
offstage: !_visible,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
],
);
}
}
使用Offstage来避免当Container不可见时的布局计算。
4. 使用RepaintBoundary避免重复绘制:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: Container(
width: 100,
height: 100,
child: CustomPaint(
painter: MyPainter(),
),
),
);
}
}
使用RepaintBoundary来避免重复绘制相同的CustomPaint。
避免过多的UI重绘是Flutter中的一个重要性能优化技巧,可以通过使用shouldRepaint方法、ClipRect、Offstage、RepaintBoundary等方法来实现。
优化图片加载
优化图片加载是Flutter中的一个重要性能优化技巧,以下是一些常用的优化方案:
1. 使用缓存技术:
Flutter中提供了ImageCache类来实现图片缓存,当图片被加载后,会将图片缓存到内存中,下次再次加载同样的图片时,就可以直接从缓存中获取,避免了不必要的网络请求。使用ImageCache类可以通过以下代码实现:
ImageCache imageCache = PaintingBinding.instance.imageCache;
imageCache.maximumSize = 100; // 设置缓存的最大大小
2. 预加载图片:
对于一些常用的图片,可以在应用启动时预加载,这样可以避免在使用时才进行加载,从而提高应用的响应速度。可以通过以下代码实现:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
precacheImage(NetworkImage('https://example.com/my-image.jpg'), context);
return MaterialApp(
// ...
);
}
}
3. 压缩图片:
对于一些较大的图片,可以使用压缩技术来减少图片大小,从而减少网络传输时间。可以使用dart-image库来实现图片压缩,例如:
import 'package:image/image.dart' as img;
import 'dart:io';
File compressedImage(File imageFile) {
img.Image image = img.decodeImage(imageFile.readAsBytesSync());
img.Image smallerImage = img.copyResize(image, width: 800, height: 600);
return File(imageFile.path)
..writeAsBytesSync(img.encodeJpg(smallerImage, quality: 70));
}
4. 使用占位图:
对于一些网络加载较慢的图片,可以使用占位图来填充,以提高用户体验。可以使用Flutter自带的FadeInImage组件来实现占位图,例如:
FadeInImage.assetNetwork(
placeholder: 'assets/placeholder.png',
image: 'https://example.com/my-image.jpg',
);
优化图片加载是Flutter中的一个重要性能优化技巧,可以通过使用缓存。
避免过多的网络请求
避免过多的网络请求是Flutter中的一个重要性能优化技巧,以下是一些常用的优化方案:
1. 使用缓存技术:
对于一些重复请求的数据,可以使用缓存技术来避免过多的网络请求。可以使用Flutter自带的dio库或者像Hive、sqflite这样的第三方库来实现缓存,例如:
Dio dio = Dio();
var cacheManager = CacheManager(
Config(
'my_cache_key',
stalePeriod: const Duration(days: 1),
maxNrOfCacheObjects: 20,
),
);
dio.interceptors.add(DioCacheInterceptor(
options: CacheOptions(
store: cacheManager,
policy: CachePolicy.requestFirst,
hitCacheOnErrorExcept: [401, 403],
),
));
2. 减少请求次数:
对于一些重复请求的数据,可以使用分页或者滚动加载等方式来减少请求次数,从而避免过多的网络请求。例如,在一个列表中使用分页加载的方式来减少网络请求次数。
合并请求:
对于一些需要同时请求多个接口的场景,可以将多个请求合并成一个请求,从而减少网络请求次数。可以使用Flutter自带的dio库或者像chopper这样的第三方库来实现请求合并,例如:
final chopper = ChopperClient(
baseUrl: 'https://api.github.com',
services: [
GithubApiService.create(),
],
converter: JsonConverter(),
interceptors: [
HttpLoggingInterceptor(),
HeadersInterceptor({'User-Agent': 'Chopper'}),
例如,在GithubApiService中定义多个请求方法:
part 'github_api_service.chopper.dart';
@ChopperApi(baseUrl: '/users')
abstract class GithubApiService extends ChopperService {
static GithubApiService create([ChopperClient client]) => _$GithubApiService(client);
@Get(path: '/{username}')
Future<Response> getUser(@Path('username') String username);
@Get(path: '/{username}/repos')
Future<Response> getUserRepos(@Path('username') String username);
}
然后,在使用时只需要合并多个请求:
final chopper = ChopperClient(
baseUrl: 'https://api.github.com',
services: [
GithubApiService.create(),
],
converter: JsonConverter(),
interceptors: [
HttpLoggingInterceptor(),
HeadersInterceptor({'User-Agent': 'Chopper'}),
CombineRequestInterceptor(),
],
);
final githubApiService = chopper.getService<GithubApiService>();
Response<List<dynamic>> response = await githubApiService.getUser('defunkt').then((userResponse) async {
final user = userResponse.body;
final reposResponse = await githubApiService.getUserRepos(user['login']);
final repos = reposResponse.body;
return Response<List<dynamic>>(repos, reposResponse.base);
});
4. 使用WebSocket:
对于一些需要实时更新的数据,可以使用WebSocket技术来避免过多的网络请求。可以使用Flutter自带的WebSocket库或者像SocketIO这样的第三方库来实现WebSocket,例如:
final channel = IOWebSocketChannel.connect('ws://localhost:1234');
channel.stream.listen((message) {
print('Received: $message');
});
channel.sink.add('Hello, WebSocket!');
总之,避免过多的网络请求是Flutter中的一个重要性能优化技巧,可以通过使用缓存技术、减少请求次数、合并请求、使用WebSocket等方法来实现。需要根据实际应用场景进行选择和应用。
优化布局
优化布局是Flutter中的一个重要性能优化技巧,以下是一些常用的优化方案:
1. 使用Flex布局:
使用Flex布局可以更加灵活地控制布局,从而避免不必要的布局计算。可以使用Row、Column、Flex等组件来实现Flex布局,例如:
Flex(
direction: Axis.horizontal,
children: [
Expanded(
child: Container(
height: 100,
color: Colors.red,
),
),
Expanded(
child: Container(
height: 100,
color: Colors.blue,
),
),
],
)
2. 使用CustomMultiChildLayout:
使用CustomMultiChildLayout可以自定义布局,从而避免不必要的布局计算。可以使用CustomMultiChildLayout来实现自定义布局,例如:
class MyLayoutDelegate extends MultiChildLayoutDelegate {
@override
void performLayout(Size size) {
Size leadingSize = Size.zero;
if (hasChild(0)) {
leadingSize = layoutChild(0, BoxConstraints.loose(size));
positionChild(0, Offset.zero);
}
if (hasChild(1)) {
Size trailingSize = layoutChild(1, BoxConstraints.loose(size));
positionChild(1, Offset(size.width - trailingSize.width, 0));
}
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true;
}
然后在使用时:
CustomMultiChildLayout(
delegate: MyLayoutDelegate(),
children: [
LayoutId(
id: 0,
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
LayoutId(
id: 1,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
],
)
``
3. 使用IndexedStack:
使用IndexedStack可以在多个Widget之间快速切换,从而避免不必要的布局计算。可以使用IndexedStack来实现快速切换多个Widget,例如:
IndexedStack(
index: _currentIndex,
children: [
Container(
width: 100,
height: 100,
color: Colors.red,
),
Container(
width: 100,
height: 100,
color: Colors.blue,
),
],
)
4. 使用AspectRatio:
使用AspectRatio可以控制Widget的宽高比,从而避免不必要的布局计算。可以使用AspectRatio来实现控制宽高比,例如:
AspectRatio(
aspectRatio: 1.0 / 0.5,
child: Container(
width: 100,
color: Colors.red,
),
)
优化布局是Flutter中的一个重要性能优化技巧,可以通过使用Flex布局、CustomMultiChildLayout、IndexedStack、AspectRatio等方法来实现。需要根据实际应用场景进行选择和应用。
使用异步操作
使用异步操作是Flutter中的一个重要性能优化技巧,以下是一些常用的异步操作方案:
1. 2使用Future:
使用Future可以在后台线程执行耗时操作,从而避免阻塞主线程。可以使用async和await关键字来实现异步操作,例如:
Future<String> fetchData() async {
return Future.delayed(Duration(seconds: 1), () => 'Hello, world!');
}
FutureBuilder(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text(snapshot.data);
} else {
return CircularProgressIndicator();
}
},
)
使用Future.delayed模拟了一个耗时1秒的操作,并使用FutureBuilder来展示异步操作的结果。
2. 使用Isolate:
使用Isolate可以在多个线程执行耗时操作,从而避免阻塞主线程。可以使用Flutter自带的compute函数来实现Isolate,例如:
Future<String> fetchData(String input) async {
await Future.delayed(Duration(seconds: 1));
return 'Hello, $input!';
}
FutureBuilder(
future: compute(fetchData, 'world'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text(snapshot.data);
} else {
return CircularProgressIndicator();
}
},
)
使用compute函数将耗时操作放到另外的线程中执行,并使用FutureBuilder来展示异步操作的结果。
3. 使用Stream:
使用Stream可以实现数据流的异步处理,从而避免阻塞主线程。可以使用StreamController来创建和管理Stream,例如:
StreamController<int> _counterController = StreamController<int>();
int _counter = 0;
void _incrementCounter() {
_counter++;
_counterController.sink.add(_counter);
}
@override
void dispose() {
_counterController.close();
super.dispose();
}
StreamBuilder<int>(
stream: _counterController.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Counter: ${snapshot.data}');
} else {
return CircularProgressIndicator();
}
},
)
使用StreamController创建了一个数据流,并在按钮点击时向数据流中添加数据。使用StreamBuilder来展示数据流的结果。
4. 使用async/await和Future.wait:
使用async/await和Future.wait可以同时执行多个异步操作,并等待所有操作完成后再统一处理结果。例如:
Future<List<String>> fetchData() async {
List<Future<String>> futures = [
Future.delayed(Duration(seconds: 1), () => 'Hello'),
Future.delayed(Duration(seconds: 2), () => 'World'),
];
List<String> results = await Future.wait(futures);
return results;
}
FutureBuilder(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return Text(snapshot.data.join(' '));
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text('No data');
}
} else {
return CircularProgressIndicator();
}
},
)
使用Future.delayed模拟了两个耗时操作,并使用Future.wait同时执行这两个操作。在所有操作完成后,使用FutureBuilder来展示异步操作的结果。
所以,使用异步操作是Flutter中的一个重要性能优化技巧,可以通过使用Future、Isolate、Stream、async/await和Future.wait等方法来实现。需要根据实际应用场景进行选择和应用。
避免过多的内存使用
避免过多的内存使用是Flutter中的一个重要性能优化技巧,以下是一些常用的优化方案:
1. 避免不必要的对象创建:
避免不必要的对象创建可以减少内存使用,从而提高应用性能。可以使用const关键字来避免重复创建相同的对象,例如:
const TextStyle style = TextStyle(fontSize: 16, color: Colors.black);
2. 使用图片压缩:
对于一些较大的图片,可以使用压缩技术来减少图片大小,从而减少内存使用。可以使用dart-image库来实现图片压缩,例如:
import 'package:image/image.dart' as img;
import 'dart:io';
File compressedImage(File imageFile) {
img.Image image = img.decodeImage(imageFile.readAsBytesSync());
img.Image smallerImage = img.copyResize(image, width: 800, height: 600);
return File(imageFile.path)
..writeAsBytesSync(img.encodeJpg(smallerImage, quality: 70));
}
3. 及时释放无用资源:
及时释放无用资源可以避免内存泄漏,从而减少内存使用。可以使用dispose方法来释放无用资源,例如:
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My App'),
),
body: TextField(
controller: _controller,
decoration: InputDecoration(
labelText: 'Username',
),
),
);
}
}
使用dispose方法释放了无用的TextEditingController资源。
4. 使用缓存技术:
使用缓存技术可以避免重复创建相同的对象,从而减少内存使用。可以使用Flutter自带的ImageCache类来实现图片缓存,例如:
ImageCache imageCache = PaintingBinding.instance.imageCache;
imageCache.maximumSize = 100; // 设置缓存的最大大小
避免过多的内存使用是Flutter中的一个重要性能优化技巧,可以通过避免不必要的对象创建、使用图片压缩、及时释放无用资源、使用缓存技术等方法来实现。需要根据实际应用场景进行选择和应用。
使用热重载
使用热重载是Flutter中的一个重要开发技巧,以下是一些常用的热重载方案:
1. 使用Flutter命令行工具:
使用Flutter命令行工具可以实现热重载,从而快速预览应用的修改效果。可以使用flutter run命令启动应用,并使用r或者R键来触发热重载,例如:
flutter run
在启动后,可以在终端中按下r或者R键来触发热重载。
2. 使用Flutter插件:
使用Flutter插件可以在开发工具中实现热重载,从而快速预览应用的修改效果。可以使用Android Studio、Visual Studio Code等开发工具,并安装Flutter插件,例如:
- Android Studio:安装Flutter插件,并使用Ctrl+\或者Ctrl+R键来触发热重载。
- Visual Studio Code:安装Flutter插件,并使用Ctrl+F5键来触发热重载。
在使用开发工具的热重载时,需要先启动应用,并将焦点放在编辑器中。
3. 使用Flutter DevTools:
使用Flutter DevTools可以在浏览器中实现热重载,从而快速预览应用的修改效果。可以使用Flutter DevTools的hot reload功能,例如:
- 使用flutter run命令启动应用,并使用–observatory-port选项指定端口号:
f lutter run --observatory-port 8888
- 打开浏览器,访问http://localhost:8888/,并启动DevTools。
- 在DevTools中选择“Hot Reload”选项,并点击“Reload”按钮来触发热重载。
在使用Flutter DevTools的热重载时,需要先启动应用,并启动DevTools。
使用热重载是Flutter中的一个重要开发技巧,可以通过使用Flutter命令行工具、Flutter插件、Flutter DevTools等方法来实现。需要根据实际开发场景选择和应用。
Flutter 性能优化
性能优化的最好时机是在编码阶段,如果你能掌握一些性能优化技巧和最佳实践,那么在编码时就可以考虑到怎么设计会性能最优,怎么实现会拖慢性能;而不是开发完成之后在去着手做性能优化,这样先污染后治理的思路。
在我们前面的Flutter开发过程中已经应用了不少Flutter性能优化的技巧以及一些最佳实践,所以说呢,在这一节主要还是对性能优化做个总结,以帮助大家更新的学习和掌握。
优化前要以 性能模式 运行 Flutter 应用
- 内存优化
- build()方法优化
- 在build()方法中执行了耗时的操作
- build()方法中堆砌了庞大的Widget
- 列表优化方法
- 案例:帧率优化
内存优化
要进行内存优化首先我们需要了解下内存的检测手段,这样我们才好进行内存优化前后的效果对比。
Flutter性能检测工具Flutter Performance
在 IDE 的 Flutter plugin 中提供了 Flutter Performance工具,它是一个可用来检测Flutter滑动帧率和内存的工具。
我们可以从IDE的侧边栏中打开这个工具,也可以借助Dart DevTools来查看内存的使用情况:
此时可以打开一个页面或进行一些操作来观察内存的变化,如果内存突然增大很多就要特别关注是否是合理的增加,必要时要排查导致内存增加的原因和考虑对于的优化方案。
关于如何判断优化后内存有没有变化,可以通过Dart DevTools的Memory选项卡来完成,当你销毁一个FlutterEngine后可以通过GC按钮来触发一次GC来查看内存的变化。
build()方法优化
在我们用Flutter开发UI的时候,打交道最多的方法便是build()了,使用build()方法时有两个常见的陷阱,我们一起来看一下:
在build()方法中执行了耗时的操作
我们应该尽量避免在build()中执行耗时的操作,这是因为build()方法会频繁的调用,尤其是当父Widget重建的时候;所以,耗时的操作建议挪到initState()这种不会被频繁调用的方法中;
另外,我们尽量不要在代码中进行阻塞式操作,可以将文件读取,数据库操作,网络请求这些操作通过Future来转成异步完成;另外对于CPU计算频繁的操作比如:图片压缩等可以使用Isolate来充分利用多核心CPU;
build()方法中堆砌了庞大的Wdiget
在画UI的时候有的小伙伴喜欢一把梭,其实这是一个特别不好的习惯;如果build()中返回的Widget过于庞大会导致三个问题:
- 代码可读性差:因为Flutter的布局方式的特殊性,画界面我们离不了的需要一个Wdiget嵌套一个Wdiget,但如果Wdiget嵌套太深则会导致代码的可读性变差,也不利于后期的维护和扩展;
- 复用难:由于所有的代码都在一个build()方法中,会到导致无法将公共的UI代码服用到其它的页面或模块;
- 影响性能:我们在 State 上调用 setState()时,所有build()中的 Widget 都将被重建;因此build()中返回的Widget树越大那么需要重新建的Widget越多,对性能越不利;见下图:
假如上图是我们在一个build()方法中所返回的widget树,那么当左侧红框中的widget需要更新的时候,最小的更新成本是只更新需要跟新的部分,但是由于它们都在一个State的build方法所以,调用setState()时会导致右边很多不需要更新的widget也需要重新;正确的做法是,将 setState() 的调用转移到其 UI 实际需要更改的 Widget 子树部分。
列表优化方法
在构建大型网格或列表时,我们要尽量避免直接使用ListView(children: [],)或GridView(children: [],),因为在这种场景下会导致列表中所有的数据都会被一次性绘制出来不管列表内容是否可见,这种用法类似Android的ScrollView;所以说当你的列表中的数据量比较大时建议你用:
- ListView.builder(itemBuilder: null)
- GridView.builder(gridDelegate: null, itemBuilder: null)
这两个方法,因为这两个方法只有在屏幕的可见部分是在列表的内容才开始被创建,这种又发类似于Android的RecyclerView。
帧率优化
决定列表性能的好坏一个很关键的因素就是帧率,通常情况下手机的刷新频率为60fps,当然目前陆续出现一些高刷屏的手机能够达到90甚至120的fps。在Flutter中获取应用的帧率我们可以通过Flutter Performance选项卡来查看页面帧率:
另外可以点击上图左上角Performance overlay按钮来打开性能图层功能:
通过这个图表我们可以帮助我们分析 UI是否产生了卡顿,垂直的绿色条条代表的是当前帧,每一帧都应该在 1/60 秒(大约 16 ms)内创建并显示。如果有一帧超时而无法显示,就导致了卡顿,上述图表就会展示出来一个红色竖条。如果是在 UI 图表出现了红色竖条,则表明 Dart 代码消耗了大量资源。红色竖条表明当前帧的渲染和绘制都很耗时。
帧率优化案例
借助Flutter Performance我们对课程项目的首页进行帧率检测:
因为debug模式下Flutter的性能会受到比较大的限制,为了还原检测的真实性我们需要在分析模式运行APP。
- 在 Android Studio 和 IntelliJ 使用 Run > Flutter Run main.dart in Profile Mode 选项
- 或者通过命令行使用 --profile 参数运行 : flutter run --profile
注意:模拟器不支持分析模式,可以用真机连接电脑来进行分析
优化前
优化后
超级全面的Flutter性能优化实践
前言
Flutter是谷歌的移动UI结构,能够快速在iOS和Android上构建高质量的原生用户界面。 Flutter能够与现有的代码一起作业。在全世界,Flutter正在被越来越多的开发者和组织运用,而且Flutter是完全免费、开源的,能够用一套代码一起构建Android和iOS运用,功能能够到达原生运用相同的功能。可是,在较为杂乱的 App 中,运用 Flutter 开发也很难防止产生各种各样的功能问题。在这篇文章中,我将介绍一些 Flutter 功能优化方面的运用实践。
一、优化检测东西
flutter编译方式
Flutter支撑Release、Profile、Debug编译方式。
-
Release方式,运用AOT预编译方式,预编译为机器码,经过编译生成对应架构的代码,在用户设备上直接运转对应的机器码,运转速度快,履行功能好;此方式关闭了一切调试东西,只支撑真机。
-
Profile方式,和Release方式类似,运用AOT预编译方式,此方式最重要的作用是能够用DevTools来检测运用的功能,做功能调试剖析。
-
Debug方式,运用JIT(Just in time)即时编译技能,支撑常用的开发调试功能hot reload,在开发调试时运用,包括支撑的调试信息、服务扩展、Observatory、DevTools等调试东西,支撑模拟器和真机。
经过以上介绍咱们能够知道,flutter为咱们供给 profile方式发动运用,进行功能剖析,profile方式在Release方式的基础之上,为剖析东西供给了少量必要的运用追踪信息。
如何敞开profile方式?
假如是独立flutter工程能够运用flutter run –profile发动。假如是混合 Flutter 运用,在 flutter/packages/flutter_tools/gradle/flutter.gradle 的 buildModeFor 办法中将 debug 方式改为 profile即可。
检测东西
1、Flutter Inspector (debug方式下)
Flutter Inspector有很多功能,其间有两个功能更值得咱们去重视,例如:“Select Widget Mode” 和 “Highlight Repaints”。
Select Widget Mode点击 “Select Widget Mode” 图标,能够在手机上检查当时页面的布局结构与容器类型。
经过“Select Widget Mode”咱们能够快速检查生疏页面的布局实现办法。
Select Widget Mode方式下,也能够在app里点击相应的布局控件检查
Highlight Repaints
点击 “Highlight Repaints” 图标,它会为一切 RenderBox 制作一层外框,并在它们重绘时会改变色彩。
这样做帮你找到 App 中频频重绘导致功能耗费过大的部分。
例如:一个小动画可能会导致整个页面重绘,这个时分运用 RepaintBoundary Widget 包裹它,能够将重绘范围缩小至自身所占用的区域,这样就能够削减制作耗费。
2、Performance Overlay(功能图层)
在完结了运用发动之后,接下来咱们就能够运用 Flutter 供给的烘托问题剖析东西,即功能图层(Performance Overlay),来剖析烘托问题了。
咱们能够经过以下办法敞开功能图层
功能图层会在当时运用的最上层,以 Flutter 引擎自绘的办法展现 GPU 与 UI 线程的履行图表,而其间每一张图表都代表当时线程最近 300 帧的表现,假如 UI 产生了卡顿,这些图表能够协助咱们剖析并找到原因。 下图演示了功能图层的展现款式。其间,GPU 线程的功能状况在上面,UI 线程的状况显现在下面,蓝色垂直的线条表明已履行的正常帧,绿色的线条代表的是当时帧:
假如有一帧处理时刻过长,就会导致界面卡顿,图表中就会展现出一个赤色竖条。下图演示了运用呈现烘托和制作耗时的状况下,功能图层的展现款式:
假如赤色竖条呈现在 GPU 线程图表,意味着烘托的图形太杂乱,导致无法快速烘托;而假如是呈现在了 UI 线程图表,则表明 Dart 代码耗费了很多资源,需求优化代码履行时刻。
3、CPU Profiler(UI 线程问题定位)
在视图构建时,在 build 办法中运用了一些杂乱的运算,或是在主 Isolate 中进行了同步的 I/O 操作。 咱们能够运用 CPU Profiler 进行检测:
你需求手动点击 “Record” 按钮去主动触发,在完结信息的抽样采集后,点击 “Stop” 按钮结束录制。这时,你就能够得到在这期间运用的履行状况了。
其间:
x 轴:表明单位时刻,一个函数在 x 轴占有的宽度越宽,就表明它被采样到的次数越多,即履行时刻越长。
y 轴:表明调用栈,其每一层都是一个函数。调用栈越深,火焰就越高,底部就是正在履行的函数,上方都是它的父函数。
经过上述CPU帧图咱们能够大概剖分出哪些办法存在耗时操作,针对性的进行优化
一般的耗时问题,咱们一般能够运用 Isolate(或 compute)将这些耗时的操作挪到并发主 Isolate 之外去完结。
例如:杂乱JSON解析子线程化
Flutter的isolate默许是单线程模型,而一切的UI操作又都是在UI线程进行的,想运用多线程的并发优势需新开isolate 或compute。无论如何await,scheduleTask 都只是延后使命的调用时机,仍然会占用“UI线程”, 所以在大Json解析或很多的channel调用时,一定要观测对UI线程的耗费状况。
二、Flutter布局优化
Flutter 运用了声明式的 UI 编写办法,而不是 Android 和 iOS 中的指令式编写办法。
-
声明式:简单的说,你只需求告诉计算机,你要得到什么样的成果,计算机则会完结你想要的成果,声明式更重视成果。
-
指令式:用具体的指令机器怎么去处理一件事情以到达你想要的成果,指令式更重视履行进程。
flutter声明式的布局办法经过三棵树去构建布局,如图:
-
Widget Tree: 控件的装备信息,不涉及烘托,更新价值极低。
-
Element Tree : Widget树和RenderObject树之间的粘合剂,担任将Widget树的变更以最低的价值映射到RenderObject树上。
-
RenderObject Tree : 真正的UI烘托树,担任烘托UI,更新价值极大。
1、常规优化
常规优化即针对 build() 进行优化,build() 办法中的功能问题一般有两种:耗时操作和 Widget 层叠。
1)、在 build() 办法中履行了耗时操作
咱们应该尽量防止在 build() 中履行耗时操作,因为 build() 会被频频地调用,尤其是当 Widget 重建的时分。 此外,咱们不要在代码中进行堵塞式操作,能够将一般耗时操作等经过 Future 来转化成异步办法来完结。 对于 CPU 计算频频的操作,例如图片压缩,能够运用 isolate 来充分运用多核心 CPU。
2)、build() 办法中堆叠了很多的 Widget
这将会导致三个问题:
1、代码可读性差:画界面时需求一个 Widget 嵌套一个 Widget,但假如 Widget 嵌套太深,就会导致代码的可读性变差,也晦气于后期的维护和扩展。
2、复用难:由于一切的代码都在一个 build(),会导致无法将公共的 UI 代码复用到其它的页面或模块。
3、影响功能:咱们在 State 上调用 setState() 时,一切 build() 中的 Widget 都将被重建,因而 build() 中回来的 Widget 树越大,那么需求重建的 Widget 就越多,也就会对功能越晦气。
所以,你需求 控制 build 办法耗时,将 Widget 拆小,防止直接回来一个巨大的 Widget,这样 Widget 会享有更细粒度的重建和复用。
3)、尽可能地运用 const 结构器
当构建你自己的 Widget 或者运用 Flutter 的 Widget 时,这将会协助 Flutter 只是去 rebuild 那些应当被更新的 Widget。 因而,你应该尽量多用 const 组件,这样即使父组件更新了,子组件也不会从头进行 rebuild 操作。特别是针对一些长时刻不修正的组件,例如通用报错组件和通用 loading 组件等。
4)、列表优化
-
尽量防止运用 ListView默许结构办法
不管列表内容是否可见,会导致列表中一切的数据都会被一次性制作出来
-
主张运用 ListView 和 GridView 的 builder 办法
它们只会制作可见的列表内容,类似于 Android 的 RecyclerView。
其实,本质上,就是对列表采用了懒加载而不是直接一次性创立一切的子 Widget,这样视图的初始化时刻就削减了。
2、深入光栅化优化
优化光栅线程
屏幕显现器一般以60Hz的固定频率刷新,每一帧图画制作完结后,会继续制作下一帧,这时显现器就会宣布一个Vsync信号,按60Hz计算,屏幕每秒会宣布60次这样的信号。CPU计算好显现内容提交给GPU,GPU烘托好传递给显现器显现。 Flutter遵循了这种方式,烘托流程如图:
flutter经过native获取屏幕刷新信号经过engine层传递给flutter framework
一切的 Flutter 运用至少都会运转在两个并行的线程上:UI 线程和 Raster 线程。
-
UI 线程
构建 Widgets 和运转运用逻辑的当地。
-
Raster 线程
用来光栅化运用。它从 UI 线程获取指令将其转化成为GPU指令并发送到GPU。
咱们一般能够运用Flutter DevTools-Performance 进行检测,过程如下:
-
在 Performance Overlay 中,检查光栅线程和 UI 线程哪个负载过重。
-
在 Timeline Events 中,找到那些耗费时刻最长的事件,例如常见的 SkCanvas::Flush,它担任处理一切待处理的 GPU 操作。
-
找到对应的代码区域,经过删去 Widgets 或办法的办法来看对功能的影响。
三、Flutter内存优化
1、const 实例化
const 对象只会创立一个编译时的常量值。在代码被加载进 Dart Vm 时,在编译时会存储在一个特殊的查询表里,只是只分配一次内存给当时实例。
咱们能够运用 flutter_lints 库对咱们的代码进行检测提示
2、检测耗费多余内存的图片
Flutter Inspector:点击 “Highlight Oversizeded Images”,它会识别出那些解码大小超过展现大小的图片,而且系统会将其倒置,这些你就能更简单在 App 页面中找到它。
经过下面两张图能够明晰的看出运用“Highlight Oversizeded Images”的检测作用
针对这些图片,你能够指定 cacheWidth 和 cacheHeight 为展现大小,这样能够让 flutter 引擎以指定大小解析图片,削减内存耗费。
3、针对 ListView item 中有 image 的状况来优化内存
ListView 不会毁掉那些在屏幕可视范围之外的那些 item,假如 item 运用了高分辨率的图片,那么它将会耗费十分多的内存。
ListView 在默许状况下会在整个滑动/不滑动的进程中让子 Widget 保持活动状况,这一点是经过 AutomaticKeepAlive 来确保,在默许状况下,每个子 Widget 都会被这个 Widget 包裹,以使被包裹的子 Widget 保持活跃。 其次,假如用户向后翻滚,则不会再次从头制作子 Widget,这一点是经过 RepaintBoundaries 来确保,在默许状况下,每个子 Widget 都会被这个 Widget 包裹,它会让被包裹的子 Widget 只是制作一次,以此获得更高的功能。 但,这样的问题在于,假如加载很多的图片,则会耗费很多的内存,最终可能使 App 溃散。
经过将这两个选项置为 false 来禁用它们,这样不可见的子元素就会被主动处理和 GC。
4、多变图层与不变图层别离
在日常开发中,会经常遇到页面中大部分元素不变,某个元素实时改变。如Gif,动画。这时咱们就需求RepaintBoundary,不过独立图层合成也是有耗费,这块需实测把握。
这会导致页面同一图层从头Paint。此时能够用RepaintBoundary包裹该多变的Gif组件,让其处在独自的图层,待最终再一块图层合成上屏。
5、降级CustomScrollView,ListView等预烘托区域为合理值
默许状况下,CustomScrollView除了烘托屏幕内的内容,还会烘托上下各250区域的组件内容,例如当时屏幕可显现4个组件,实践仍有上下共4个组件在显现状况,假如setState(),则会进行8个组件重绘。实践用户只看到4个,其实应该也只需烘托4个, 且上下滑动也会触发屏幕外的Widget创立毁掉,形成翻滚卡顿。高功能的手机可预烘托,在低端机降级该区域距离为0或较小值。
四、总结
Flutter为什么会卡顿、帧率低?总的来说均为以下2个原因:
-
UI线程慢了–>烘托指令出的慢
-
GPU线程慢了–>光栅化慢、图层合成慢、像素上屏慢
所以咱们一般运用flutter布局尽量依照以下原则
Flutter优化基本原则:
-
尽量不要为 Widget 设置半透明作用,而是考虑用图片的方式代替,这样被遮挡的 Widget 部分区域就不需求制作了;
-
控制 build 办法耗时,将 Widget 拆小,防止直接回来一个巨大的 Widget,这样 Widget 会享有更细粒度的重建和复用;
-
对列表采用懒加载而不是直接一次性创立一切的子 Widget,这样视图的初始化时刻就削减了。