【项目踩坑实录】并发环境下,Glide缓存引起的图片加载异常

作者:Lei_official日期:2025/12/16

在现实主义者身上,并不是奇迹产生信仰,而是信仰产生奇迹。——《卡拉马佐夫兄弟》

glide_logo.png

背景简述

在维护智能手表主题管理功能时,我遇到过一个十分有趣的bug,从测试首次发现问题时感到十分困惑且不解,到自己我不断尝试并成功复现,直至最终找到根本原因与解决方案,历经一周左右时间。虽然是存在已久的历史问题,但仍有记录和总结的意义,同时也警醒自己在设计并发模块时,一定要心存敬意、考虑周全。

问题现象

问题的表现如上,用户编辑相册表盘后,返回到表盘列表页,预期是可以展示出新设置的相册表盘的预览图,但实际效果却是,图片确实有刷新出来,但又没有完全刷新,只展示了上半部分,下半部分是黑色。

技术设计方案

这个模块是我中途接手的,在初期接手时就惊讶其功能复杂之高、逻辑嵌套之深。为了更好地理解问题,有必要介绍一下这个功能的技术方案设计。

暂且称之为“手表主题模块”,用来管理智能手表主题的样式,用户可以自定义智能手表上的字体、颜色、布局、背景图等,当用户完成设置后,生成当前配置下的预览图,并保存在应用数据目录下(Android/data/packageName/files/aaa_111.png),其它页面(例如表盘列表页)监听到预览图变化的事件后,更新 UI,展示出最新的预览图。

  • WatchThemePreviewManager:单例类,提供接口更新并保存预览图png,并通知监听者。
  • WatchThemeView:继承自 FrameLayout,是预览图的展示View,在 onAttachedToWindow()/onDetachedFromWindow() 中进行注册/反注册,监听预览图变化。

用户操作流程图.png

预期效果是,用户在APP中操作完主题样式设置后,其它所有展示这个手表表盘的页面,都会加载新的样式图。绝大多数场景下,的确表现如预期所想。但测试偶尔会发现前文图中的bug,新的预览图只展示了上半部分,其下半部分是纯黑色。查看应用数据目录后,发现生成的图片是完整的,并不存在缺失现象;从日志中也可以看到回调确实发生了,结果让人百思不得其解。

归结为2个问题

  • Q1:为什么图片只展示了局部,另一部分是纯黑的?
  • Q2:为什么回调发生后,没有把正常的图片刷新出来?

最终分析结论

略去中间繁琐的分析过程(无非就是在关键节点增加日志、打断点逐步调试等),直接将结论奉上。

  • A1:使用 Glide 显示 png 文件时,文件虽然存在,但其内容尚未完全写入。
  • A2:Glide 加载同名文件,命中磁盘缓存,不会重新读取文件。

Q1:图片加载不完整的原因

虽然已经在代码里考虑到,当 png 文件保存完成(大约300ms)后,才回调通知监听者。但存在极限场景,即在“表盘编辑页”编辑后,快速返回到上一级的“表盘列表页”,由于“表盘列表页”在onRestart()时刷新界面,会读取到最新的预览图png路径,文件此时已存在,但尚未完成写入,因此 Glide 加载到的是只写入部分内容的 png,从而发生了图中的错误场景。

A1:解决方案-先写tmp文件再rename

实践中,对于这种保存数据到文件的场景,一般采用“保存临时文件->rename”的方案,先把数据写入到临时文件f.tmp中,写入完成后,再将其rename为f,可以避免外部发生“读取部分文件”的场景。

这里还使用了 FileOutputStream.flush()FileOutputStream.fd.sync(),对于 高频写入&读取 的场景,这样做可以保障文件被迅速推给内核和落盘持久化。但 sync() 函数有性能损耗,在工程中慎用。

样例代码如下:

1/**
2 * 原子性保存图片文件,外部不会读取到一部份文件
3 */
4fun saveBitmapToFileAtomic(bitmap: Bitmap, path: String): Boolean {
5    val dst = File(path)
6    dst.parentFile?.mkdirs()
7
8    val tmp = File(dst.parentFile, dst.name + ".tmp")
9
10    return try {
11        FileOutputStream(tmp).use { out ->
12            if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)) return false
13            out.flush() // 把文件推给内核
14            out.fd.sync() // 把文件强制落盘(更稳,但更慢,勿大批量调用),适用于写完立刻读的场景
15        }
16
17        // renameTo 在同一分区通常是“原子替换”效果:读者要么看到旧文件,要么看到新完整文件
18        if (dst.exists()) dst.delete()
19        tmp.renameTo(dst)
20    } catch (e: Exception) {
21        tmp.delete()
22        false
23    }
24}
25

Q2:回调后没有触发重新加载图片的原因

对于同一个 ImageView,使用 Glide 加载同名文件,如果不增加文件签名校验,会导致直接复用前一次产生的缓存。这也就解释了,为什么当真正完成 png 文件保存后,回调触发时,预览图并没有刷新为正常版本。

A2:解决方案-增加signature

解决方法是,在调用 Glide 加载图片时,使用 signature() 函数,用于在原有的缓存Key计算方法上增加唯一性校验,其内部使用参数的 equals()hashCode() 实现。接口文档如下:

1public RequestOptions signature(@NonNull Key signature)
2
3Sets some additional data to be mixed in to the memory and disk cache keys allowing the caller more control over when cached data is invalidated.
4
5Note - The signature does not replace the cache key, it is purely additive.
6
7Parameters:
8signature - A unique non-null Key representing the current state of the model that will be mixed in to the cache key.
9
10Returns:
11This request builder.
12
13See Also:
14ObjectKey
15

