学习SDWebImage

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_setAssociatedObjectobjc_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就代表这两个值相同。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容