图片缓存架构:Android与iOS的底层逻辑

做移动端开发这些年,我最大的体会就是——图片缓存这东西,说白了就是「拿空间换时间」。你想啊,用户滑动列表时每张图都从网络拉,就算网速再快,用户也等不起。所以咱们得搞一套缓存体系,让图片先在内存里待命,内存满了再存磁盘,磁盘要是还不够……嗯,那就得回源了。

Android和iOS虽然平台不同,但缓存思路其实是相通的。我翻过Glide和SDWebImage的源码,发现它们都遵循了「三级缓存」的模式。我把这套架构抽象成下面这张图,你看完就能明白整个链路怎么跑。

图片缓存架构核心流程 1. 网络资源 2. 磁盘缓存 3. 内存缓存 (LRU算法 + 软引用/弱引用) 4. 解码 → Bitmap/UIImage 5. 显示到ImageView/UIView 关键策略 1. 内存命中:直接显示 2. 磁盘命中:加载到内存 3. 未命中:下载→磁盘→内存 4. LRU淘汰:移除最久未用 5. 磁盘容量控制:定期清理 6. 内存软/弱引用: 避免OOM

Android平台的缓存架构

Android这边,我常用的框架是Glide,其次是Picasso和Coil。它们的核心都是三级缓存:内存→磁盘→网络。但细节上有不少门道。

1. 内存缓存

Glide默认使用LruCache,大小是App可用内存的1/8。我习惯在低端机上手动调小这个比例,比如1/16。为什么呢?因为之前有个项目在红米Note上频繁OOM,排查下来就是默认缓存太大。

// 自定义Glide内存缓存大小
int memoryCacheSize = (int) (Runtime.getRuntime().maxMemory() / 8);
Glide.get(context).setMemorySizeMultiplier(0.5f); 
// 或者直接覆盖LruResourceCache

还有一个容易被忽略的点——Bitmap复用。Glide内部用BitmapPool来回收和重用Bitmap,避免频繁GC。我见过很多团队只配置了LruCache,却忘了调BitmapPool的大小,结果内存碎片化严重。

2. 磁盘缓存

磁盘缓存默认在cache/glide目录,大小250MB。我记得有一次用户反馈APP越用越卡,一看磁盘缓存已经撑到1.2GB了,原来是下载的图片分辨率太大。后来我把磁盘缓存改成按文件数量+总大小双重限制,问题才解决。

Android平台常见的磁盘缓存策略对比:

缓存策略 触发条件 优缺点
LRU(最少最近使用) 超出缓存上限 实现简单,但对频繁使用的多图不友好
LFU(最少使用频率) 使用频率最低 避免热点图片被误删,但计算开销大
混合策略(LRU+TTL) 容量+过期时间 兼顾时效性,我目前最推荐

我的小技巧:在Glide的diskCacheStrategy里,我一般用DiskCacheStrategy.AUTOMATIC。如果你需要图片实时更新(比如头像),记得加上signature()或者使用onlyRetrieveFromCache绕过磁盘缓存。

iOS平台的缓存架构

iOS这边,我用得最多的就是SDWebImage和Kingfisher。它们的架构更简洁一些,因为iOS的内存管理和Android不太一样——ARC帮你管理对象,但图片解码后生成的UIImage占用内存还是得自己操心。

1. 内存缓存

SDWebImage内部用NSCache,它会在系统内存紧张时自动释放对象。我遇到过一个问题:某次内存警告时,NSCache直接把所有缓存全清了,导致列表滑动时大量图片重加载。后来我给NSCache设置了countLimit和totalCostLimit,让它在清理时更「温柔」一点。

// SDWebImage内存缓存配置示例
SDImageCacheConfig *config = [SDImageCacheConfig defaultCacheConfig];
config.maxMemoryCount = 200;  // 最多缓存200张图
config.maxMemoryCost = 80 * 1024 * 1024; // 80MB
config.shouldCacheImagesInMemory = YES;

Kingfisher则使用MemoryStorage搭配LRU算法,默认大小也是可用内存的1/4。我个人感觉Kingfisher的内存控制更精细一些,比如它可以单独设置memoryStorage.config.expiration来让缓存更快过期。

2. 磁盘缓存

iOS的磁盘缓存默认在Library/Caches/目录,SDWebImage使用文件系统直接存储图片数据。Kingfisher则把图片序列化成文件,存储元数据到SQLite。

我对比过两者的性能:

  • SDWebImage:文件系统读取快,但大量小文件时inode占用高,适合图片总数可控的场景。
  • Kingfisher:SQLite管理元数据,清理策略更灵活,适合长期运行的应用。

注意:千万不要同时使用两种缓存框架,我见过有的项目既用SDWebImage又手动存本地文件,结果磁盘满了都不知道是谁占的。统一一个缓存入口,你想想看,出问题排查也方便。

缓存架构的「避坑」总结

我做了这么多年图片优化,踩过的坑至少能写一篇文章。这里列几个最疼的:

  1. 内存缓存和磁盘缓存尺寸要联动。如果你内存缓存本来就很大,磁盘缓存再设个超大容量,结果就是内存吃紧的时候磁盘还是满的,两边都不讨好。
  2. 软引用/弱引用不是银弹。Android的WeakReference在GC频繁时会被迅速回收,导致缓存命中率极低。我建议以LRU为主,软引用只做辅助层。
  3. 解码后的Bitmap大小要跟展示控件匹配。你下载一张4000x3000的图,却用一个200x150的ImageView显示,纯粹浪费内存。一定要设置override()或resize()。

我曾经接手过一个项目,内存缓存用了LinkedHashMap自己手写的LRU,结果忘记加同步锁,多线程下缓存一直乱淘汰。后来换成了Glide内置的LruResourceCache,问题立刻消失。所以啊,能用成熟框架就别自己造轮子。

最后说一句,缓存架构的核心不是「缓存多少」,而是「什么时候淘汰」。你把淘汰策略做对了,内存占用自然就下来了。嗯,今天先聊到这里,你们在实际项目中遇到什么缓存问题,随时可以一起讨论。