【项目踩坑实录】并发环境下,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缓存引起的图片加载异常》 是转载文章,点击查看原文


相关推荐


C++ 波澜壮阔 40 年:从 C with Classes 到现代 C++ 的进化史诗
雾忱星2025/12/7

🔥@晨非辰Tong: 个人主页 👀专栏:《数据结构与算法入门指南》、《C++学习之旅》 💪学习阶段:C语言、数据结构与算法初学者 ⏳“人理解迭代,神理解递归。” 文章目录 引言一、波澜壮阔的C++“发家”历史1. 1 C with Classes (1979-1983)-起源1.2 早期发展:C++的"++"从何来(1983-1989)1.3 标准化与成熟(1990-1998)1.4 现代化浪潮(2011-至今) 二、推荐C++学习参考网站、好用书籍


微服务项目开发环境
努力-坚持2025/11/28

一、微服务项目开发环境 搭建微服务项目开发环境: 注册中心nacos:登录然后查看注册服务 配置中心nacos:登录然后查看配置 消息队列RabbitMQ:管理界面,登陆后查看消息队列 Redis:


面向课堂与自习场景的智能坐姿识别系统——从行为感知到可视化部署的完整工程【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


Settings,变量保存
cfqq19892026/1/28

作用: 变量在exe文件内。比txt操作方便。 步骤: 就这么简单: Settings.Default.Save();  // 放到窗口关闭事件中。 private void Form1_Load(object sender, EventArgs e) { fa = new FA(); //【4】订阅委托广播 fa.wt_get += wt_get; //


2026 AI Agent 风口必看|四大技术变革+多Agent实战
User_芊芊君子2026/2/6

🎁个人主页:User_芊芊君子 🎉欢迎大家点赞👍评论📝收藏⭐文章 🔍系列专栏:AI 文章目录: 一、先破后立:2026年AI Agent的核心变革(新颖切入点)1.1 变革1:架构升级——从“四段式”到“PDA+记忆+反思”闭环1.2 变革2:协同升级——A2A协议主导,多Agent协作常态化1.3 变革3:工具升级——MCP协议统一,工具调用标准化1.4 变革4:能力升级——Skills模块化,Agent能力可复用 二、实战落地:2026年多Agent协作项目(


字节发力,豆包大模型2.0 震撼来袭(附 Trae 实测)
苍何2026/2/15

这是苍何的第 496 篇原创! 大家好,我是苍何。 其实在早些时候,我就深度参与了豆包大模型2.0 的内测。 今天,终于,豆包大模型 2.0 正式发布了。 说实话,这次的升级幅度,属实把我整不会了。 先说结论:「豆包 2.0 Pro 全面对标 GPT 5.2 和 Gemini 3 Pro」。 「人类最后的考试」HLE-Text 拿下 54.2 分最高分,ICPC 编程竞赛金牌,IMO 数学奥赛也是金牌。 好家伙,字节这是要掀桌子啊。 豆包 2.0,到底升级了啥 这次发布的是一整个系列,包含 P


再论自然数全加和 - 质数螺旋
铸人2026/2/23

下面考虑质数螺旋 曾经以1开始绘制螺旋图,但是计算质数坐标的时候就出现困难。所以我们用0开始,并把它放在螺旋的中心。 观察如下图像, 最中心的数字0,不算大小。圈数为 ,对应的数的个数,也就是面积为, 这些圈的最小值是0,最大值是, 相邻两项的差为, 这是一个二阶等差数列,对应的数值的和为, 这些数值,并不关心旋转的起点。仔细观察我们发现这些质数构成的线都几乎都是对角线,相当于旋转了45°的结果,既然如此,我们把起点旋转45°,看看能不能把斜线变成横竖的直线。

首页编辑器站点地图

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

Copyright © 2026 XYZ博客