标签:
这段时间研究了一下SDWebImage源码,因个人能力有限,如有不对的地方,请指出或留言,谢谢!
SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能。它具有以下功能:
1.一个异步的图片加载器。
2.一个异步的内存+磁盘图片缓存
3.支持GIF、WebP图片
4.后台图片解压缩处理
5.确保同一个URL的图片不被多次下载
6.确保非法的URL不会被反复加载
7.确保下载及缓存时,主线程不被阻塞。
SDWebImage大致流程如下图所示:
SDWebImage是一个成熟而且比较庞大的框架,但使用非常方便。最常用的是给UIImageView设置图片的接口,SDWebImage有提供多个接口给UIImageview设置图片,
最常用的接口为-(void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder,代码如下所示:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil]; }
但所有的接口,最终都会调用下面代码所示的方法:
- (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); 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; id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { //移除进度条 [wself removeActivityIndicator]; //self是否被释放 if (!wself) return; dispatch_main_sync_safe(^{ if (!wself) return; //不要自动设置图片,调用Block传入UIImage对象 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); } }); }]; //将生成的加载操作赋给UIView的自定义属性,并赋值 [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); } }); } }
1.取消先前的下载任务。开始加载之前图片先取消对应的UIImageView先前的图片下载操作。给UIImageView设置一张新图片时,用户不会关心UIImageView先前加载了哪张图片。
//取消图片加载操作 - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key { // Cancel in progress downloader from queue NSMutableDictionary *operationDictionary = [self operationDictionary]; id operations = [operationDictionary objectForKey:key]; if (operations) { if ([operations isKindOfClass:[NSArray class]]) { for (id <SDWebImageOperation> operation in operations) { if (operation) { [operation cancel]; } } } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){ [(id<SDWebImageOperation>) operations cancel]; } //字典属性移除 [operationDictionary removeObjectForKey:key]; } }
SDWebImageManager
在上面讲到的UIImageView+WebCache等控件的分类都是基于SDWebImageManager对象的。UIImageView+WebCache提供的-(void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder方法中,图片的下载操作最终是调用SDWebImageManager的方法,如下所示:
[SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)</span>该对象将一个下载器和一个图片缓存绑定在一起,并对外提供两个只读属性来获取它们,如下代码所示:
@interface SDWebImageManager : NSObject @property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate; @property (strong, nonatomic, readonly) SDImageCache *imageCache; @property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader; ... @end
在上面的代码中可以看到有一个delegate属性,该代理声明了两个可选实现的方法:
// 控制当图片在缓存中没有找到时,应该下载哪个图片 - (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL; // 允许在图片已经被下载完成且被缓存到磁盘或内存前立即转换 - (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;</span>在两个代理方法会在SDWebImageManager的核心方法-downloadImageWithURL:options:progress:completed:中调用,具体实现如下所示:
//SDWebImageManager核心代码 - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock { // Invoking this method without a completedBlock is pointless NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead"); //1.判断URL的合法性 // 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; } //2.创建SDWebImageCombinedOperation对象 __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; __weak SDWebImageCombinedOperation *weakOperation = operation; //3.查看url是否在以前下载失败过 BOOL isFailedUrl = NO; @synchronized (self.failedURLs) { isFailedUrl = [self.failedURLs containsObject:url]; } //4.如果url为nil,或者在不可重试的情况下是一个下载失败的url,则直接返回操作对象并调用完成回调 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]; operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { 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 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); }); } ... //下载 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) { // 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 } //如果出错,则调用完成回调,并将url放入下载失败url数组中 else if (error) { dispatch_main_sync_safe(^{ if (strongOperation && !strongOperation.isCancelled) { completedBlock(nil, error, SDImageCacheTypeNone, finished, url); } }); ... } else { 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 } 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), ^{ ..... } 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]; } } } }]; ... } 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]; } } else { // Image not in cache and download disallowed by delegate dispatch_main_sync_safe(^{ __strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation && !weakOperation.isCancelled) { completedBlock(nil, nil, SDImageCacheTypeNone, YES, url); } }); @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } }]; return operation; }
SDWebImageDownloader
在SDWebImage中,图片的下载是由SDWebImageDownloader类来完成的。它是一个异步下载器,并对图像加载做了优化处理。
下载选项
在下载过程中,程序会根据设置的不同的下载选项,而执行不同的操作。下载选项由枚举SDWebImageDownloaderOptions定义,具体如下:
<span style="font-size:12px;">typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) { SDWebImageDownloaderLowPriority = 1 <<0, SDWebImageDownloaderProgressiveDownload = 1 << 1, // 默认情况下请求不使用NSURLCache,如果设置该选项,则以默认的缓存策略来使用NSURLCache SDWebImageDownloaderUseNSURLCache = 1 << 2, // 如果从NSURLCache缓存中读取图片,则使用nil作为参数来调用完成block SDWebImageDownloaderIgnoreCachedResponse = 1 << 3, // 在iOS4+系统上,允许程序进入后台后继续下载图片。该操作通过向系统申请额外的时间来完成后台下载。如果后台任务终止,则操作会被取消 SDWebImageDownloaderContinueInBackground = 1 << 4, // 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie SDWebImageDownloaderHandleCookies = 1 << 5, // 允许不受信任的SSL证书。主要用于测试目的。 <pre name="code" class="objc">SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, </span>// 将图片下载放到高优先级队列中 SDWebImageDownloaderHighPriority = 1 << 7,};
下载顺序
SDWebImage的下载操作是按一定顺序来处理的,它定义了两种下载顺序,如下所示:
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) { // 以队列的方式,按照先进先出的顺序下载。这是默认的下载顺序 SDWebImageDownloaderFIFOExecutionOrder, // 以栈的方式,按照后进先出的顺序下载。 SDWebImageDownloaderLIFOExecutionOrder };下载管理器
SDWebImageDownloader下载管理器是一个单例类,它主要负责图片的下载操作的管理。图片的下载是放在一个NSOperationQueue操作队列中来完成的,其声明如下:
@property (strong, nonatomic) NSOperationQueue *downloadQueue;默认情况下,队列最大并发数是6。如果需要的话,我们可以通过SDWebImageDownloader类的maxConcurrentDownloads属性来修改。所有下载操作的网络响应序列化处理是放在一个自定义的并行调度队列中来处理,其声明及定义如下:
// This queue is used to serialize the handling of the network responses of all the download operation in a single queue @property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue; _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);每一个图片的下载都会对应一些回调操作,如下载进度回调,下载完成回调,这些回调操作是以block形式来呈现,如下所示:
//下载进度 typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize); //下载完成 typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
图片下载的这些回调信息存储在SDWebImageDownloader类的URLCallbacks属性中,该属性是一个字典,key是图片的URL地址,value则是一个数组,包含每个图片的多组回调信息。由于允许多个图片同时下载,因此可能会有多个线程同时操作URLCallbacks属性。为了保证URLCallbacks操作(添加、删除)的线程安全,SDWebImageDownloader将这些操作作为一个个任务放到barrierQueue队列中,并通过同步机制设置屏障来确保同一时间只有一个线程操作URLCallbacks属性。
dispatch_barrier_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; //如果finished为YES,则将该url对象的回调信息从URLCallbacks中删除 if (finished) { [sself.URLCallbacks removeObjectForKey:url]; } });整个下载管理器对于下载请求的管理都是放在downloadImageWithURL:options:progress:completed:方法里面来处理的,该方法调用了上面所提到的如下方法:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback { // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data. //URLCallbacks属性,key是图片的URL地址,value是一个数组,包含每个图片的多组回调信息 //url为空的话,immediately call the completed block with no image or data. if (url == nil) { if (completedBlock != nil) { completedBlock(nil, nil, nil, NO); } return; } /** * 保证同一时间只有一个线程对URLCallbacks进行操作 */ dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } // Handle single download of simultaneous download request for the same URL NSMutableArray *callbacksForURL = self.URLCallbacks[url]; NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; [callbacksForURL addObject:callbacks]; self.URLCallbacks[url] = callbacksForURL; if (first) { createCallback(); } }); }来将请求的信息存入管理器中,同时在创建回调的block中创建新的操作,配置之后将其放入downloadQueue操作队列中,最后方法返回新创建的操作。
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { __block SDWebImageDownloaderOperation *operation; __weak __typeof(self)wself = self; [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{ NSTimeInterval timeoutInterval = wself.downloadTimeout; if (timeoutInterval == 0.0) { timeoutInterval = 15.0; } //1.创建请求对象,并根据options参数设置其属性 //为了避免潜在的重复缓存,如果没有明确告知需要缓存,则禁用图片请求的缓存操作 // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); request.HTTPShouldUsePipelining = YES; if (wself.headersFilter) { request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]); } else { request.allHTTPHeaderFields = wself.HTTPHeaders; } //2.创建SDWebImageDownloaderOperation操作对象,并进行配置 //配置信息包括是否需要认证、优先级 operation = [[wself.operationClass alloc] initWithRequest:request options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; }); //3.从管理器的callbacksForURL中找出该URL所有的进度处理回调并调用 for (NSDictionary *callbacks in callbacksForURL) { dispatch_async(dispatch_get_main_queue(), ^{ SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; if (callback) callback(receivedSize, expectedSize); }); } } //4.从管理器的callbacksForURL中找出该URL所有的完成处理回调并调用 completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_barrier_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; //如果finished为YES,则将该url对象的回调信息从URLCallbacks中删除 if (finished) { [sself.URLCallbacks removeObjectForKey:url]; } }); for (NSDictionary *callbacks in callbacksForURL) { SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; if (callback) callback(image, data, error, finished); } } //5.取消操作将该url对应的回调信息从URLCallbacks中删除 cancelled:^{ SDWebImageDownloader *sself = wself; if (!sself) return; dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url]; }); }]; operation.shouldDecompressImages = wself.shouldDecompressImages; if (wself.urlCredential) { operation.credential = wself.urlCredential; } else if (wself.username && wself.password) { operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]; } if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; } //6.将操作加入到操作队列downloadQueue中 //如果是LIFO顺序,则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作 [wself.downloadQueue addOperation:operation]; if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { // Emulate LIFO execution order by systematically adding new operations as last operation's dependency [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } }]; return operation; }下载操作
每个图片的下载都是一个Operation操作。在上面分析过这个操作的创建及加入操作队列的过程。现在来看看单个操作的具体实现,SDWebImage定义了一个协议,即SDWebImageOperation作为图片下载操作的基础协议。它只声明了一个cancel方法,用于取消操作。协议的具体声明如下:
@protocol SDWebImageOperation <NSObject> - (void)cancel; @endSDWebImage自定义了一个Operation类,即SDWebImageDownloaderOperation,它继承自NSOperation,并采用了SDWebImageOperation协议。除了继承而来的方法,该类只向外暴露了一个方法,即上面所用到的初始化方法initWithRequest:options:progress:completed:cancelled。
对于图片的下载,SDWebImageDownloaderOperation完全依赖于URL加载系统中的NSURLConnection类(并未使用7.0以后的NSURLSession类)。我们先来分析一下SDWebImageDownloaderOperation类中对于图片实际数据的下载处理,即NSURLConnection各代理方法的实现。
首先,SDWebimageDownloaderOperation在分类中采用了NSURLConnectionDataDelegate协议,并实现了该协议的以下几个方法:
- connection:didReceiveResponse: - connection:didReceiveData: - connectionDidFinishLoading: - connection:didFailWithError: - connection:willCacheResponse: - connectionShouldUseCredentialStorage: - connection:willSendRequestForAuthenticationChallenge:我们在此不逐一分析每个方法的实现,就重点分析一下-connection:didReceiveData:方法。该方法的主要任务是接收数据。每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef对象来做处理。在首次获取到数据时(width+height==0)会从这些包含图像信息的数据中取出图像的长、宽、方向等信息以备使用。而后在图片下载完成之前,会使用CGImageSourceRef对象创建一个图片对象,经过缩放、解压缩操作后生成一个UIImage对象供完成回调使用。当然,在这个方法中还需要处理的就是进度信息。如果我们有设置进度回调的话,就调用这个进度回调以处理当前图片的下载进度。
//接收数据 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { //1.附加数据 [self.imageData appendData:data]; if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) { // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/ // Thanks to the author @Nyx0uf // Get the total bytes downloaded //获取已下载数据总大小 const NSInteger totalSize = self.imageData.length; // Update the data source, we must pass ALL the data, not just the new bytes //3.更新数据源,我们需要传入所有数据,而不仅仅是新数据 //用CGImageSourceRef对象 CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL); //4.首次获取到数据时,从这些数据中获取图片的长、宽、方向属性值 if (width + height == 0) { CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); if (properties) { //获取图像信息的数据中取出图像的长、宽、方向等信息 NSInteger orientationValue = -1; CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); if (val) CFNumberGetValue(val, kCFNumberLongType, &height); val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); if (val) CFNumberGetValue(val, kCFNumberLongType, &width); val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue); CFRelease(properties); //5.绘制到Core Graphics时,我们会丢弃方向信息 // When we draw to Core Graphics, we lose orientation information, // which means the image below born of initWithCGIImage will be // oriented incorrectly sometimes. (Unlike the image born of initWithData // in connectionDidFinishLoading.) So save it here and pass it on later. //方向不对,在这边我们先保存这个信息并在后面使用 orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; } } //6.图片还未下载完成 if (width + height > 0 && totalSize < self.expectedSize) { // Create the image //7.使用现有的数据创建图片对象,如果数据中存有多张图片,则取第一张 CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); #ifdef TARGET_OS_IPHONE //8.适合于手机iOS变形图像的解决方案。 // Workaround for iOS anamorphic image if (partialImageRef) { const size_t partialHeight = CGImageGetHeight(partialImageRef); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); if (bmContext) { CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef); CGImageRelease(partialImageRef); partialImageRef = CGBitmapContextCreateImage(bmContext); CGContextRelease(bmContext); } else { CGImageRelease(partialImageRef); partialImageRef = nil; } } #endif //9.对图片进行缩放,解码操作 if (partialImageRef) { UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; UIImage *scaledImage = [self scaledImageForKey:key image:image]; if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:scaledImage]; } else { image = scaledImage; } CGImageRelease(partialImageRef); dispatch_main_sync_safe(^{ if (self.completedBlock) { self.completedBlock(image, nil, nil, NO); } }); } } CFRelease(imageSource); } if (self.progressBlock) { self.progressBlock(self.imageData.length, self.expectedSize); } }
前面说过SDWebImageDownloaderOperation类是继承自NSOperation类。它没有简单的实现main方法,而是采用更加灵活的start方法,以便自己管理下载的状态。
在start方法中,创建了我们下载所使用的NSURLConnection对象,开启了图片的下载,同时抛出一个下载开始的通知。当然,如果我们期望下载在后台处理,则只需要配置下载选项,使其包含SDWebImageDownloaderContinueInBackground选项。start方法的具体实现如下:
- (void)start {
//同步锁 //管理下载状态,如果已取消,则重置当前下载并设置完成状态为YES @synchronized (self) { if (self.isCancelled) { self.finished = YES; [self reset]; return; } #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 Class UIApplicationClass = NSClassFromString(@"UIApplication"); BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)]; //1.如果设置了在后台执行,则进行后台执行 if (hasApplication && [self shouldContinueWhenAppEntersBackground]) { __weak __typeof__ (self) wself = self; UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)]; self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{ __strong __typeof (wself) sself = wself; if (sself) { [sself cancel]; [app endBackgroundTask:sself.backgroundTaskId]; sself.backgroundTaskId = UIBackgroundTaskInvalid; } }]; } #endif self.executing = YES; self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; self.thread = [NSThread currentThread]; } [self.connection start]; if (self.connection) { if (self.progressBlock) { self.progressBlock(0, NSURLResponseUnknownLength); } //2.在主线程中抛出下载通知 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self]; }); //3.启动run loop if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) { // Make sure to run the runloop in our background thread so it can process downloaded data // Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5 // not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466) CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false); } else { CFRunLoopRun(); } //4.如果未完成,则取消连接 if (!self.isFinished) { [self.connection cancel]; [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]]; } } else { if (self.completedBlock) { self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES); } } #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 Class UIApplicationClass = NSClassFromString(@"UIApplication"); if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { return; } if (self.backgroundTaskId != UIBackgroundTaskInvalid) { UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)]; [app endBackgroundTask:self.backgroundTaskId]; self.backgroundTaskId = UIBackgroundTaskInvalid; } #endif }
为什么会重写start方法而不重写main方法可能是为了控制operation的生命周期
当然,在下载完成或下载失败后,需要停止当前线程的run loop,清除连接,并抛出下载停止的通知。如果下载成功,则会处理完整的图片数据,对其进行适当的缩放与解压缩操作,以提供给完成回调使用。具体可参考-connectionDidFinishLoading:与-connection:didFailWithError:的实现。
SDImageCache
为了减少网络流量的消耗,我们都希望下载下来的图片缓存到本地,下次再去获取同一张图片时,可以直接从本地获取,而不再从远程服务器获取。这样做的另一个好处是提升了用户体验,用户第二次查看同一幅图片时,能快速从本地获取图片直接呈现给用户。@interface SDImageCache () @property (strong, nonatomic) NSCache *memCache; @property (strong, nonatomic) NSString *diskCachePath; @property (strong, nonatomic) NSMutableArray *customPaths; @property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue; @end - (id)initWithNamespace:(NSString *)ns { if ((self = [super init])) { NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns]; ... _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL); ... // Init the memory cache _memCache = [[NSCache alloc] init]; _memCache.name = fullNamespace; // Init the disk cache NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); _diskCachePath = [paths[0] stringByAppendingPathComponent:fullNamespace]; dispatch_sync(_ioQueue, ^{ _fileManager = [NSFileManager new]; }); ... } return self; }
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk { ... // 1. 内存缓存,将其存入NSCache中,同时传入图片的消耗值 [self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale * image.scale]; if (toDisk) { // 2. 如果确定需要磁盘缓存,则将缓存操作作为一个任务放入ioQueue中 dispatch_async(self.ioQueue, ^{ NSData *data = imageData; if (image && (recalculate || !data)) { #if TARGET_OS_IPHONE // 3. 需要确定图片是PNG还是JPEG。PNG图片容易检测,因为有一个唯一签名。PNG图像的前8个字节总是包含以下值:137 80 78 71 13 10 26 10 // 在imageData为nil的情况下假定图像为PNG。我们将其当作PNG以避免丢失透明度。而当有图片数据时,我们检测其前缀,确定图片的类型 BOOL imageIsPng = YES; if ([imageData length] >= [kPNGSignatureData length]) { imageIsPng = ImageDataHasPNGPreffix(imageData); } if (imageIsPng) { data = UIImagePNGRepresentation(image); } else { data = UIImageJPEGRepresentation(image, (CGFloat)1.0); } #else data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil]; #endif } // 4. 创建缓存文件并存储图片 if (data) { if (![_fileManager fileExistsAtPath:_diskCachePath]) { [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; } [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil]; } }); } }
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock { ... // 1. 首先查看内存缓存,如果查找到,则直接回调doneBlock并返回 UIImage *image = [self imageFromDiskCacheForKey:key]; if (image) { doneBlock(image, SDImageCacheTypeMemory); return nil; } // 2. 如果内存中没有,则在磁盘中查找。如果找到,则将其放到内存缓存,并调用doneBlock回调 NSOperation *operation = [NSOperation new]; dispatch_async(self.ioQueue, ^{ if (operation.isCancelled) { return; } @autoreleasepool { UIImage *diskImage = [self diskImageForKey:key]; if (diskImage) { CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale * diskImage.scale; [self.memCache setObject:diskImage forKey:key cost:cost]; } dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); }); } }); return operation; }
- (void)removeImageForKey:(NSString *)key; - (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion; - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk; - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock { dispatch_async(self.ioQueue, ^{ NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey]; // 1. 该枚举器预先获取缓存文件的有用的属性 NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL includingPropertiesForKeys:resourceKeys options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL]; NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge]; NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary]; NSUInteger currentCacheSize = 0; // 2. 枚举缓存文件夹中所有文件,该迭代有两个目的:移除比过期日期更老的文件;存储文件属性以备后面执行基于缓存大小的清理操作 NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init]; for (NSURL *fileURL in fileEnumerator) { NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL]; // 3. 跳过文件夹 if ([resourceValues[NSURLIsDirectoryKey] boolValue]) { continue; } // 4. 移除早于有效期的老文件 NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { [urlsToDelete addObject:fileURL]; continue; } // 5. 存储文件的引用并计算所有文件的总大小,以备后用 NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize += [totalAllocatedSize unsignedIntegerValue]; [cacheFiles setObject:resourceValues forKey:fileURL]; } for (NSURL *fileURL in urlsToDelete) { [_fileManager removeItemAtURL:fileURL error:nil]; } // 6.如果磁盘缓存的大小大于我们配置的最大大小,则执行基于文件大小的清理,我们首先删除最老的文件 if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) { // 7. 以设置的最大缓存大小的一半作为清理目标 const NSUInteger desiredCacheSize = self.maxCacheSize / 2; // 8. 按照最后修改时间来排序剩下的缓存文件 NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) { return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]]; }]; // 9. 删除文件,直到缓存总大小降到我们期望的大小 for (NSURL *fileURL in sortedFiles) { if ([_fileManager removeItemAtURL:fileURL error:nil]) { NSDictionary *resourceValues = cacheFiles[fileURL]; NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize -= [totalAllocatedSize unsignedIntegerValue]; if (currentCacheSize < desiredCacheSize) { break; } } } } if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(); }); } }); }
参考文献:http://southpeak.github.io/blog/2015/02/07/sourcecode-sdwebimage/
标签:
原文地址:http://blog.csdn.net/jasonjwl/article/details/51376036