Devlog#06|Flutter 优化,从图片开始

发布于:01/27/2025 18:04:00

Flutter 好归好,但是如果你没忍住看了任务管理器的话,就会发现内存占用异常的大,不是大一点,而是非常多。

尤其是对于图片非常多的场景,这个占用异常的夸张。如果你这时候开 DevTools 的时候,就会发现大部份都是 RSS 内存(Resident Set Size 不是那个 Really Simple Syndication 的 RSS

我曾经尝试过各种各样方式优化,包括删掉所有没有纯 Dart 实现的依赖(因为看到说 RSS 内存是不归 Dart VM 管的原生占用部份),都没有什么实质上的影响。一度让我以为 Flutter 的内存占用就是少不了的,想要逃离到 React Native 去。

但是在一次无聊滑手机的时候看到了 Medium 上的一篇文章讲的是 Flutter Image Optimzation,包着尝试的心态试着用了一些上面的策略。结果的确有很大的改善。所以想分享一下。

优化

做过 Web 开发的可能会知道会要优化网页上使用到的一些素材,因为加载会用掉很多时间。但是没有人说要优化素材是因为内存占用的问题(一方面可能是 JS 引擎本身占用就比这些多,也有可能是浏览器自身花了很多功夫做优化)但是这件事情到了 Flutter 上就变得非常重要。

为了渲染图片,Flutter 要读取全部的图片数据,并且存的大小还会比原始文件还大,因为 ui.Image 存储的是每一个像素点的颜色值,不是经过 JPEG、PNG 之类算法压缩过的图片。

那么这些数据去哪里了呢?不用猜,就是内存里,而且这些还是直接由 Flutter Engine 处理的,算在 RSS 内存里,因此调试很不好知道那块是图片占用的。

为了解决这个问题,我们可以使用很多方案。

从源头解决

如果你比较富裕,或者是项目用不到什么网络图片。可以考虑从图片源文件解决问题。本地素材可以做多个版本的针对不同分辨率的屏幕;网络图片可以自己写 / 部署图片优化代理,或者用 Cloudflare Images、Cloudinary 之类的外部服务(共同点都是很贵,按请求 / 流量收费)这样,Flutter 要加载的数据变小了,它也没有理由往里面塞垃圾数据。

从传播处解决

其实,Flutter 也不是完全没有在意这个问题,他们给了两个属性在 Image 组件上,一个是 cacheHeightcacheWidth,表示要缓存的大小。(如果你用 cached_network_image 的话叫做 memCacheHeightmemCacheWidth

在这上面标注这两个属性,告诉 Flutter 你需要的图片大小,它也会少存一点。

注意:如果你单纯只传图片大小,可能会得到很低清的结果,这是因为你的设备逻辑像素和实际上显示的像素不是 1:1,这常见于一些高端设备和高分辨率显示屏上,这是需要将传入的 cacheHeightcacheWidth 乘以 devicePixelRatio 即可

devicePixelRatio 可以通过 MediaQuery.of(context).devicePixelRatio 获取

btw 如果你用什么 CircleAvatar、DecorationImage 之类要求传入 ImageProvider 的组件时候,你也可以优化你的图片,只用在原本的 ImageProvider 外面包一个 ResizeImage 即可。其中的 widthheight 就是原本的 cacheWidthcacheHeight

ps, btw 中提到的 ResizeImage 的默认行为可能会拉伸你的图片导致奇奇怪怪的效果,这时候传入 policy: ResizeImagePolicy.fit 即可解决。

pps, 不知道最近某次更新之后 Flutter 改坏了什么东西,在 Web 上使用 ResizeImage 和 cacheWidth/Height 会导致图片纯黑,不显示。目前的解决方法就是不要使用

gcc ./flutter.cpp -O3 -o ./Solian.app

但是,这样有一个问题,你不知道每一个图片的长和宽,尤其是用户上传的内容。就算你利用 Paperclip 知道了,也不知道实际上它会渲染多大。这时候就要请出我自用的解决方案(Surface 上的)ResizeImage + LayoutBuilder

其原理就是利用 LayoutBuilder 获取到这个 Image 组件所占用的大小,将其设为 cacheWidthcacheHeight。同时这也带来了一些局限性,如果说你要让这个图片像压缩毛巾一样变大变高的话,在做这个过程中由于 cacheHeightcacheWidth 一直在改变,图片可能会一闪一闪,性能不好的设备可能会卡(因为要重新解码图片)

具体实现可以查看 HyperNet.Surface 项目源码:https://github.com/Solsynth/HyperNet.Surface/blob/master/lib/widgets/universal_image.dart#L112

调试

为了查看你当前页面上什么图片超出渲染所需的大小,你可以用 debugInvertOversizedImages API 来查看。它是一个全局变量,boolean 类型,只用在入口函数放一个这个语句之后 Hot Restart 即可。

void main() {
  // ...
  debugInvertOversizedImages = true
  // 或者你想留着这个代码到生产上去
  debugInvertOversizedImages = kDebugMode
  // ...
}

之后,过重的图片就会改从地狱来(看起来很阴间),便于你优化。

吓人

总结

以上的方法中最好的还是从源头解决,但是为了不破坏项目,替换 Image 组件使用 ResizeImage + LayoutBuilder 也不失为一种佳策。

但是 ResizeImage 也好,cacheWidth / Height 也罢,都会让设备重新解码、优化图片,这对一些低端设备(在 SN 的例子里,iPhone 8+)可能不太友好,会造成卡断。尤其是 Web 版本会造成严重卡顿(单线程立大功)。因此又到了你不能同时拥有以下这三项:

  • 功能
  • 空间
  • 速度

你会怎么取舍呢?反正我是这么选的。希望本篇文章能对 Flutter 开发的你做到一些帮助。最终我还是没用 React Native,主要是 RN 在多设备上的样式不一样,很难调整。而且没有原生 Windows / Linux / macOS,只能用 Web。

还是 Flutter 好呀~