💊 flutter 性能优化
!本篇文章过于久远,其中观点和内容可能已经不准确,请见谅!~
想分享的是让你知道flutter
开发中遇到的性能问题,和一些很容易的优化方法。
无论用什么语言开发应用的时候,性能和稳定性都是一个更高要求的方面,用 80% 的时间来拔高 20% 的效果。
这里因为业务的 APP 并没有达到那么大的用户量,所以不需要太深入,毕竟收益能效还不到深入的时候,所以这里也是仅仅优化常见的业务问题,比如非常影响性能的地方。
Performance best practices 这是官方的一篇文档,里面包含了很多的性能优化实践,比如:
- 将耗时的渲染操作,从 build 细化到子组件中。比如一个 build 渲染函数中如果包含了一个复杂计算,其他的依赖更新,就会导致这个复杂计算被执行,徒增耗时。
- 避免使用涉及 effects 的组件。比如 Opacity、Text、ShaderMask、ColorFilter、Chip 组件。effects 效果都会使用很复杂的渲染(比如 saveLayer)。Opacity + 图片可以替换为 FadeInImage,Clip 裁剪矩形可以使用 borderRadius 代替。
- 长列表必须使用带有 buider/sliver 的滚动组件。这样可以保证在 viewPort 外的内容可以被销毁,然后节省内存。
- 动画中调用耗时组件。
- 保证每次 build 函数都小于 16ms。
这些最佳实践说明还是很有用的,比如我在滑动的时候会渐隐一个组件,用到了 Opacity 组件,界面出现卡顿,在去除渐隐的效果后性能明显改善。
除此之外:
- 可以尝试将部分组件实例保持不变,比如增加 const 修饰。
- build 中的耗时计算结果可以缓存起来。
- 不可见的控件,尽量不进行依赖更新和 build 渲染。
- 分清楚一个组件的生命周期,在恰当的生命钩子中执行恰当的动作。
参考文档优化自己的应用,能够解决很多的性能问题,如果依然出现问题可以向下分析性能。
流畅度是眼睛看到的视觉,不是一个能明确报错行数的 bug,所以需要一个指标来看到底哪里导致的渲染问题。Dart DevTool 提供诸如性能分析、内存占用以及显示运行火焰图等功能。
- 在 Android Studio 中使用 Run > Flutter Run main.dart in Profile Mode 选项
能看到此时的 performance
- 在 Run 窗口中打开 devTools 工具。
在其中的 performance 功能中,录制停止之后,可以看到绘制的函数调用时间分布:
其中图示就是 CPU 的时间分布火焰图,很方便的看到每一个函数的调用时间,细化到具体的函数。一般如果函数的执行性能有问题,在这里就能看到是哪一个函数的耗时出现了问题。
这个工具使用之后就能解决一些比价容易忽略的性能问题了。
APP 编译之后在手机上运行的时候内存占用非常高,尤其在低端手机上非常卡顿,在分析下之后发现了内存上很严重的问题。
发现内存占用随着路由添加内存增长非常快,几个页面之后就将近 1G 的内存水平了,但是页面也就几个图片,而且滑动的时候内存也快速上升,即使业务中已经对于滚动优化了越界区域组件的释放,很明显哪里出了问题。
flutter run --profile
这个图能看到确实是图片的占用占据了很大的一部分内存没有释放
这个图大概能看到出了什么问题,图片的展示利用了第三方插件实现缓存,并且出现的时候有进度和逐渐显示的效果,所以
AdvancedNetworkImage
和 Image
的占用非常高。但是也不至于这么多,同时 _TransitionToImageState
也不应该占用这么多,猜测是 TransitionToImage
组件可能多次缓存了图片数据甚至可能有内存泄露,导致内存占用被放大了。有了猜想去检验一下,将这个组件替换成纯图片组件
Image
作为 ImageProvider
,然后用 AdvancedNetworkImage
作为数据源,再测试:类似的操作之后,内存水平看简直不是同一个 App,内存下降的非常明显,流畅度也有很大的提高,唯一缺失的只有进度这一个功能,业务层面也完全能够接受。
业务的形式与性能也很有关系,很多业务天生就占内存,比如长图片列表等图片组件。
任何的图片组件,都可以使用裁剪过的图片来减少内存占用,这是一个非常简单但是非常有效的途径,而且能提高加载速度、降低服务器带宽等。
列表中的大量图片,需要使用符合大小的缩略图,没有缩略图可以使用 CDN 的 url 裁剪功能,或者 nginx 的图片裁剪来实现自动裁剪,对业务的侵入也很小。同时保证各处的裁剪大小相同也能命中裁剪缓存。这样客户端在展示的时候,可以使用较低质量的图片显著降低内存的占用。
我们的 APP 有很多的滚动列表,每个列表几乎都是图片列表,裁剪图片这个做法将内存占用降低了 70%,加载速度也肉眼可见的更好了,流畅度上比之前好很多。
刚开始用 flutter 的时候会出现比如切换 tab 的时候,没办法保留组件状态,因为组件一旦销毁,再重建不会使用之前的状态。这很容易理解,所以一番搜索之后发现
AutomaticKeepAliveClientMixin
的 keep alive 能够实现,所以在很多的常用组件,尤其是滚动组件上加上了这个属性。问题就是这个组件,导致了滚动组件没有回收其中的图片资源和其中的列表数据,占用了很多内存,所以这个 keep alive 的方法只能在关键的页面使用。而一般的组件呢?尤其是滚动组件,为了让内存保持低水平,必须让组件及时销毁,但是为了让组件切换回来的时候不至于丢了滚动位置,还是需要保留这个状态。
解决方案就是封装滚动组件页面,将内部的列表数据、滚动位置、相关附加数据等暂存,以页面 Key 为键保存值,每次组件获取数据、更改位置都保存下这些状态,然后销毁重建的时候,去数据中心读取这些数据,因为是内存中的,组件根据内存数据重新初始化很快,感觉不到组件的 loading,所以也就实现了保存状态,但是不保存组件的目的。
- 第一版:使用内存缓存和本地文件缓存
封装原生图片组件,使用 ImageProvider 和 ImageCache 来缓存图片到内存和本地文件储存上。
缓存到内存里面,可以实现图片的最快加载,甚至不会出现 loading 动画,而且不同分辨率的图片做动画也是丝滑变换的,体验很好。
但是内存中缓存太奢侈了,尤其是列表中的,大量图片的使用一定会导致内存爆炸增长,最后崩溃。所以内存的缓存在列表中去除了,图片组件被销毁,内存中也不保留图片信息,图片加载的时候回从本地文件系统中读取,虽然会导致一些 loading,但是好在内存的使用不会太大的增长。
- 第二版:添加图片加载队列
对于一些比较大的图片,比如 webp 动图和 gif 图片,在加载的时候会出现很严重的 loading 等待,此时在图片加载逻辑中添加了 队列加载的功能,滑动过程中同时最多只加载 2 张图片,然后滑动的过程中,在视图中的图片会被提到最优先的位置。更优先保证用户的视觉关注点的图片加载,而已经划过去的图片会被延后甚至直接取消下载。
- 第三版:图片格式和质量
webp 的支持比较好,相比之前的 gif 文件更小,所以缓存和加载更理想,后期全部换成 webp 的格式来提高加载效率。
另一方面梳理全部应用内的图片质量,确定缩略图的质量,在服务端直接切好需要的尺寸,质量也使用更高压缩的格式和恰当的质量选项。
在使用内存分析工具的时候,发现页面堆栈增多的时候,内存也是增长的,也就是新页面的入栈之后,之前的页面并不会回收内存占用不会减少,在内存紧张的时候也并不会销毁。这个特性尤其是在多个页面都包含很多图片资源的时候,很多页面的资源内存叠加,就会出现偶尔的内存暴涨崩溃,我们遇到了很多情况。
这个问题和之前的 keep-alive 比较相似,只不过这个是路由的机制。所以我们考虑页面切换的时候,在新的页面打开后,前面的页面如果是可以缓存的(得益于之前的列表缓存策略),就标记为可回收,让组件的内存占用销毁,但是保留页面的数据内存。页面出栈的时候,之前的页面再重新根据以保存的数据重新初始化,这样就可以和 state 的保留方案相同,节省不可见页面的内存占用,效果还是可以接受的。
比较重要的就是怎么判断当前页面是在前面而且不可见的,比如 overlay 的路由之前的页面需要可见,这整个路由层面的判定需要一个额外的封装,提供路由相关的信息。由于我们 flutter 的路由比较集中,而且页面的 state 能够缓存,这个地方比较好梳理。如果页面内容复杂,路由交错,这里的多页面数据暂存可能就不会那么好处理了。
而且如果需要页面承载一定的逻辑,比如不可见状态也要监听某个操作做一些更新,那么久没办法做页面的销毁,这个还是根据具体的页面做特别的处理,不能全部切割。
这个多页面的内存优化对内存增长的限制优化很不错。
最后的 APP 效果虽然牺牲了一部分拔高的体验,但是内存和稳定性都提高了很多。进一步的优化还有很多,但是目前已经解决了很大的部分,后续根据需要再针对性的优化一些场景,对极致的追求是没有尽头的,但是业务的效益和平衡也都需要考虑。
感谢您的阅读,本文由 Ubug 版权所有。如若转载,请注明出处:Ubug(https://ubug.io/blog/flutter-performance)