写作费时,敬请点赞,关注,收藏三连。

图片解码和缓存管理是渲染引擎的一个重要模块,这是因为图片解码的耗时很长,特别是对于设计为跨平台的通用渲染引擎来说,依赖于CPU来做图片解码,会消耗大量的CPU时间,并且图片解码后占用的内存很大,一张 1024x1024 分辨率的图片解码后就需要 4M 内存(除非硬件支持实时生成无损压缩格式纹理,通常这也不在通用渲染引擎的考虑范围之内)。所以一个设计良好的图片解码和缓存管理模块需要平衡很多不同的因素,包括内存占用,CPU占用,解码任务调度的及时性等。

在对 Flutter 的图片解码和缓存管理模块进行研究后,发现它跟 Chromium 有很大的差别。一方面它实现比较简单,给予了应用更直接的控制权,引擎本身只提供了最基本的支持,更契合 Native UI 的实际使用场景,另外一方面因为引擎本身缺少控制权,如果应用生成的 UI 界面较为极端,可能会导致比较灾难性的结果。

在这篇文章,我会先对 Flutter 的图片解码和缓存管理机制进行说明。然后再说明这种机制存在的一些问题。

Image Widget and Provider

class Image extends StatefulWidget {

...

/// The image to display. final ImageProvider image;

}

abstract class NetworkImage extends ImageProvider {

...

}

Flutter 通过 Image.asset,Image.file,Image.network 等方法创建一个 Image Widget 来显示图片,方法名字说明了图片数据的来源,他们实际上是为 Image Widget 提供了不同的 ImageProvider,比如说 Image.network 创建的 Image Widget,它的 ImageProvider 就是 NetworkImage。因为 Image Widget 是一个 StatefulWidget,所以它核心的状态处理逻辑代码是位于 _ImageState 对象中,由它来创建真正显示图片的 RawImage Widget。

class _ImageState extends State with WidgetsBindingObserver {

...

@override

void didChangeDependencies() {

_updateInvertColors();

_resolveImage();

if (TickerMode.of(context))

_listenToStream();

else

_stopListeningToStream();

super.didChangeDependencies();

}

void _resolveImage() {

final ScrollAwareImageProvider provider = ScrollAwareImageProvider(

context: _scrollAwareContext,

imageProvider: widget.image,

);

final ImageStream newStream =

provider.resolve(createLocalImageConfiguration(

context,

size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,

));

assert(newStream != null);

_updateSourceStream(newStream);

}

void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {

setState(() {

_imageInfo = imageInfo;

_loadingProgress = null;

_frameNumber = _frameNumber == null ? 0 : _frameNumber + 1;

_wasSynchronouslyLoaded |= synchronousCall;

});

}

}ScrollAwareImageProvider 是新版本新增的优化,它包装了最初的 ImageProvider,用来避免在快速滚动的过程中加载图片,也就是说快速滚动过程新增的 Image Widget,它加载图片的时机会被延迟,如果它在滚动过程中移除屏幕然后被移除,就完全不会触发加载。

当 Image Widget 被加入到 UI 的 Widget 树时,Flutter 就会调用 _ImageState.didChangeDependencies,然后 _ImageState._resolveImage 被调用,最后调用 ImageProvider.resolve 来加载图片。ImageProvider.resolve 触发了一连串的事情发生,它会先在 ImageCache 中生成 Entry,然后开始加载数据(异步方法,由 ImageProvider 的子类提供),加载完数据后生成相应的 Codec 开始请求解码(异步方法,由 Native Engine 提供),解码完成后最终通知 _ImageState._handleImageFrame 改变状态,产生新的 child Widget 显示图片。Flutter 单帧图片的解码是运行在 worker 线程池(可以并发),解码后的 GPU 纹理上传是 io 线程,多帧图片的解码和纹理上传都是在 io 线程。

也就是说:Flutter 图片解码的调度和图片缓存的管理都在 Widget 层,由 Image Widget 关联的 _ImageState 对象和 ImageProvider 对象负责;

图片缓存的实现是 ImageCache 对象,通过 PaintingBinding.instance.imageCache 访问,ImageProvider 封装了 ImageCache 的访问;

当 Image Widget 被加入 Widget 树,就会触发图片的加载,加载完后就会自动请求解码,加载和解码是连在一起不可分割的;

解码完成后 Image Widget 才会产生 RawImage 作为 child Widget 真正显示图片。

ImageCache 图片缓存

class ImageCache {

final Map _pendingImages = {};

final Map _cache = {};

final Map _liveImages = {};

ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(),

{ImageErrorListener onError}) {

...

}

}

当 ImageProvide.resolve 被调用时,它会去调用 ImageCache.putIfAbsent 生成 Cache Entry(Key 由 ImageProvider 产生),返回一个 ImageStreamCompleter 对象用于监听图片加载和解码完成的情况,如果已经有缓存的 Entry,则直接返回。

