1.SDWebImage有什么用
SDWebImage为UIImageView、UIImage、UIButton添加webcache分类
异步下载图片
自动的异步缓存,包括内存缓存和磁盘缓存
在后台解压图片
保证相同的url不会被重复访问多次,保证错误的url不会被反复请求
保证不会阻塞主线程
用block和arc实现
2.框架结构
管理类:SDWebImageManager
处理缓存类:SDImageCache(基于NSCache类,线程安全的)
处理图片下载:SDWebImageDownloader->SDWebImageDownloaderOperation(真正处理下载操作的类)
3.关于内存不足时,清除缓存
当内存不足,发生内存警告到时候需要清除缓存。不同的警告有不同的清除缓存的方式。
当监听到UiApplicationDidReceiveMemoryWarningNotification的时候,也就是系统级的内存警告,会调用clearMemory方法。
//清除内存缓存
- (void)clearMemory {
//把所有的内存缓存都删除
[self.memCache removeAllObjects];
}
当监听到UIApplicationWillTerminateNotification警告的时候,程序即将终止的时候(按home键),调用cleanDisk方法。
当监听到UIApplicationDidEnterBackgroundNotification警告的时候,调用backgroundCleanDisk方法。
cleanDisk清除过期缓存,清除了过期缓存之后计算当前缓存,和设置的最大缓存数做比较,如果当前缓存大于最大缓存,要继续清除,清除的时候,按照文件创建的时间顺序,从最旧的开始清除。最大缓存数量:maxCacheSize,缓存图像总大小,以字节为单位,默认数值为0,表示不作限制。
过期时间:7天maxCacheAge
clearDisk直接把缓存全部删除,然后重新创建一个文件夹。
清空过期的磁盘缓存:
//清除过期的磁盘缓存- (void)cleanDisk { [self cleanDiskWithCompletionBlock:nil];}
删除过期磁盘缓存的具体步骤:
1.计算过期日期,比这个日期还早的文件就是过期文件
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
2.遍历缓存路径中的所有文件,删除早于过期日期的所有文件,并保存文件属性来计算缓存占用空间大小。
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { [urlsToDelete addObject:fileURL]; continue; }
⚠️:[modificationDate laterDate:expirationDate]返回的是modificationDate和:expirationDate中更晚的一个日期,如果返回expirationDate就代表这个文件最早的修改时间比过期日期还早,这个时候就要删除它。
3.// 删除过期的文件 for (NSURL *fileURL in urlsToDelete) { [_fileManager removeItemAtURL:fileURL error:nil]; }
4.计算磁盘缓存,如果剩余磁盘缓存超过最大限额继续删除,从最旧的文件开始删除。
⚠️:面试的时候被问到了一个问题,如何去判断一个图片有没有超过最大缓存时间呢?
图片的修改时间就是图片的一个属性,用这个时间和最大缓存时间去比较,如果比最大缓存时间还早就说明是过期图片。
FOUNDATION_EXPORT NSString * const NSURLContentModificationDateKey NS_AVAILABLE(10_6, 4_0);
4.下载图片核心代码
给UIImageView设置图片需要用到UIImageView+WebCache这个类提供的接口,这个类提供了多个加载图片的借口,但是核心都是调用一个方法。
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {}
这个方法的实现流程:
1. 取消当前图像下载
[self sd_cancelCurrentImageLoad];
下载图片之前先取消下载这个UIImageView之前要下载的图片。因为已经要下载新的一张图片了,原来要下载什么已经不重要了。这个方法的本质是把operationDictionary字典中对应的操作移除了。这个operationDictionary字典里存储了所有的下载操作。
⚠️:这个方法可以用来解决一个问题,因为UITableView里的cell是重用的,一个cell上的imageView开启了图片下载的方法,这个时候这个cell被重用了,新的cell上的imageView又开启了一个下载方法,两个下载操作回调给同一个imageView,就会造成数据的错乱。所以,在开始下载之前要把之前的下载操作取消。
2.设置占位图片placeholder
//判断,如果传入的下载策略不是延迟显示占位图片,那么在主线程中设置占位图片
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
// 设置占位图像
self.image = placeholder;
});
}
3.判断url是否为空,如果为空则生成一个错误信息,把错误信息回传
4.如果url不为空,创建一个新的下载操作
id operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {}
创建下载操作时,会调用SDWebImageManager中的方法
- (id)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {};
这个方法的逻辑:
4.1.检查图片的URL,先判断URL的数据类型是否正确,如果不正确需要赋值为nil,防止因参数类型错误导致程序崩溃。
if (![url isKindOfClass:NSURL.class]) { url = nil; }
4.2.检查URL是否在URL黑名单里,黑名单里存储曾经下载失败的URL。这也就避免了请求失败的URL不会被多次请求。
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}failedURLs是一个NSMutableSet,用来存放请求失败的URL。
4.3.如果URL不正确,或者URL存放在URL黑名单里但是没有选择请求失败重新下载的策略,就直接返回。回调completedBlock块,把错误信息返回。
4.4.URL正确,添加当前任务到正在下载的任务数组中。
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
4.5.根据URL生成一个key,用来对应图片的缓存。
NSString *key = [self cacheKeyForURL:url];
4.6.根据key值检查图片缓存是否存在,在SDWebImageManager里调用
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {}方法。
相当于调用SDImageCache里的
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {}这个方法。
先检查缓存对应的key是否为空,如果为空直接返回doneBlock。
先检查图片在内存缓存中是否存在,
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;}
如果图片存在,就直接把图片返回,并且把图片的缓存方式(内存缓存)返回。检查的时候其实就是用kvc在memCache这个NSCache里检查有没有对应的value。稍后我们具体看一下NSCache是怎么用的。
如果内存缓存不存在,就去检查磁盘缓存。
开启子线程,检查图片是否在磁盘缓存里。
UIImage *diskImage = [self diskImageForKey:key];
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; 如果data为空,就代表缓存没有命中。
根据key在默认路径和自定义路径下面找:
NSString *defaultPath = [self defaultCachePathForKey:key];
NSArray *customPaths = [self.customPaths copy];
NSString *filePath = [self cachePathForKey:key inPath:path];//这个path是存在customPath这个数组里的,用户自定义的路径。
如果磁盘缓存存在,计算图片的大小,然后保存到内存缓存里。
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost]; }
然后把图片和图片的缓存方式(磁盘缓存)返回,在主线程返回。
如果内存缓存和磁盘缓存都没有命中,就开始下载图片。
4.7.下载图片调用SDWebImageDownloader的方法。设置一个下载超时时间,默认是15秒。真正的下载操作在SDWebImageDownloaderOperation里。
创建一个SDWebImageDownloaderOperation对象,当这个SDWebImageDownloaderOperation对象被添加到队列downloadQueue的时候,就会自动调用start方法。在start方法中,创建NSURLConnection请求。
//创建NSURLConnection对象,并设置代理(没有马上发送请求)
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
//获得当前线程 self.thread = [NSThread currentThread];
发出开始下载图片的请求,然后开启runloop直到请求结束。
[self.connection start]; //发送网络请求
//开启Runloop CFRunLoopRun();
下载图片的过程中通过NSURLConnection的代理方法接受数据,第一个方法 - connection:didReceiveResponse: 被调用后,接着会多次调用 - connection:didReceiveData: 方法来更新进度、拼接图片数据,当图片数据全部下载完成时,- connectionDidFinishLoading: 方法就会被调用。
⚠️:下面具体分析一下这三个代理方法的实现:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
1.首先要判断请求回传的状态码,如果状态码是请求成功,就继续执行,如果是304或者是其他请求失败状态码就取消请求操作。
2.//初始化可变的Data用来接收图片数据 self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
3. //得到请求的响应头信息 self.response = response;
4.//注册通知中心,在主线程中发送通知SDWebImageDownloadReceiveResponseNotification【接收到服务器的响应】 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self]; });}
接受到服务器返回的数据以后调用该方法,并且调用多次。
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
//不断拼接接收到的图片数据(二进制数据)
[self.imageData appendData:data];
处理图片下载的进度
}
图片下载完成以后,调用该方法:
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
@synchronized(self) {
//关停当前的runloop
CFRunLoopStop(CFRunLoopGetCurrent());
//把线程和连接对象清空
self.thread = nil;
self.connection = nil;
//在主线程中发出通知:
//SDWebImageDownloadStopNotification 任务停止
//SDWebImageDownloadFinishNotification 任务完成
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
⚠️:如果请求过程中发生了错误,就只发送请求停止的通知,而不要发送请求结束的通知。
});
把得到的图片的二进制数据转换成图片
UIImage *image = [UIImage sd_imageWithData:self.imageData];
根据url获得缓存key,对图片进行缩放和解压
if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:image]; }
}
}
4.8.下载完成后回到SDWebImageManager,根据得到的图片进行缓存处理(SDImageCache)。
4.9.把operation返回,给控件设置图片。
5.将创建好的操作添加到操作字典里 ,实现多张图片同时下载
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
5.如何给图片命名
将图片的url进行md5加密,得到一个字符串就是图片的名称。
6.如何判断图片类型
只判断图片的第一个字节,因为不同类型的十六进制数据的第一个字节是不一样的,相同类型图片的第一个字节是一样的。
7.⚠️
使用SDWebImageManager来下载图片,自动就会进行图片的内存缓存和磁盘缓存。
单独使用SDWebImageDownloader下载图片,不会进行缓存。
单独使用SDImageCache异步缓存图片,缓存图片时可以选择缓存策略,是同时进行内存缓存和磁盘缓存,还是单独选择一种缓存方式。
读取缓存的时候,根据图片url生成的唯一的key,就可以在缓存里读取到对应的图片。
8.图片解码
使用SDWebImageDecoder进行图片解码
首先我们要知道为什么要对图片进行解码,从网络上下载下来的数据被缓存在磁盘里是png或者jpeg的格式,这种格式的图片是不能直接显示在imageView上面的,需要进行解压,将图片解压成位图,这个过程是非常消耗CPU的。
⚠️:两种加载图片的方式
[UIImageView setImage:XXX];
UIImageView的setImage的时候,这时候内存才会增加.并且这时候你将imageView移除,内存也不会有减少。
[UIImage ImageNamed: @"xxx.jpg"]
不适合加载大的不常用的图片.因为它会默认在程序里保存这张图片数据(不会随ImageView的移除而移除).只有经常使用图片适合这种方式加载.
因为解码的过程是在调用setImage方法的时候才进行的,这个过程默认是在主线程执行的,为了防止主线程的卡顿,应该将其优化到在子线程执行。
在子线程执行图片的解压过程,然后对解压后的图片进行缓存,在主线程显示图片的时候直接就可以用了。
9.URL如何管理?
就是我们请求加载的图片地址,是以什么方式保存管理的呢?答案是Runtime中的objc_setAssociatedObject及objc_getAssociatedObject方法,在运行时动态的将url值绑定到具体的对象(例如ImageView)中,以imageURLKey全局变量作为绑定值的key。
10.SDWebImage如何做到相同的url不去请求多次?
我们可以认为url和图片是一一对应的,也就是说我们请求了这个url以后对应的图片就被保存到了缓存里,这个时候也就不需要重新发送url去请求了。而我们为了根据url找到缓存里的图片,图片的名称就是对url进行md5加密以后的字符串。
⚠️:还有一个延伸的问题,就是如何保证在同一时间请求相同的url,只请求一次。因为同一时间发请求,那么请求到的结果还没有缓存起来,就没办法根据缓存避免重复的请求。
解决方案:读SDWebImage库系列(1)-如何保证同一时间请求相同URL时,只进行一次网络请求
有一个字典self.URLCallbacks,这个字典保存着url为key,self.URLCallbacks中的value是一个数组callbacksForURL,这是一个以字典为元素的数组,保存一些不同类型的回调如completeblock,progress block等,我们根据url找到callbacksForURL数组,就能找到个钟类型的回调,可以知道这个url请求的执行状态。
11.如果url相同但是url里图片的内容改变了怎么办?
为什么会发生这种情况呢,就是我们默认的url不改变的话那么图片也不变,我们用这个url去请求的时候系统认为他已经在缓存里了,就不会重新去下载,这个时候要怎么解决呢?
将加载图片的策略设置成SDWebImageRefreshCached。
NSURL *imgURL = [NSURL URLWithString:@"http://handy-img-storage.b0.upaiyun.com/3.jpg"];
[[self imageView] sd_setImageWithURL: imgURL placeholderImage:nil options:SDWebImageRefreshCached];
然后修改SDWebImageDownloader的headersFilter,让开发者对所有的图片请求设置一些额外的header。
给请求添加一个If-Modified-Since属性。
思路就是:
与服务器返回的Last-Modified相对应的request header里可以加一个名为If-Modified-Since的key,value即是服务器回传的服务端图片最后被修改的时间,第一次图片请求时If-Modified-Since的值为空,第二次及以后的客户端请求会把服务器回传的Last-Modified值作为If-Modified-Since的值传给服务器,这样服务器每次接收到图片请求时就将If-Modified-Since与Last-Modified进行比较。如果不同就代表图片被更新了,返回200,如果返回304就代表这两个值相同。