概述:
SDWebImage类库提供一个UIImageView类别以支持加载来自网络的远程图片。具有缓存管理、异步下载、图片解码、同一个URL下载次数控制和优化等特征。方便使用,为我们节省了大量开发工作。
核心方法一:
1 - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock { 2 [self sd_cancelCurrentImageLoad]; 3 objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 4 5 if (!(options & SDWebImageDelayPlaceholder)) { 6 self.image = placeholder; 7 } 8 9 if (url) { 10 __weak UIImageView *wself = self; 11 id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL, NSData* data) { 12 if (!wself) return; 13 dispatch_main_sync_safe(^{ 14 if (!wself) return; 15 if (image) { 16 wself.image = image; 17 [wself setNeedsLayout]; 18 } else { 19 if ((options & SDWebImageDelayPlaceholder)) { 20 wself.image = placeholder; 21 [wself setNeedsLayout]; 22 } 23 } 24 if (completedBlock && finished) { 25 completedBlock(image, error, cacheType, url, data); 26 } 27 }); 28 }]; 29 [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; 30 } else { 31 dispatch_main_async_safe(^{ 32 NSError *error = [NSError errorWithDomain:@"SDWebImageErrorDomain" code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}]; 33 if (completedBlock) { 34 completedBlock(nil, error, SDImageCacheTypeNone, url, nil); 35 } 36 }); 37 } 38 }
解析如下:
1. [self sd_cancelCurrentImageLoad]
取消正在下载的队列,防止重复下载
2.objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
相当于给当前对象增加一个imageURLKey属性,并设置值为url。这句话对于功能上来说不是必须的,只是为了方便获取正在下载图片的url这个值。
3.设置图像的占位符,除非选项里标明延迟设置
4.关键部分了,检测url是否正常。如果不正常,很简单,报错。如果正常的话,开始下载,这里为了防止主线程卡顿,采用了队列。
并将队列加入缓存中:[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad”]。这行代码和第一行相对应,可以在需要取消时进行取消。待下载完成后,进行图像设置,并通知设用方下载结果。这里做了一些安全检查,比如检测自身是否为空。
5. dispatch_main_sync_safe是自己定义的一个宏,从字面意思上也可以看出来,为了安全地跳到主线程上操作,宏定义如下:
#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}
很简单,判断当前是否在主线程,如果在的话,直接调用,不在的话,先指到主线程上再调用。
核心代码二:图片下载器
上面的代码,已经基本满足了我们的使用需要,我们可以不用关心内部的实现细节了,但如果想深入理解的话,可以再研究一下这段代码。
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock { // Invoking this method without a completedBlock is pointless NSParameterAssert(completedBlock); // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, XCode won't // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString. if ([url isKindOfClass:NSString.class]) { url = [NSURL URLWithString:(NSString *)url]; } // Prevents app crashing on argument type error like sending NSNull instead of NSURL if (![url isKindOfClass:NSURL.class]) { url = nil; } __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; __weak SDWebImageCombinedOperation *weakOperation = operation; BOOL isFailedUrl = NO; @synchronized (self.failedURLs) { isFailedUrl = [self.failedURLs containsObject:url]; } if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) { dispatch_main_sync_safe(^{ NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]; completedBlock(nil, error, SDImageCacheTypeNone, YES, url, nil); }); return operation; } @synchronized (self.runningOperations) { [self.runningOperations addObject:operation]; } NSString *key = [self cacheKeyForURL:url]; operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType, NSData* data) { if (operation.isCancelled) { @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } return; } 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 bug 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, data); }); } // 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; 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; } id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { if (weakOperation.isCancelled) { // Do nothing if the operation was cancelled // See #699 for more details // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data } else if (error) { dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(nil, error, SDImageCacheTypeNone, finished, url, data); } }); if (error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut) { @synchronized (self.failedURLs) { [self.failedURLs addObject:url]; } } } else { BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly); if (options & SDWebImageRefreshCached && image && !downloadedImage) { // Image refresh hit the NSURLCache cache, do not call the completion block } // NOTE: We don't call transformDownloadedImage delegate method on animated images as most transformation code would mangle it else if (downloadedImage && !downloadedImage.images && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 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:data forKey:key toDisk:cacheOnDisk]; } dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url, data); } }); }); } else { if (downloadedImage && finished) { [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; } dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url, data); } }); } } if (finished) { @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } }]; operation.cancelBlock = ^{ [subOperation cancel]; @synchronized (self.runningOperations) { [self.runningOperations removeObject:weakOperation]; } }; } else if (image) { dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(image, nil, cacheType, YES, url, data); } }); @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } else { // Image not in cache and download disallowed by delegate dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(nil, nil, SDImageCacheTypeNone, YES, url, data); } }); @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } }]; return operation; }
解析如下:
1.首先做了一堆容错处理,作为被广泛使用的地个框架,良好的容错性是必须要考虑的
2.针对曾经下载失败的图片地址做了缓存(请求超时的除外),下次请求时不再真正发送请求,而是直接报错,这也算是一个优化吧。这个仅在内存中缓存,下次启动app后会清空。
3.根据不同的下载选项,做了对应的下载策略
4.在block处做了weak处理
题外话:
从这个框架中,我们可以学到:动态添加属性,二级图片缓存,队列读取或下载等等。平时在一些候选人的简历里,会看到罗列一大堆开源类库,能熟练使用。但这证明不了这个人的能力,恰好说明这些开源类库在设计和实现上都比较不错,有许多值得学习的地方。能够运用是最基本的要求,更多的时候我们还是要多看看里面的代码,了解一下实现原理,这样能够帮我们更好的运用,遇到问题也能够抛根问题找到原因。再深入一些的话,想想从这些源码里有什么收获,是不是可以把里面的一些知识运用到我们自己的开发中。
延伸:
https://github.com/rs/SDWebImage
http://blog.cnbang.net/tech/2578/