因此,在使用Glide为此 ImageView 加载图片时,对于文件名不变但内容可能发生变化的场景,建议进一步增加签名校验,常见的 signature 参数有 file.lastModified()、文件字节数、md5等。

这里我使用 文件长度_更新时间,作为唯一key,可以解决文件内容更新的问题。

1private fun loadPngIntoImageView(imageView: ImageView, pngFile: File) {
2    glide.with(context)
3        .load(pngFile)
4        .signature(ObjectKey("${file.length()}_${file.lastModified()}")) // 文件长度_更新时间戳,解决文件更新问题
5        // 
6        .into(imageView)
7}
8

写在最后的反思

  1. 这种“先写tmp文件,然后重命名”的模式,适用于大多数写文件的场景。笔者之前开发apk下载工具时,也处理过类似问题,没有下载完成的apk文件,也会被资源管理器识别成安装包,但解析必定失败。
  2. 缓存虽好,使用要谨慎。使用Glide加载图片文件,在启用缓存的情况下,如果文件名不变但文件内容发生变化,是不会读取更新后的内容的。

【项目踩坑实录】并发环境下,Glide缓存引起的图片加载异常》 是转载文章,点击查看原文


相关推荐


计算机十万个为什么--数据库索引
无限大62025/12/8

计算机十万个为什么--数据库索引 大家好,欢迎来到最新一期的无限大博客。 突然发现自己对数据库相关的内容掌握不够扎实,于是就去学习了一下,顺便也将自己的理解写成了一篇博客。 希望这篇文章能对大家有所帮助 数据库索引:给数据仓库装个"智能导航系统" 🧭 想象一下,你走进一个占地 1000 平方米的超级图书馆 📚,里面塞满了几十万本书,却连个分类牌都没有。老板忽然喊你找一本《数据库从入门到放弃》,你是不是当场想表演一个原地消失术?😱 这就是没有索引的数据库的日常!每次查询都像蒙眼找书,全表


失业7个月,我把公司开起来了:一个程序媛的“野蛮生长”
后端小肥肠2025/11/28

大家好,我是小肥肠。 4月被裁,11月注册公司。 这7个月,我一个人赚回了以前一年的工资,也攒够了人生第一台CC的首付。今天不讲技术,聊聊这半年一个程序媛的野蛮生长。 1. 半年了我开起了公司 从4月到现在已经创业半年多了(7个月),这7个月以来,我从一个一无所有的失业人到现在攒够了一台cc的首付(赚的比以前上班一年还多),我的共学社群实现了从0到现在的300多人。 其中有很多和我一样的程序员,他们都是被我的文章吸引来共学群一起成长,也有很多小白进来一步一步成长为可以自行搭建自己的智能体。 在这


面向课堂与自习场景的智能坐姿识别系统——从行为感知到可视化部署的完整工程【YOLOv8】
我是杰尼2025/12/24

面向课堂与自习场景的智能坐姿识别系统——从行为感知到可视化部署的完整工程【YOLOv8】 一、研究背景:为什么要做“坐姿识别”? 在信息化学习与办公环境中,久坐与不良坐姿已成为青少年与上班族普遍面临的健康问题。长期驼背、前倾、低头等坐姿行为,容易引发: 脊柱侧弯、颈椎病 注意力下降、学习效率降低 视觉疲劳与肌肉劳损 传统的坐姿管理主要依赖人工监督或简单硬件传感器,不仅成本高、实时性差,而且难以规模化推广。 随着计算机视觉与深度学习技术的发展,基于摄像头的坐姿自动识别系统逐渐成为一种可行且低成


fmtlib/fmt仓库熟悉
LumiTiger2026/1/2

一、仓库(fmtlib/fmt)依赖/用到的开源库 fmt 核心设计为无外部运行时依赖(self-contained),仅在特定功能/实现中引用少量开源算法/工具(非链接依赖): Dragonbox: 内嵌该开源算法(https://github.com/jk-jeon/dragonbox),用于实现 IEEE 754 浮点数的高性能格式化(保证正确舍入、短长度、往返一致性),是 fmt 浮点格式化的核心实现基础。构建/测试类工具(非业务依赖): CMake:跨平台构建系统;oss-f


JNI是什么?
自由生长20242026/1/11

JNI是什么? JNI(Java Native Interface,Java本地接口)是Java平台自1.1版本起提供的标准编程接口,它是一套强大的编程框架,允许运行在Java虚拟机(JVM)中的Java代码与用C、C++等其他编程语言编写的本地代码进行交互。 核心特点 功能扩展:允许Java程序调用本地代码,实现标准Java类库无法支持的功能 性能优化:对于性能敏感的计算密集型任务(如图像处理、音视频编解码、复杂数学运算),本地代码通常比Java实现更高效 代码复用:可以重用已有的C/C++


ooder-agent v0.6.2 升级实测:SDK 封装 + Skill 化 VFS,AI 一键生成分布式存储应用
OneCodeCN2026/1/19

作为一名深耕分布式Agent框架的开发者,我踩过最多的坑,就是分布式存储的配置复杂、断网数据丢失、自定义应用开发成本高这三大难题。 直到上手 ooder-agent v0.6.2 版本,我才发现原来分布式存储应用可以这么简单——这次升级直接把两个核心痛点连根拔起:agent-sdk 深度封装降低开发门槛,skill-vfs 变身完整Skill程序适配复杂网络场景,更关键的是,AI一句话就能生成存储应用,零代码自动部署。 今天就从技术角度,聊聊这次升级的两大核心亮点和实际使用价值。 一、核心升级1

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客