前言
在iOS开发中,很多都使用到SDWebImage 库,该库使用了外观模式,提供简洁的API,UIButton
、UIImageView
等类可以直接调用API进行网络图片的下载,接下来,就来分析一下SDWebImage
的整个工作流程。
整体结构
查看SDWebImage
的整个项目结构,除去一些辅助的类,其结构大致如下图所示,UIView+WebCacheOperation
类作为UIView
的类别,提供基本的方法,如设置、取消、移除Operation
操作,该Operation
满足<SDWebImageOperation>
协议,并通过关联引用来进行存储。UIImageView+WebCache
、SDWebImageManager
等类提供对外的接口。SDWebImageManager
类将作为Manager来进行下载和缓存的管理,而真正负责下载的类为SDWebImageDownloader
类,SDWebImageDownloaderOperation
负责封装下载对象;管理缓存的类为SDImageCache
。 好了,大致结构就是这样,接下来将通过一次调用来分析其内部的具体流程。
具体流程
接下来,以设置UIImageView
类的图片为例,我们将使用UIImageView+WebCache
提供的API,其接口方法非常简便,你可以什么都不用管,直接调用- (void)sd_setImageWithURL:(NSURL *)url
方法就能进行图片的赋值,SDWebImageCache
库将自动根据默认配置进行下载和缓存。UIImageView+WebCache
类提供的下载接口如下,每个接口方法只是参宿的个数不一样,不管是使用其中的哪个方法,最终都会等同于调用最后一个方法,其中缺省的参数会使用默认值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 - (void)sd_setImageWithURL:(NSURL *)url; - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options; - (void)sd_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completedBlock; - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock; - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock; //最终都会通过我来调用 - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
接下来,我们就来看一下方法的处理流程,代码如下所示:
代码1:用来取消之前的图片加载。
代码2:当选项不包含SDWebImageDelayPlaceholder
时,设置占位图片。
代码3:调用SDWebImageManager
单例类的方法来进行图片的下载和缓存管理,该方法返回满足<SDWebImageOperation>
协议的对象,在该方法的completed
参数中,我们进行一些基本的操作,如将图片赋给ImageView
等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock { //1 [self sd_cancelCurrentImageLoad]; objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); //2 if (!(options & SDWebImageDelayPlaceholder)) { dispatch_main_async_safe(^{ self.image = placeholder; }); } if (url) { // check if activityView is enabled or not if ([self showActivityIndicatorView]) { [self addActivityIndicator]; } __weak __typeof(self)wself = self; //3 id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { [wself removeActivityIndicator]; if (!wself) return; dispatch_main_sync_safe(^{ if (!wself) return; //不自动进行图片的赋值 if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) { completedBlock(image, error, cacheType, url); return; } else if (image) { wself.image = image; [wself setNeedsLayout]; } else { if ((options & SDWebImageDelayPlaceholder)) { wself.image = placeholder; [wself setNeedsLayout]; } } if (completedBlock && finished) { completedBlock(image, error, cacheType, url); } }); }]; //将operation通过关联引用存储到字典对象中进行管理 [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; } else { dispatch_main_async_safe(^{ [self removeActivityIndicator]; if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}]; completedBlock(nil, error, SDImageCacheTypeNone, url); } }); } }
接下来,我们再接着往下走,分析一下上面代码中调用的- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
方法,代码如下,由于原代码代码量较多,所以裁剪了一些简单的处理代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock { __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; __weak SDWebImageCombinedOperation *weakOperation = operation; BOOL isFailedUrl = NO; @synchronized (self.failedURLs) { isFailedUrl = [self.failedURLs containsObject:url]; } //1 if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) { dispatch_main_sync_safe(^{ NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]; completedBlock(nil, error, SDImageCacheTypeNone, YES, url); }); return operation; } @synchronized (self.runningOperations) { [self.runningOperations addObject:operation]; } NSString *key = [self cacheKeyForURL:url]; //2 operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { if (operation.isCancelled) { @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } return; } //3 if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { if (image && options & SDWebImageRefreshCached) { dispatch_main_sync_safe(^{ // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server. completedBlock(image, nil, cacheType, YES, url); }); } //配置项的赋值 // download if no image or requested to refresh anyway, and download allowed by delegate SDWebImageDownloaderOptions downloaderOptions = 0; if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority; if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload; if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache; if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground; if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies; if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates; if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority; //4 if (image && options & SDWebImageRefreshCached) { // force progressive off if image already cached but forced refreshing downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload; // ignore image read from NSURLCache if image if cached but force refreshing downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse; } //5 id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { __strong __typeof(weakOperation) strongOperation = weakOperation; if (!strongOperation || strongOperation.isCancelled) { } else { //6 if ((options & SDWebImageRetryFailed)) { @synchronized (self.failedURLs) { [self.failedURLs removeObject:url]; } } BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly); if (options & SDWebImageRefreshCached && image && !downloadedImage) { // Image refresh hit the NSURLCache cache, do not call the completion block } //7 else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ //调用委托方法来获得transform后的图片 UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url]; if (transformedImage && finished) { BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage]; //进行图片的缓存 [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk]; } dispatch_main_sync_safe(^{ if (strongOperation && !strongOperation.isCancelled) { completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url); } }); }); } else { if (downloadedImage && finished) { //进行图片缓存 [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; } dispatch_main_sync_safe(^{ if (strongOperation && !strongOperation.isCancelled) { completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url); } }); } } if (finished) { @synchronized (self.runningOperations) { if (strongOperation) { [self.runningOperations removeObject:strongOperation]; } } } }]; //取消的Block,当取消操作执行时调用 operation.cancelBlock = ^{ [subOperation cancel]; @synchronized (self.runningOperations) { __strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation) { [self.runningOperations removeObject:strongOperation]; } } }; } //找到缓存图片时调用 else if (image) { dispatch_main_sync_safe(^{ __strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation && !strongOperation.isCancelled) { completedBlock(image, nil, cacheType, YES, url); } }); //移除操作对象 @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } }]; return operation; }
代码1用来判断请求的URL是否包含在失败的URL列表中,如果包含,且请求没有设置重试选项,则直接返回,不再进行接下来的流程。
接下来,着重看一下代码2部分,该操作是给operation
的cacheOperation
属性赋值,调用SDImageCache
类的- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock
方法,该方法将查询是否在硬盘或者内存中已经存在该图片,查询完之后调用done
完成块,接下来看一下done
完成块中的操作。
代码3的if
判断满足条件为:没有找到缓存的image,或者有需要刷新缓存 的选项且调用的委托方法返回True
。
代码4:当image存在,且选项包含SDWebImageRefreshCached
时,去掉下载选项中的SDWebImageDownloaderProgressiveDownload
选项,既移除progresive
功能,该功能可以在图片下载的过程中渐进式的展示图片;除此之外,再把SDWebImageDownloaderIgnoreCachedResponse
选项加到下载选项中,该选项忽略NSURLCache
的缓存(使用SDWebImageCache
库作请求图片时默认是不使用NSURLCache
缓存的),关于NSURLCache
,可参考NSURLCache
代码5:创建一个下载图片的操作,调用SDWebImageDownloader
类的- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
方法,先来看一下completed
完成块的操作,先假设下载操作完成。
代码6:如果下载选项包括错误重试时,将URL从错误URL列表中移除。
代码7:如果下载图片时有用到transform,将进入该if块,接着会调用委托来让我们进行相应的transform,之后将调用SDImageCache
类来进行图片的缓存。
缓存 接下来,分析一下SDImageCache
类的工作流程,SDImageCache
类负责图片的缓存管理,缓存包括内存、硬盘两种缓存方式:
内存缓存:使用NSCache
类,通过监听内存警告的通知,在得到通知后移除NSCache
类中保存的数据。
硬盘缓存:既将图片作为文件保存到硬盘上,关于图片过期的问题,SDImageCache
类的处理方式是:监听App的状态,当进入后台运行时,将开启一个后台任务,对硬盘上保存的图片进行清理,这个过程包含两个操作(具体代码可参见SDImageCache
类的- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock
方法):
移除过期的文件。
当缓存的文件大小已经操作最大缓存大小时,将移除文件更新时间最老的图片文件,直到小于最大缓存为止。
下载 图片的下载使用的NSOperationQueue
来实现,它的好处就是可以cacel,且可以添加依赖,SDWebImage
除了默认的FIFO
先进先出策略外,还提供LIFO
的策略,既后进先出,先执行后加入的下载操作,这个策略就是通过依赖来完成的。 下载操作被封装在SDWebImageDownloaderOperation
对象中,SDWebImageDownloaderOperation
继承自NSOperation
类。 图片的下载使用的NSURLSession
,通过实现NSURLSession
相关的委托协议,来实现Progress的调用、协议认证等操作。
总结 SDWebImage
库整体结构还是很清晰的。对网络部分比较感兴趣的可以读一下源码。