一、SDWebImage的核心是:SDWebImageManger。SDWebImage的工作就是由它调度SDImageCache(一个处理缓存的类)和一个SDWebImageDownloader(负责下载网络图片)来完成的。
二、SDWebImage提供了如下三个category来进行缓存。
1.UIImageView + WebCache imageView的图片
2.UIButton + WebCache 给按钮设置图片
3.MKAnnotationView + WebCache 地图大头针
1.首先将placeholderImage进行展示,SDWebImageManager根据URL开始处理图片
2.SDImageCache从缓存中查找图片图片,如果有SDImageCacheDelegate回调image:didFindImage:forkey:useInfo:给SDWebImageManager ,SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片
3.缓存中没有,生成NSInvocationOperation添加到队列中开始在硬盘中查找,
根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。
如果找到会将图片添加到内存缓存中(如果空闲缓存不够,会先清理)然后SDImageCacheDelegate回调imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。
4.如果硬盘中没有则共享或生成下载器SDWebImageDownLoader开始下载图片,图片下载由NSURLConnection来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。
connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。
connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
5.图片解码处理在一个NSOperationQueue完成,不会拖慢主线程UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
6.在主线程notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给SDWebImageDownloader。
imageDownloader:didFinishWithImage: 回调给SDWebImageManager告知图片下载完成
7.通知所有的downloadDelegates下载完成,回调给需要的地方展示图片。将图片保存到SDImageCache中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独NSInvocationOperation完成,避免拖慢主线程。
8.SDImageCache在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片,
当应用进入后台时,会涉及到『Long-Running Task』正常程序在进入后台后、虽然可以继续执行任务。但是在时间很短内就会被挂起待机。
Long-Running可以让系统为app再多分配一些时间来处理一些耗时任务。
流程图如下
四、缓存要点
1.SDWebImage 实现了一个叫做 AutoPurgeCache 的类 继承自 NSCache ,相比于普通的 NSCache, 它提供了一个在内存紧张时候释放缓存的能力。
自动删除机制:当系统内存紧张时,NSCache 会自动删除一些缓存对象
线程安全:从不同线程中对同一个 NSCache 对象进行增删改查时,不需要加锁
不同于 NSMutableDictionary、NSCache存储对象时不会对 key 进行 copy 操作
@end
@implementation AutoPurgeCache
- (nonnull instancetype)init {
self = [super init];
if (self) {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
return self;
}
- (void)dealloc {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
@end
2.通过内部的一个枚举可以看出磁盘不是强制写入的
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { /**
* 禁用磁盘缓存
*/ SDWebImageCacheMemoryOnly = 1 << 2,
}
3.缓存的时机有两种情况:一个是在下载完成之后,自动保存,或者开发者通过代理处理完图片并返回后缓存。二是当缓存中没有、但是从硬盘中查询到了图片。
4.磁盘的缓存时长默认是一周,清除时间是当程序退出到后台、或者被杀死的时候,在后台执行耗时任务是要申请时间,不要一进入后台短时间就被挂起
// 默认缓存时长
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
// 清理时间
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
/* 磁盘清理的原则
首先、通过时间进行清理。(最后修改时间>一周)
然后、根据占据内存大小进行清理。(如果占据内存大于上限、则按时间排序、删除到上限的1/2。)
由于在源码中没有找到给maxCacheSize设置最大显示的代码,所以猜测默认没有设置上限
*/
// 清理磁盘的方法
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;
5.如何保证内存和磁盘的读写安全?
a:NScache是线程安全的,在多线程操作中,不需要对Cache加锁。
读取缓存的时候是在主线程进行。所有不需要要担心线程安全
b:磁盘的读取虽然创建了一个NSOperation对象、但据我所见这个对象只是用来标记该操作是否被取消、以及取消之后不再读取磁盘文件的作用。
真正的磁盘缓存是在另一个IO专属线程中的一个串行队列下进行的。
如果你搜索self.ioQueue还能发现、不只是读取磁盘内容。
包括删除、写入等所有磁盘内容都是在这个IO线程进行、以保证线程安全。
但计算大小、获取文件总数等操作。则是在主线程进行。
- (void)checkIfQueueIsIOQueue {
const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue);
if (strcmp(currentQueueLabel, ioQueueLabel) != 0) {
NSLog(@"This method should be called from the ioQueue");
}
}
6.磁盘的路径:默认路径
缓存在磁盘沙盒目录下Library/Caches
二级目录为~/Library/Caches/default/com.hackemist.SDWebImageCache.default
当然也可自定义文件名
- 下载最大并发数、超时时长?
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadTimeout = 15.0;
8、缓存图片命名
//1.写入缓存时、直接用图片url作为key
//写入缓存 NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost]
// 2.写入磁盘用url的MD5编码作为key。可以防止文件名过长,以及避免重名
五、图片格式和解码
1.为什么下载和从磁盘中读取的图片要解码?
一般下载或者从磁盘获取的图片是PNG或者JPG,这是经过编码压缩后的图片数据,不是位图,要把它们渲染到屏幕前就需要进行解码转成位图数据,而这个解码操作比较耗时。
你也可以这么理解,图片在远端存储一定都是编码后存储的,这样体积小,一个图像可以看做是一个图像文件,里面包含了文件头,文件体和文件尾,图像的数据就包含在文件体中,而我们的解码就是运用算法将文件体中的图像数据转化为位图数据,方便渲染和展示。
iOS默认是在主线程解码,所以SDWebImage将这个过程放到子线程了。
同时因为位图体积很大,所以磁盘缓存不会直接缓存位图数据,而是编码压缩后的PNG或JPG数据。
2.怎么判断图片格式?
将数据data转为十六进制数据,取第一个字节数据进行判断。
3.如何播放gif图片
3.1 把用户传入的gif图片转成NSData
3.2 根据该Data创建一个图片数据源(NSData->CFImageSourceRef)
3.3 计算该数据源中一共有多少帧,把每一帧数据取出来放到图片数组中
3.4 根据得到的数组+计算的动画时间
3.5 [UIImage animatedImageWithImages:images duration:duration];
六、SDWebImage在多线程下载图片时防止错乱的策略
由于cell的重用机制,在我们加载出一个cell的时候imageView数据源开启一个下载任务并返回一个image,当cell重用时,其数据源又会开启一个下载任务下载新的image,但关联的对象是同一个imageView,这个时候直接setImage时会发生错乱。
SDWebImage的处理是:
imageView对象会关联一个下载列表(列表是给AnimationImages用的,这个时候会下载多张图片),当tableview滑动,imageView重设数据源(url)时,会cancel掉下载列表中所有的任务,然后开启一个新的下载任务。这样子就保证了只有当前可见的cell对象的imageView对象关联的下载任务能够回调,不会发生image错乱。
同时,SDWebImage管理了一个全局下载队列(在DownloadManager中),并发量设置为6.也就是说如果可见cell的数目是大于6的,就会有部分下载队列处于等待状态。而且,在添加下载任务到全局的下载队列中去的时候,SDWebImage默认是采取LIFO(last in,first out)策略的,具体是在添加下载任务的时候,将上次添加的下载任务添加依赖为新添加的下载任务。
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperationaddDependency:operation];
wself.lastAddedOperation = operation;
}
七、下载图片失败后的处理
使用
- (void)sd_setImageWithURL:(NSURL *)url
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder
这两个方法下载图片,如果下载第一次 失败了,再使用同样的url 调用该方法,也不会进行第二次尝试, 因为 SD会记录 失败的URL ,对它直接进行错误处理,
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
好在 SD 有提供接口 灵活 处理这样的情况,避免这样的情况 是使用
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
SDWebImageRetryFailed = 1 << 0, // 值为2的0次方
SDWebImageLowPriority = 1 << 1, // 值为2的1次方
SDWebImageCacheMemoryOnly = 1 << 2, // 值为2的2次方
SDWebImageProgressiveDownload = 1 << 3, // 值为2的3次方
SDWebImageRefreshCached = 1 << 4, // 值为2的4次方
SDWebImageContinueInBackground = 1 << 5, // 值为2的5次方
SDWebImageHandleCookies = 1 << 6, // 值为2的6次方
SDWebImageAllowInvalidSSLCertificates = 1 << 7, // 值为2的7次方
SDWebImageHighPriority = 1 << 8,
SDWebImageDelayPlaceholder = 1 << 9,
SDWebImageTransformAnimatedImage = 1 << 10,
SDWebImageAvoidAutoSetImage = 1 << 11,
SDWebImageScaleDownLargeImages = 1 << 12
};
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options
// 由于SD提供的这个枚举是位移枚举,可以同时传入多个,比如:
[icommageView sd_setImageWithURL:url placeholderImage:image options:SDWebImageRefreshCached | SDWebImageRetryFailed progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
}];
注:本文是通过个人研读SD源码和参考其他博文总结的SD实现流程和使用过程中有可能遇到的问题,如有大佬发现有误之处,欢迎指正👏