图片缓存架构:Android与iOS的底层逻辑
做移动端开发这些年,我最大的体会就是——图片缓存这东西,说白了就是「拿空间换时间」。你想啊,用户滑动列表时每张图都从网络拉,就算网速再快,用户也等不起。所以咱们得搞一套缓存体系,让图片先在内存里待命,内存满了再存磁盘,磁盘要是还不够……嗯,那就得回源了。
Android和iOS虽然平台不同,但缓存思路其实是相通的。我翻过Glide和SDWebImage的源码,发现它们都遵循了「三级缓存」的模式。我把这套架构抽象成下面这张图,你看完就能明白整个链路怎么跑。
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又手动存本地文件,结果磁盘满了都不知道是谁占的。统一一个缓存入口,你想想看,出问题排查也方便。
缓存架构的「避坑」总结
我做了这么多年图片优化,踩过的坑至少能写一篇文章。这里列几个最疼的:
- 内存缓存和磁盘缓存尺寸要联动。如果你内存缓存本来就很大,磁盘缓存再设个超大容量,结果就是内存吃紧的时候磁盘还是满的,两边都不讨好。
- 软引用/弱引用不是银弹。Android的
WeakReference在GC频繁时会被迅速回收,导致缓存命中率极低。我建议以LRU为主,软引用只做辅助层。 - 解码后的Bitmap大小要跟展示控件匹配。你下载一张4000x3000的图,却用一个200x150的ImageView显示,纯粹浪费内存。一定要设置
override()或resize()。
我曾经接手过一个项目,内存缓存用了LinkedHashMap自己手写的LRU,结果忘记加同步锁,多线程下缓存一直乱淘汰。后来换成了Glide内置的LruResourceCache,问题立刻消失。所以啊,能用成熟框架就别自己造轮子。
最后说一句,缓存架构的核心不是「缓存多少」,而是「什么时候淘汰」。你把淘汰策略做对了,内存占用自然就下来了。嗯,今天先聊到这里,你们在实际项目中遇到什么缓存问题,随时可以一起讨论。