图片缓存区太大的案例
1、案例描述
在我们的Flutter打包的app上,出现测试人员对首页的无限列表疯狂频繁刷,冷启动刷的时候还好,越刷越后面越卡。
2、案例分析过程
经测试验证在其他机器上无法重现。后通过观察应用的磁盘占比,发现出现崩溃的该设备该应用的磁盘占比巨大,达好几G(只要占用有网络点播视频缓存、打赏动画视频、图片)。进一步观察发现在图片不大的情况下,其中图片的占比竟然也能接近上G,说明图片数量巨大。由此猜测是图片数量太多导致。随后通过高磁盘占用环境模拟,确实也出现了同样的情况。由此我们开始设想磁盘对应用性能的影响 的几种原因,并进行对应修复。经过代码排查图片库内部的同步、阻塞问题,发现了一个潜在的同步危险。
1 | if (cacheFlie.existsSync()) { /// existsSync是一个同步方法,用于检查具有该路径的文件系统实体是否存在。 |
3、为什么原生iOS上没发现类似问题/微信为什么没出现类似问题?
可能原因:①之前原图相同的图片显示在不同地方,也只是原图和缩略图两套而已,而不像图片万象这样,可能会存在好多套。②原生的SDWebImage在监测到内存不足的时候会自动进行清除。
图片缓存(无、太小、太大)的问题
小结:
| 图片缓冲区默认太小时(一定有问题) | 图片缓冲区设大后(可能有问题) | |
|---|---|---|
| 出现的平台 | Flutter上默认的缓存上限1000个图片或者100MB内。 原生iOS的SDWebImage一般不设大小,只根据maxCacheAge |
Flutter上extended_image判断图片是否已缓存时候有性能问题,而导致卡顿。原因滑动列表会进行很多图片的获取,从而由于任务过多,对性能产生影响,例如增加CPU使用率,导致电池消耗加快,进而可能引发崩溃。 |
| 后果 | 加载过的图片很容易再次出现。 | 可能:存的图片数量太大。影响图片存取,而崩溃。 |
| 出现的案例 | ①列表划过之后再回来; ②列表页面离开后再回来。 |
测试人员频繁刷,导致刷满图片缓冲区后,图片数量太大,影响图片存取,从而崩溃。 |
| 解决方案 | 图片存储进行分区。对 url 进行 md5 取值,对 md5 取前两个字母为新文件夹。 |
1、图片缓冲区默认太小时
在Flutter中加载图片(一般是网络图片),我们常常会遇到下面几个问题:Flutter 图片缓存问题分析
同页面内,加载过的图片,再次出现的时候,会重新加载,特别是列表的图片;
根源:Flutter 内置的缓存机制 PaintingBinding.instance.imageCache 的
maximumSize和maximumSizeBytes属性默认的缓存上限1000个图片或者100MB内。1
2const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB即图片没有加载到100MB,加载到1000个图片,也会开始根据LRU的规则清理释放缓存。在某些情况下,比如电商,一整个页面80%的元素都用图片占满,小到图标,大到广告banner,个数很容易就到1000了,但是经过压缩剪裁,图片普遍都控制在几十kb甚至10kb以下,即使1000张也远远达不到内存的上限,并且100MB的上限对于某些机型来讲也相对较小了。
解决:设置大一点
1
2PaintingBinding.instance.imageCache.maximumSize = 10000;
PaintingBinding.instance.imageCache.maximumSizeBytes = 800 << 20; // 800 MiB附:除了 Flutter 内置的缓存机制外,还有第三方库如
cached_network_image提供了更丰富的图片缓存功能,包括硬盘缓存等 。如果需要更复杂的图片缓存策略,可以考虑使用这类第三方库来扩展 Flutter 的图片加载和缓存能力。附2:
extended_image提供了一套完整的图片加载和缓存解决方案,其缓存的控制也还是通过修改ImageCache的配置来控制内存缓存的大小和数量。附2:SDWebImage 默认情况下会缓存图片一周(
maxCacheAge的默认值),并且没有对缓存空间大小设置限制(maxCacheSize默认值未设定),这意味着理论上应用中的图片缓存可以占满整个设备存储空间。1
[SDImageCache sharedImageCache].maxCacheSize = 50 * 1024 * 1024; *// 50MB*
列表快速滑动时,加载完成再往回滑动,之前的图片还是需要重新加载;
根源:等同于按需加载。
解释:加载图片时,为了避免过快滑动,使得同时加载的图片过多导致卡顿甚至崩溃。所以若是内存中已有缓存,则直接返回缓存,若是没有则判断是否在快速滑动,若是正在快速滑动,则下一帧再加入队列处理,若是图片已经被移出屏幕(即没有在tree上),可能会被跳过。
解决:快速滑动如此,可不处理。
有时返回上一页面时,上一页面已经加载完成的图片,会重新加载,假如没有占位图会特别明显的闪动。
同现象1一样。
2、图片缓冲区设大后
图片分区:
SDWebImage 所有的图片都放在 _diskCachePath 目录下,_diskCachePath是只有一层吗?还是里面还会再分文件目录?
SDWebImage 的
_diskCachePath目录是 SDWebImage 用来存储磁盘缓存的根目录。在这个根目录下,SDWebImage 会进一步组织文件结构,以避免所有图片都放在单一目录下造成文件系统性能问题。具体来说,**图片文件会根据图片 URL 的 MD5 值进行散列,然后根据这个散列值来创建子目录和文件名,从而在磁盘上形成一个树状的目录结构 **。这种目录结构的设计有助于分散文件系统操作,减少单个目录下的文件数量,从而提高文件检索和写入的性能。此外,SDWebImage 还提供了设置选项,允许开发者自定义图片的存储路径,以及通过
addReadOnlyCachePath:方法添加额外的只读缓存路径 。总的来说,
_diskCachePath不是单一层级,而是一个包含多个子目录的复杂结构,每个子目录下存储着根据 MD5 散列值组织好的图片文件,以此来优化磁盘缓存的性能和效率 。
一、磁盘对应用性能的影响
1、原因猜测
可能原因:应用使用过程时候,存在对磁盘中的某个文件的操作,从而导致应用性能问题。操作的可能性如下:
| 序号 | 阶段 | 如果 | 后果 |
|---|---|---|---|
| 1 | 访问 | 文件量太大 | 访问效率问题 |
| 2 | 读写 | 文件大小太大 | 读写问题 |
2、排查方案
图片来源于:高磁盘占用的排查与优化.graffle
二、高磁盘占用环境模拟
| 步骤 | 阶段 | 操作 | 目的 |
|---|---|---|---|
| 1 | 资源准备1 | 提供图片的万象接口,以通过万象能获取到各种图片地址 | 避免得去找非常多的图片数据源 |
| 2 | 资源准备2 | 提供下载万象图片的接口(指定宽范围),以通过万象图片能下载到很多图片 | 避免得去找非常多的图片数据源 |
| 3 | 资源下载 | 下载指定宽范围的万象图片,得到各种图片 每次只添加 100 张,避免模拟功能在同步状态下执行出错; |
通过下载占据磁盘空间 |
| 4 | 数据验证1 | 打印磁盘(沙盒)目录,验证下载 iOS-查看沙盒文件(真机+模拟器) |
验证确实磁盘空间被增加 |
| 5 | 数据验证2 | 计算磁盘大小 | 验证确实磁盘空间被增加 |
相应的代码实现,见 附1:高磁盘占用环境模拟的相关代码
三、高磁盘占用环境优化
| 序号 | 解决 | 方案 |
|---|---|---|
| 1 | 避免单文件夹太大 | 存储分区处理 |
| 2 | 单文件夹已经太大 | 存储空间清理 |
1、避免单文件夹太大—-存储分区处理
1、背景:图片文件下文件数量太多,导致搜索效率下降
1 | final Directory _cacheImagesDirectory = Directory( |
2、优化方案:对图片存储,进行分区存储。获取方式同理。
3、优化步骤如下
对 url 进行 md5 取值
对 md5 取首字母为新文件夹
将该url所对应的图片存储在图片存储目录下的子目录
1 | /// get md5 from key |
4、优化验证
1 | /// 计算图片文件的查找耗时(查找失败返回-1) |
2、单文件夹已经太大—-存储空间清理
文件的属性有创建时间、修改时间
原理:变化过期时间,遍历文件属性,删除修改时间大于该过期时间的文件
1、设置过期时间的初始值,遍历文件属性,删除修改时间大于该过期时间的文件;
2、检查清理后的大小,如果大小还不在规定大小内,则继续清理;
3、继续清理:设置每次清理的过期时间增加值,对过期时间的初始值增加,并重复以上步骤;
四、优化方案的实施
目的:避免功能异常,出现集体性问题。
灰度方案:请参照 灰度系统
五、延伸
问题:一旦修改分区规则(如原本使用url进行md5后的首位值,后面改成md5后的两位值),旧文件就得重新下载。
解决:使用一致性哈希算法。16 张图解带你掌握一致性哈希算法
1、对存储节点进行hash,创建节点和hash值的哈希映射,如 {100001: “a”, 300001: “a”, 400001: “a”}
2、对数据url进行hash寻址
- 首先,对 key 进行哈希计算,确定此 key 在环上的位置;
- 然后,从这个位置沿着顺时针方向走,遇到的第一节点就是存储 key 的节点。(对「数据」进行哈希映射得到一个结果要怎么找到存储该数据的节点呢?)
结果:在一致哈希算法中,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响。
假设节点数量从 3 减少到了 2,比如将节点 A 移除:你可以看到,key-02 和 key-03 不会受到影响,只有 key-01 需要被迁移节点 B。
隐患:一致性哈希算法虽然减少了数据迁移量,但是存在节点分布不均匀的问题。如下:
说明:一致哈希算法也用了取模运算,但与哈希算法不同的是,哈希算法是对节点的数量进行取模运算,而一致哈希算法是对 2^32 进行取模运算,是一个固定的值。
附:hash算法
使用一致性哈希(Consistent Hashing)可以帮助您将大批图片文件分散存储到不同的文件中,其中每个文件对应一个字母或数字。一致性哈希算法能够提供高效的数据分布和负载均衡,同时允许动态添加或删除文件节点。
1 | import 'dart:convert'; |
附1:高磁盘占用环境模拟的相关代码
1、ts_toomany_image_data_vientiane.dart
1 | /// [图片处理方式:快速缩略模板](https://cloud.tencent.com/document/product/460/6929) |
2、ts_toomany_image_download_util.dart
1 | import 'dart:io'; |
3、ts_toomany_image_optimize_check_util.dart
1 | import 'dart:convert'; |