ImageCache 实际上有三个 Pool,分别是 Pending,Cache 和 Live Pool,一个新的 Entry 一开始会被加入到这三个 Pool 中。Pending Pool 用来跟踪正在加载和解码的图片,当图片加载和解码完成后,ImageCache 会自动移除 Pending Pool 相应的 Entry。Live Pool 是用来跟踪使用中的图片,当 Image Widget 移除或者更换图片,或者 Image Widget 自身被移除,ImageCache 会从 Live Pool 移除相应的 Entry。如果图片缓存的数量和内存占用大小没有超过 ImageCache 的上限,Cache Pool 就会一直保留 Cache Entry,如果超过则按 LRU 进行释放。只有 ImageCache 从所有 Pool 都释放了同一个图片的 Entry,该图片解码后生成的纹理内存才会真正被释放。

我们可以通过一个实际的场景来说明 ImageCache 的处理逻辑。假设 ImageCache 缓存的限制是 100M(100M 也是 Flutter 的默认值),我们的 UI 陆续加入 200 个 Image Widget,每个 Image Widget 显示一个 512x512 的图片,每个图片解码后的纹理内存占用为 1M。当 UI 加入 100 个 Image Widget 的时候,Live Pool 和 Cache Pool 都有对应 100 个 Entey,假设图片都已经加载和解码完毕,Pending Pool 里面的 Entry 被全部移除,当前总的图片纹理缓存占用为 100M;

当加入 101 个 Image Widget,并且图片加载和解码完毕的时候,Live Pool 里面有 101 个 Entry,但是 Cache Pool 因为超过上限,最初的 Entry 被移除,只保留了后面 100 个 Entry,当前总的图片纹理缓存占用为 101M;

当加入 200 个 Image Widget,并且图片加载和解码完毕的时候,Live Pool 里面有 200 个 Entry,但是 Cache Pool 因为超过上限,最初的 100 个 Entry 被移除,只保留了后面 100 个 Entry,当前总的图片纹理缓存占用为 200M;

我们移除这 200 个 Image Widget,Live Pool 的 Entry 被完全移除,但是 Cache Pool 没有超过上限,仍然保留,当前总的图片纹理缓存占用为 100M;

我们使用后 100 张同样的图片重新加入 100 个 Image Widget,因为图片已经存在于 Cache Pool,所以不需要重新加载和解码,ImageCache 会从 Cache Pool 里面取出对应 Entry,并且重新在 Live Pool 生成对应的 Entry,最后 Live Pool 和 Cache Pool 都包含同样的 100 个 Entry,当前总的图片纹理缓存占用为 100M;

我们继续使用前 100 张图片再加入 100 个 Image Widget,因为 Cache Pool 已经移除了对应的 Entry,所以需要重新加载和解码,最终 Live Pool 包含了 200 个 Entry,Cache Pool 包含 100 对应前 100 张图片的 Entry,当前总的图片纹理缓存占用为 200M;

我们再次移除所有的 Image Widget,并且手动设置 ImageCache 的内存上限为 0,这样 ImageCache 会移除 Live Pool 和 Cache Pool 的所有 Entry,当前总的图片纹理缓存占用为 0M;

Flutter 图片缓存设计的一些问题

应该说 Flutter 的图片缓存设计还是比较契合 Native UI 的使用场景的,但是对于一些设计比较糟糕的 UI,或者是自动生成的类 Web 的长页面,这样的设计可能会造成一些灾难性的后果。Flutter 解码的时机非常靠前,如果一次性加入大量的 Image Widget 对象,会马上产生相应数量的加载和解码任务,这可能造成系统较为严重的阻塞,并且部分 Image Widget 实际上可能距离可见区域较远,解码后产生的纹理暂时不会被使用,这造成了内存浪费;

ImageCache 实际上是没有真正封顶的(Live Pool 是无上限的),如果当前的 Widget 树同时包含了大量的 Image Widget,内存峰值可能会非常夸张,很容易造成 OOM;

ImageCache 是在 Framework 层的实现而不是 Engine 层,它的实例由 Widget 层产生,通过 PaintingBinding.instance.imageCache 访问,这意味着每个 FlutterView,每个 Root Isolate 都有一个不同的 ImageCache,如果是混合应用,同时展现多个 FlutterView,不但 ImageCache 的 Live Pool 没法控制,Cache Pool 也会处于叠加的状态,导致内存的峰值会更难以控制;目前已经有不少尝试是先生成 DOM 树,然后再用不同的后端将 DOM 树转换成适合不同渲染引擎的产物,交给对应的引擎去渲染。比如生成真正的 Web DOM 交给 Web 引擎渲染,或者生成 Flutter Widget 树交给 Flutter 渲染。这种代码自动生成的 Widget 树可能存在的一个问题就是可能会一次性生成大量的 Widget,并且同时加入 Widget 树。

