ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SDWebImage图片加载本地缓存清理:从机制到实战的完整指南

SDWebImage图片加载本地缓存清理:从机制到实战的完整指南 用过SDWebImage的iOS开发者十有八九都是直接一行sd_setImageWithURL:把图片加载就完事了。但真正到了产品上线、用户量起来之后图片缓存导致的磁盘膨胀、内存警告、加载失效、甚至审核被拒本地缓存过大这些问题才会让人意识到——图片加载只是开始缓存管理才是真正的技术活。这篇文章就围绕“SDWebImage图片加载本地缓存清理”这个主题把我这些年在这上面踩过的坑、常用的清理方案、底层原理和一些排查经验全部梳理出来。无论你是在做社交App、电商App还是工具类产品只要你用了SDWebImage加载网络图片这篇内容都能给你提供一套可以“抄作业”的缓存管理方案。1. SDWebImage缓存机制到底是怎么工作的写清理逻辑之前必须先搞清楚SDWebImage的缓存结构。很多人只知道它“有缓存”但不知道它其实是有两级缓存、三层结构的。如果你不清楚这些后面写清理代码的时候就会发现自己总是删了一部分另一部分没删掉缓存大小统计也不准。1.1 两级缓存内存缓存和磁盘缓存SDWebImage的缓存核心在SDImageCache这个类它内部维护了两套独立但协作的缓存机制内存缓存Memory Cache基于NSCache实现的速度极快读取图片时不走文件IO直接内存命中返回。内存缓存的清理由系统内存警告触发也可以手动调用clearMemory清理。默认配置下内存缓存的总成本上限是totalCostLimit通常设置为设备内存的1/1000左右比如2GB内存的手机缓存上限大概就是 2MB × 1024 ≈ 2048KB实际上SDWebImage默认用的是SDMemoryCache自己实现的totalCostLimit逻辑。磁盘缓存Disk Cache基于文件系统实现默认存储路径是Library/Caches/default/com.hackemist.SDImageCache/default/这个路径在真机和模拟器上会有差异。磁盘缓存的读取速度比内存缓存慢几个量级但优势是容量不受内存限制可以存储大量图片。磁盘缓存默认没有大小上限它会一直增长直到你用deleteOldFiles或者clearDisk手动清理。这两级缓存的配合逻辑是读取图片时SDWebImage先从内存缓存查找没找到再从磁盘缓存查找再没找到才走网络下载。写入图片时图片下载完成后先写内存缓存再异步写入磁盘缓存。这里有一个很多人不知道的细节磁盘缓存分两个子目录——Config和Data。Data目录存放的是实际图片文件二进制数据Config目录存放的是缓存元信息文件名、过期时间等iOS 13 之后SDWebImage 对磁盘缓存目录启用了.dataProtectionKey等文件属性可能你在文件系统里看到的结构和早先版本会稍有不同但清理逻辑并不会因此改变。1.2 缓存键Key和文件名映射规则SDWebImage的缓存键默认就是图片的URL字符串但存到磁盘时文件名是经过MD5哈希处理的。比如你加载https://example.com/header.png磁盘上的文件名就不是header.png而是9e107d9d372bb6826bd81d3542a419d6这样的32位MD5字符串无扩展名。这就带来了一个重要的实操影响你没法直接在磁盘上通过人眼识别哪张图是哪个URL的。想要精确清理某一张图片的缓存必须用SDImageCache提供的方法而不能自己去Data目录里找文件名来删。我第一次写清理功能的时候就试过直接遍历Data目录删文件最后发现目录结构和我想的完全不一样而且直接操作文件系统容易导致缓存元数据不一致。后来才老老实实改回用API操作。1.3 缓存生命周期没有默认过期时间很多开发者以为SDWebImage的磁盘缓存会像HTTP缓存那样自动过期这是一个误区。SDWebImage默认设置的缓存过期时间是1周7天但是——这个过期时间只会在调用deleteOldFiles时才会被真正执行。换句话说如果你一直不调用deleteOldFiles磁盘缓存会一直保留所有图片永不自动清理除非系统空间不足时清掉整个App沙盒目录。这就是为什么很多App运行几个月后Caches目录能膨胀到几个GB的原因。所以主动的缓存清理不是可选项而是必须项。2. 本地缓存清理方案设计与选型了解了SDWebImage的缓存机制后清理方案的思路就清晰了。但“清理”不是简单调一句clearDisk就完事你需要覆盖不同的业务场景每种场景对应的策略都不一样。2.1 全量清理适合用户手动清缓存用户在“设置-清理缓存”页面点击删除时我们需要把内存缓存和磁盘缓存全部清空同时计算清理前缓存大小展示给用户。手动清理的核心代码如下// 清理前的图片缓存大小字节 NSUInteger cacheSize [[SDImageCache sharedImageCache] totalDiskSize]; // 先清理内存缓存立即生效 [[SDImageCache sharedImageCache] clearMemory]; // 清理磁盘缓存异步执行 [[SDImageCache sharedImageCache] clearDiskOnCompletion:^{ NSLog(磁盘缓存清理完成释放空间%lu bytes, (unsigned long)cacheSize); }];注意totalDiskSize方法的返回是异步的在老版本里是同步的新版SDWebImage改成了异步回调方式你需要在回调里拿到结果再展示给用户[[SDImageCache sharedImageCache] totalDiskSizeWithCompletionBlock:^(NSUInteger fileCount, NSUInteger totalSize) { // 在主线程更新UI dispatch_async(dispatch_get_main_queue(), ^{ self.cacheSizeLabel.text [NSString stringWithFormat:%.2fMB, totalSize / 1024.0 / 1024.0]; }); }];手动清理的页面展示上有一个小细节用户看到的“当前缓存大小”往往是上一次启动App时的计算值因为缓存大小计算需要遍历文件系统耗时较长。体验比较好的做法是进入设置页时异步计算一次展示“正在计算…”计算完成后刷新显示清理完成后立即把缓存大小置为0或“已清理”。2.2 增量清理删除过期文件如果不想全量清空而是只想删除超过缓存期限的图片比如只保留最近3天的图片可以调整过期时间然后调用deleteOldFiles// 设置磁盘缓存最长保留期限 SDImageCache *imageCache [SDImageCache sharedImageCache]; imageCache.config.maxDiskAge 3 * 24 * 60 * 60; // 3天 // 删除过期文件 [imageCache deleteOldFilesWithCompletionBlock:^{ NSLog(过期缓存清理完毕); }];deleteOldFiles的底层逻辑是遍历磁盘缓存文件对比文件的lastAccessDate最后访问时间或元数据中的过期时间超过maxDiskAge的直接删除。但这里有一个容易被忽略的问题你修改maxDiskAge后需要调用一次deleteOldFiles才会生效。如果只改了配置不调用清理方法旧文件依然躺在磁盘上。增量清理适合在App启动后做一次耗时较短不会造成明显的性能影响。我一般的做法是App启动后在后台执行一次deleteOldFiles同时配合后续的缓存监控做动态调整。2.3 定时清理防止缓存无限膨胀全量清理和增量清理都是“被动触发”的逻辑定时清理才是保证缓存不无限膨胀的关键。定时清理有两种实现思路方案A基于时间间隔——每次App启动时判断距离上次清理的时间是否超过N天如果超过就清理一次。方案B基于缓存大小阈值——每次图片加载完成后检查当前缓存总大小如果超过设定阈值比如500MB就触发清理。这种方式更精准能保证缓存大小始终在可控范围内。方案B的代码实现如下// 在AppDelegate或图片加载工具类中注册监听 [[NSNotificationCenter defaultCenter] addObserver:self selector:selector(imageCacheDidStoreImage:) name:SDImageCacheDidStoreImageInDiskNotification object:nil]; - (void)imageCacheDidStoreImage:(NSNotification *)notify { __weak typeof(self) weakSelf self; [[SDImageCache sharedImageCache] totalDiskSizeWithCompletionBlock:^(NSUInteger fileCount, NSUInteger totalSize) { __strong typeof(self) strongSelf weakSelf; const NSUInteger maxCacheSize 500 * 1024 * 1024; // 500MB if (totalSize maxCacheSize) { // 超出阈值按缓存时间排序删除部分文件 [strongSelf trimCacheToSize:maxCacheSize * 0.7]; // 清理到阈值的70% } }]; }trimCacheToSize:方法SDWebImage在新版本中直接提供了[[SDImageCache sharedImageCache] deleteCacheToSize:700 * 1024 * 1024]; // 目标缓存上限这个方法会按照文件的访问时间排序优先删除最久没有被访问的文件直到底层缓存总大小降到指定值以下。2.4 按条件清理精准删除指定图片有时候我们需要删除特定图片的缓存——比如用户更换了头像旧头像如果还在缓存里重新加载会拿到旧图。这种场景下全量清理太浪费精准删除才是正解// 按缓存键URL字符串或URL本身删除 [[SDImageCache sharedImageCache] removeImageForKey:https://example.com/avatar/12345.png]; // 带扩展到磁盘磁盘都删除的完整版本 [[SDImageCache sharedImageCache] removeImageForKey:https://example.com/avatar/12345.png fromDisk:YES withCompletion:nil];这里有一个关键细节removeImageForKey:参数传的key默认要和sd_setImageWithURL:的URL字符串一致。如果你在加载时传了带query参数的URL但删除时传的是不带参数的URL那缓存键不匹配删除就失效了。SDWebImage的缓存键就是URL字符串本身没有做标准化处理常见做法是去掉fragment或query做统一但SDWebImage默认不做。所以精准删除方案设计时的最佳实践是在图片URL不变的情况下删除时传URL的absoluteString如果你的URL可能会变化比如加时间戳建议使用sd_setImageLoaderForKey: 自定义缓存键的方式管理。3. 实操集成自己的缓存管理工具类前面讲的是SDWebImage API的用法但真实项目中我强烈建议你不要在业务代码里到处散落clearDisk、deleteOldFiles这些调用。封装一个统一的缓存管理工具类是让缓存逻辑可控、可维护、可监控的关键一步。3.1 工具类的核心设计我实战中用的缓存管理工具类ImageCacheManager包含以下核心能力查询缓存大小内存磁盘分开查询清理全部缓存内存磁盘清理过期缓存按大小阈值清理精准删除某一URL对应的图片缓存缓存监控记录每次清理释放的空间、清理耗时核心接口设计如下// ImageCacheManager.h #import Foundation/Foundation.h typedef void(^CacheSizeCompletion)(NSUInteger memoryBytes, NSUInteger diskBytes); typedef void(^CacheCleanCompletion)(NSUInteger freedBytes); interface ImageCacheManager : NSObject (instancetype)sharedManager; /// 获取当前图片缓存总大小 - (void)getCacheSizeWithCompletion:(CacheSizeCompletion)completion; /// 清理所有图片缓存内存磁盘 - (void)clearAllCacheWithCompletion:(CacheCleanCompletion)completion; /// 清理过期缓存超过maxDiskAge的文件 - (void)clearExpiredCacheWithCompletion:(CacheCleanCompletion)completion; /// 设置磁盘缓存大小上限超过上限自动清理 - (void)setMaxDiskCacheSize:(NSUInteger)maxSize; /// 精确删除指定URL的缓存 - (void)removeCacheForKey:(NSString *)imageURL; end3.2 工具类实现的关键点实现这个工具类时有几个不能忽略的实现细节首先是内存缓存大小的获取。SDWebImage的内存缓存底层是NSCache的子类SDMemoryCache它并没有直接暴露totalCost这样的API。但是你可以通过一个技巧获取近似值——读取缓存的数量和总costNSUInteger memoryCost (NSUInteger)[[SDImageCache sharedImageCache] memoryCache].totalCost;不过更稳妥的做法是把内存缓存大小估算和磁盘缓存大小分开处理内存缓存大小在展示时直接估算或者干脆不展示只展示磁盘缓存大小。因为内存缓存是动态变化的用户在设置页看到的数字每秒都可能不同意义不大。我自己实际项目里展示给用户的“缓存大小”统一指磁盘缓存大小内存缓存的大小只在内部监控时使用。其次是清理耗时的处理。磁盘缓存清理涉及文件系统遍历和删除操作在图片文件数量巨大时比如10万文件全量清理可能需要几秒钟。所以清理操作必须放在后台线程做SDWebImage的clearDiskOnCompletion方法本身就是异步的但如果你自己封装了totalDiskSize遍历逻辑要确保不阻塞主线程。清理期间如果用户连续点击“清理”按钮要加防重复点击逻辑否则会触发多次清理操作。我踩过一个具体的坑用NSFileManager的contentsOfDirectoryAtPath:遍历Data目录时在主线程上执行导致界面卡顿了好几秒。后来我把遍历和清理逻辑封装在工具类里并明确要求所有文件操作都在工具类内部的后台队列执行。3.3 清理时机的最佳实践结合我多个项目的实战经验清理时机可以做如下安排场景策略原因App启动时执行过期清理deleteOldFiles清理过期文件耗时短用户手动清理全量清理clearMemory clearDisk用户明确意图必须彻底收到内存警告只清理内存缓存clearMemory磁盘缓存不清避免重复下载图片缓存总大小超过阈值按大小清理deleteCacheToSize:控制磁盘占用用户更换头像/图片更新精准删除对应URL缓存不必全量清理这里有一个策略细节需要展开说收到内存警告时千万不要也清理磁盘缓存。磁盘缓存的清理成本高很多用户会发现清理磁盘缓存后图片浏览反而变卡了因为所有图片都要重新下载而且内存警告的本质是内存不足磁盘清理对内存压力几乎没有缓解。SDWebImage内部收到内存警告时也默认只清内存缓存磁盘缓存保留。4. 常见问题排查与避坑实录缓存清理相关的坑十个里面有八个是“表面看起来清理了实际没生效”或者“清理了结果引发新问题”。我把这些年遇到的高频问题整理成速查表并附上我的排查思路。4.1 缓存大小统计不准清理前后没变化这是我在技术社区被问得最多的一个问题。很多开发者按照API文档调用totalDiskSize发现返回的大小和实际磁盘上的文件夹大小对不上或者清理后大小变化不明显。排查这个问题的顺序建议如下确认SDWebImage版本SDWebImage 5.x 之后totalDiskSize的行为从同步变成了异步为了性能优化。如果你的代码还按老版本的同步方式写获取到的值可能是不准确的。检查totalDiskSizeWithCompletionBlock:是否正常回调。确认缓存目录是否一致SDWebImage的磁盘缓存目录在iOS系统可能会因为App的移动、备份恢复而改变路径。如果统计的目录和实际缓存写入的目录不是同一个数字自然对不上。打印[[SDImageCache sharedImageCache] diskCachePath]确认实际路径。排除沙盒其他文件Library/Caches目录下不只是SDWebImage的图片缓存还可能存在Snapshots、com.apple.metadata等系统文件、其他SDK比如 Alamofire、SDWebImage同类的Kingfisher写入的缓存。统计时要注意隔离。有一个我实际项目里遇到的坑App使用了HTTPS双向认证 自定义NSURLSessionConfiguration结果URLCache的HTTP缓存NSURLCache和SDWebImage缓存并存导致磁盘空间被双重占用。排查时一直以为是自己清理逻辑写错了后来通过[[NSURLCache sharedURLCache] currentDiskUsage]发现HTTP缓存占了几百MB。4.2 清理后图片加载变慢甚至加载失败清理了磁盘缓存后App里所有图片都要重新走网络下载。在弱网环境下用户体验会明显变差。这种情况不是Bug但可以通过策略优化。我的建议是手动全量清理只清理“不重要的图片缓存”。比如首页 Banner、商品图这些核心图片的缓存可以保留只清理用户头像、文章配图等非核心图片。具体实现上可以根据图片URL的特征比如路径中是否包含/avatar/、/logo/做精准删除而不是一刀切全部清空。另外清理完成后建议预加载当前页面的核心图片避免用户返回页面时看到一片空白// 清理完成后预加载当前可见的核心图片 [[SDWebImageManager sharedManager] loadImageWithURL:currentPageBannerURL options:SDWebImageRetryFailed progress:nil completed:nil];4.3 iOS 14 磁盘目录读取不到的坑iOS 14 之后App沙盒的Caches目录会被系统标记为“不可直接从应用外部读取”虽然开发者工具在Xcode里依然可以看到但设备上通过文件App是看不到的这本身不是新变化。我的实际经验是不要在磁盘上直接操作SDWebImage缓存文件原因有两点iOS 14 之后Library/Caches目录的文件读写有一些系统级别的缓存策略直接删除文件不会立即释放磁盘空间系统会在适当时机回收。SDWebImage 的内部元数据Config文件和实际文件Data文件需要保持一致。你用NSFileManager手动删除了Data文件但Config里仍然记录着该文件存在下次加载时SDWebImage会尝试读取这个已不存在的文件返回nil内存缓存也不会建立导致该图片永远不会被缓存下来直到Config被更新。类似的不要在清理时把整个缓存目录直接删除removeItemAtPath:删掉default目录。这样做第一可能导致SDWebImage在下次写缓存时找不到路径而重建目录本身能自愈但可能造成临时异常第二会导致正在进行的图片加载任务正在写入缓存的文件句柄失效可能引发崩溃。4.4 清理回调不触发的排查在SDWebImage 5.x版本中clearDiskOnCompletion:的回调执行的队列和主线程的关系需要注意。如果回调没有触发或者崩溃先确认是否是以下原因Block循环引用如果你在Block里使用了self要使用__weak typeof(self) weakSelf self。SDImageCache被提前释放如果你用了自定义的SDImageCache实例非单例并且放弃强引用它可能在清理完成前被释放回调随之失效。排查这类问题的标准做法是在清理代码里打日志确认清理方法确实被调用再确认回调线程是否正确。4.5 内存缓存清理后图片闪一下FlickerclearMemory清理内存缓存后屏幕上的图片通过sd_setImageWithURL:显示的并不会自动刷新但当你滚动列表时原本已经在内存中的图片需要重新查找——内存缓存已清磁盘缓存有的话就从磁盘读取。磁盘读取的耗时比内存慢所以在弱网或大图场景下用户会看到图片先显示占位图placeholder然后再渲染出来也就是俗称的“闪一下”。这个问题有几个缓解措施清理内存缓存后手动重新加载当前屏幕可见的图片利用UITableViewCell的复用机制滑动时重新调用sd_setImageWithURL:。清理内存缓存可以延迟到didReceiveMemoryWarning时再执行而不是用户手动清理时执行。用户手动清理只清理磁盘缓存内存缓存保留图片重新加载时能快速命中内存缓存。使用SDWebImageOptions的SDWebImageFromCacheOnly选项强制只从缓存加载如果缓存中没有则显示占位图避免自动走网络下载导致的白屏。5. 进阶自定义缓存清理策略的实战案例讲了API和方案之后用一个我实际落地过的完整案例来串联整个清理体系。5.1 场景描述一个信息流产品每天图片请求量在百万级别图片主要来自CDN平均单张图片50KB-500KB不等。App在用户手机运行一段时间后Caches目录膨胀到1.2GB被用户投诉“App占用手机存储空间过大”。同时因为缓存文件数量太大30万文件用户手动清理时耗时过长5秒以上体验很差。5.2 清理方案设计我设计的缓存治理方案分三步走第一步控制缓存写入源头。SDWebImage默认磁盘缓存无上限先设置一个合理的上限SDImageCacheConfig *config [SDImageCache sharedImageCache].config; config.maxDiskAge 3 * 24 * 60 * 60; // 3天内有效 config.maxDiskSize 400 * 1024 * 1024; // 缓存上限400MB同时开启了config.shouldRemoveExpiredDataWhenTerminated控制App退出时清理过期数据这个选项在SDWebImage 5.9之后才有老版本可以忽略。第二步建立分级缓存策略。根据图片用途分类核心图片首页Banner、商品图缓存7天无论多大都保留。普通图片信息流缩略图缓存3天超出上限的按时间优先删除。临时图片用户头像、分享图缓存24小时过期立即清理。实现上通过自定义缓存键或URL前缀区分存入不同的缓存子目录。不过SDWebImage的SDImageCache默认使用单个缓存目录要支持分级策略需要创建多个SDImageCache实例每个实例配置不同的缓存路径和参数。这块代码量不小但效果非常明显。第三步优化用户手动清理的耗时。30万文件的遍历删除必然慢优化思路是“优先展示预估缓存大小后台执行拆分清理”。做法如下在设置页第一次展示缓存大小时直接展示上次记录的缓存值不够准但UI秒开。后台异步重新计算准确缓存大小计算完成后更新UI。用户点击清理时先展示一个“清理中”的菊花等待清理完成但后台清理是按批次进行的每次删1000个文件防止长时间卡死主线程。按这个方案落地后效果数据为缓存膨胀速度降低了70%从每3个月1.2GB降到400MB以下手动清理耗时从5秒降到2秒以内用户投诉基本消失。5.3 缓存状态的日常监控最后补充一个长期价值很高的小实践把缓存状态作为App健康指标监控起来。我在工具类里增加了一个上报逻辑每次App冷启动时异步统计缓存总大小、文件总数、清理次数、释放空间把指标统一上报到后台。这样不用等用户投诉自己就能看到线上App的缓存分布情况。如果某个版本上线后缓存中位数明显上涨就能第一时间定位到是新增了什么图片加载逻辑没有设置缓存策略。这个监控逻辑的成本极低每次启动统计一次但收益很高——它让缓存变成了可量化的技术指标而不是一个“感觉应该处理一下”的模糊问题。我在实际使用中发现很多缓存问题不是SDWebImage本身的问题而是使用者对缓存策略的认知不够。SDWebImage提供了足够多的控制面maxDiskAge、maxDiskSize、deleteOldFiles、deleteCacheToSize、removeImageForKey关键在于你是否愿意花时间理清自己的业务场景再把这些控制面组合成一套适合自己App的清理策略。最后再分享一个小技巧SDWebImage的缓存清理代码测试不要用模拟器在真机上跑因为模拟器的文件系统策略和真机差异很大尤其是iOS 17的模拟器在Caches目录的读写条款和真机并不完全一致。关于SDWebImage图片加载本地缓存清理这件事核心思路就是这么多如果你在自己的项目里有更极端的缓存场景欢迎留言一起交流。
返回列表