flutter图片识别_Flutter 图片解码与缓存管理研究相关推荐

  1. 韩语在线翻译图片识别_图片怎么转表格excel

    图片转表格excel,快来免费用 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 相信大部分小伙伴选择拍图识字 是因为强大的ocr识别功能 图片文字一键提取导出 但是,有多少人知道 拍图识字还可以成为你的私人 ...

  2. flutter图片识别_Flutter 资源和图片

    ## 介绍 Flutter应用程序可以包含代码和 assets(有时称为资源).asset是打包到程序安装包中的,可在运行时访问.常见类型的asset包括静态数据(例如JSON文件),配置文件,图标和 ...

  3. flutter图片识别_flutter实现文字识别之图片拖拽选框选取截取文字

    最近flutter1.0终于正式发布,从目前的布局看这个项目的野心很大,ios/android.桌面,甚至还有web都有相应的解决方案.我自己一直对flutter框架保持关注,并且非常看好.也尝试做了 ...

  4. android 验证码图片识别_图片验证码的识别技术

    这里有最简单的一类验证码:他们有固定的背景颜色,相同的字符颜色和字体,字符的坐标位置也是固定的. 对于这类验证码,我们只需要对每个数字进行采样,建立标准库,然后应用的时候一一对照标准库,就可以轻易做到 ...

  5. 图片识别-合成-图片滤镜等

    支持web端.支持数百种滤镜 https://img.ly/docs/pesdk/ https://img.ly/docs/pesdk/web/introduction/getting_started ...

  6. Atitit java 二维码识别 图片识别

    Atitit java 二维码识别 图片识别 1.1. 解码1 1.2. 首先,我们先说一下二维码一共有40个尺寸.官方叫版本Version.1 1.3. 二维码的样例:2 1.4. 定位图案2 1. ...

  7. python图片识别验证码软件_基于python图片识别工具(图片识别,车牌,PDF,验证码)...

    先上图  不多说. 对于一般的用户来说识别率还是能达到百分之90以上. 已经打包成exe文件.windows用户可以直接使用.要软件的加我QQ python代码: # -*- coding: UTF- ...

  8. flutter 图片压缩_Flutter包大小治理上的探索与实践

    Flutter作为一种全新的响应式.跨平台.高性能的移动开发框架,在性能.稳定性和多端体验一致上都有着较好的表现,自开源以来,已经受到越来越多开发者的喜爱. 但是,Flutter的引入往往带来包体积的 ...

  9. python智能图片识别系统(图片切割、图片识别、区别标识)

    目录 技术介绍 运行效果 关键代码 写在最后 技术介绍 你好! python flask图片识别系统使用到的技术有:图片背景切割.图片格式转换(pdf转png).图片模板匹配.图片区别标识. 运行效果 ...

最新文章

  1. ASP.NET学习笔记(9)--SqlDataReader和SqlDataSet的比较
  2. 台式计算机驱动程序未被安装,电脑未能成功安装设备驱动程序怎么办
  3. 增强环路现象!? 为你揭示云计算魔力!
  4. python中元组的赋值_在Python中,元组可以被赋值修改。( ) (2.0分)_学小易找答案...
  5. matlab学习笔记 struct函数
  6. 企业支付宝转账到银行卡(免费率 无限额)PHP 演示示例
  7. pdf转换成jpg python_Python Wand将PDF转换为JPG background
  8. [git]warning: LF will be replaced by CRLF in 解决办法
  9. java集合中取最大值_Java后台通过Collections获取list集合中最大数,最小数代码
  10. 特写:美国B2C网站惨淡经营
  11. 基于JAVA校园爱心志愿系统计算机毕业设计源码+数据库+lw文档+系统+部署
  12. javaSE探赜索隐之三<类与对象的爱恨情仇上>
  13. utsc的计算机科学,天啊撸:中国留学生淘汰率竟比加拿大学生高出一倍
  14. halcon机器视觉软件系统框架源码visi onpro
  15. 自研数据库CynosDB存储系统如何实现即时恢复
  16. numpy ndarray与matrix互相转换及转换时维度的变换
  17. surfaceflinger原理讲解
  18. Oracle一体机电源灯闪烁,24V电源模块通电后,DC OK灯闪烁,输出24v频繁通断-工业支持中心-西门子中国...
  19. buuctf 九连环 解析
  20. halfstone 原理_逻辑分析仪原理及其参数介绍

热门文章

  1. 2022年最新浙江机动车签字授权人模拟试题及答案
  2. 拿到腾讯 offer 的第二天,我从字节跳动离职了
  3. Java学习路线和资料分析
  4. Android指纹支付 - android M / P 全适配
  5. 【论文笔记——DIM】Learning Deep Representations By Mutual Information Estimation and Maximization
  6. 李沐学AI之数据探索性分析
  7. Mtk Sensor 驱动框架变更简要分析
  8. 吴恩达AI FOR Everyone|人工智能入门笔记|
  9. 高德地图3D轨迹回放 + 视野跟随功能
  10. html左侧悬浮音乐插件,固定在网页底部的HTML5音乐播放器插件代码