iOS SDWebImage源码研究(三)

在前面的SDWebImage源码解析(一)和SDWebImage源码解析(二)中,解析了开源异步图片下载库SDWebImage的缓存部分。接下来本篇文章将对SDWebImage的下载器部分进行解析。

SDWebImage的异步下载器SDWebImageDownload利用它的单例对象sharedDownloader,可以很好的对图片的下载过程进行配置。sharedDownloader可以配置的部分:

  1. 下载选项
  2. HTTP头部
  3. 压缩、下载顺序、最大并发数、下载超时等

下载选项

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0,
    SDWebImageDownloaderProgressiveDownload = 1 << 1,

    /** * 默认,request不使用NSURLCache。如果设置该选项,则以默认的缓存策略来使用NSURLCache */
    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    /** * 如果从NSURLCache缓存中读取图片,则使用nil传递给imageData参数来调用完成block */

    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    /** * 在iOS 4+系统中,允许程序进入后台继续下载图片。该操作通过向系统申请额外的时间来完成后台下载。如果后台任务超时,则此操作会被取消。 */

    SDWebImageDownloaderContinueInBackground = 1 << 4,

    /** * 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie */
    SDWebImageDownloaderHandleCookies = 1 << 5,

    /** * 设置允许非信任的SSL证书。主要用于测试目的 */
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    /** * 将图像下载放到高优先级队列中 */
    SDWebImageDownloaderHighPriority = 1 << 7,
};

可以看出,这些选项主要涉及到下载的优先级、缓存、后台任务执行、cookie处理以及认证几个方面。

HTTP头部

@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders;

#ifdef SD_WEBP
        _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
        _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif

- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field {
    if (value) {
        self.HTTPHeaders[field] = value;
    }
    else {
        [self.HTTPHeaders removeObjectForKey:field];
    }
}

- (NSString *)valueForHTTPHeaderField:(NSString *)field {
    return self.HTTPHeaders[field];
}

通过设置request.allHTTPHeaderFields来指定HTTP头部,HTTP头部中包含可接受图片类型信息,使用者也可以自己添加或者删除HTTP头部信息。

线程安全

使用自定义的并行调度队列barrierQueue处理所有下载操作的网络响应序列化任务。

 
  
// 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;
为了保证线程安全,所有增改回调集合URLCallbacks的操作使用dispatch_barrier_sync放入队列barrierQueue中,而查询URLCallbakcs的操作只需使用dispatch_sync放入队列barrierQueue中。
//查询URLCallbacks
dispatch_sync(sself.barrierQueue, ^{
     callbacksForURL = [sself.URLCallbacks[url] copy];
    });

//增改URLCallbacks
dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        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形式来呈现的,如下所示:

//下载进度
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
//下载完成
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
//headers过滤
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);

图片下载的这些回调信息存储在SDWebImageDownloader类的URLCallbacks属性中,该属性是一个字典,key是图片的URL地址,value则是一个数组,包含每个图片的多组回调信息。

下载器

整个下载器对于下载请求的管理都是放在downloadImageWithURL:options:progress:completed:方法里面来处理的。

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
    // block中要修改的变量需要__block修饰
    __block SDWebImageDownloaderOperation *operation;
    // 防止retain cycle
    __weak __typeof(self)wself = self;

    // 调用另一方法将,并在创建回调的block中创建新的操作,配置之后将其放入downloadQueue操作队列中。
    [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
        NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // 创建请求对象,并根据options参数设置其属性
        // 为了避免潜在的重复缓存(NSURLCache+SDImageCache),如果没有明确告知,则禁用图片请求的缓存操作。
        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;
        }
        // 创建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];
                                                             });
                                                             for (NSDictionary *callbacks in callbacksForURL) {
                                                                 dispatch_async(dispatch_get_main_queue(), ^{
                                                                     SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                                                     if (callback) callback(receivedSize, expectedSize);
                                                                 });
                                                             }
                                                         }
                                                         // 完成回调调用
                                                        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];
                                                                if (finished) {
                                                                    [sself.URLCallbacks removeObjectForKey:url];
                                                                }
                                                            });
                                                            for (NSDictionary *callbacks in callbacksForURL) {
                                                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                                if (callback) callback(image, data, error, finished);
                                                            }
                                                        }
                                                        // 取消操作将该URL对应的回调信息从URLCallbacks中删除 
                                                        cancelled:^{
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            dispatch_barrier_async(sself.barrierQueue, ^{
                                                                [sself.URLCallbacks removeObjectForKey:url];
                                                            });
                                                        }];
        operation.shouldDecompressImages = wself.shouldDecompressImages;

        // operation的认证配置
        if (wself.username && wself.password) {
            operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
        }
        // operation的优先级配置
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        // 将操作加入到操作队列downloadQueue中
        [wself.downloadQueue addOperation:operation];
        // 如果是LIFO顺序,则将新的操作作为原队列中的最后一个操作的依赖,然后将新操作设置为最后一个操作。
        if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            [wself.lastAddedOperation addDependency:operation];
            wself.lastAddedOperation = operation;
        }
    }];

    return operation;
}
上面的方法调用了addProgressCallback:andCompletedBlock:forURL:createCallback:方法来将请求的信息存入下载器

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // URL作为调用字典URLCallbacks的键值,不能为空。如若为空的话,会立即调用完成block,没有图像或者数据。
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
    // 以dispatch_barrier_synv操作为保证同一时间只有一个线程能对URLCallbacks进行操作。
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // 处理同一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();
        }
    });
}

下载操作

每个图片的下载都是一个Operation操作。我们在上面分析过这个操作的创建及加入操作队列的过程。现在我们来看看单个操作的具体实现。

SDWebImage定义了一个协议,即SDWebImageOperation作为图片下载操作的基础协议。它只声明了一个cancel方法,用于取消操作。协议的具体声明如下:

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

SDWebImage自定义了一个Operation类,即SDWebImageDownloaderOperation,它继承自NSOperation,并采用了SDWebImageOperation协议。除了继承而来的方法,该类只向外暴露了一个方法,即上面所用到的初始化方法initWithRequest:options:pregress:completed:cancelled:。

对于图片的下载,SDWebImageDownloaderOperation完全依赖于URL加载系统中的NSURLConnection类(并未使用iOS7以后的NSURLSession类)。我们先来分析一下SDWebImageDownloaderOperation类中对于图片实际数据的下载处理,即NSURLConnection各代理方法的实现。

首先,SDWebImageDownloaderOperation在Extention中采用了NSURLConnectionDataDelegate协议,并实现了协议的以下几个方法

connection:didReceiveResponse:
connection:didReceiveData:
connectionDidFinishLoading:
connection:didFailWithError:
connection:willCacheResponse:
connectionShouldUseCredentialStorage:
connection:willSendRequestForAuthenticationChallenge:

这些方法我们就不逐一分析了,就终点分析一下connection:didReceiveResponse:和connection:didReceiveData:两个方法。

connection:didReceiveResponse方法通过判断NSURLResponse的实际类型和状态码,对除304以外400以内的状态码反应。

 
  
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {

    //'304 Not Modified' 另外考虑
    if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) {
        NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
        self.expectedSize = expected;
        if (self.progressBlock) {
            self.progressBlock(0, expected);
        }

        self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
        self.response = response;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
        });
    }
    else {
        NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];

        // 当服务器返回'304 Not Modified'的时候,意味着remote的图像没有改变。
        // 304的情况下我们仅需要取消operation并返回缓存中的图像
        if (code == 304) {
            [self cancelInternal];
        } else {
            [self.connection cancel];
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });

        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
        }
        CFRunLoopStop(CFRunLoopGetCurrent());
        [self done];
    }
}
connection:didReceiveData:方法的主要任务是接受数据。每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef对象以作处理。在首次获取到数据时(width+height==0)会从这些包含图像信息的数据中取出图像的长、宽、方向等信息以备使用。而后在图片下载完成之前,会使用CGImageSourceRef对象创建一个图像对象,经过缩放、解压缩操作后生成一个UIImage对象供完成回调使用。当然,在这个方法中还需要处理的就是进度信息。如果我们有设置进度回调的话,就调用进度回调以处理当前图片的下载进度。
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.imageData appendData:data];

    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
        // 以下的代码来自于 http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
        // 谢谢作者 @Nyx0uf

        // 获得已经下载的总共字节数
        const NSInteger totalSize = self.imageData.length;

        // 更新数据源,我们必须传递所有的数据,而不只是新的字节
        CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
        // 首次获取到数据时,从这些数据中获取图片的长、宽、方向属性值
        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);

                // 当绘制到Core Graphics时,我们会丢失方向信息,这意味着有时候由initWithCGIImage创建的
                        // 图片的方向不对。(不像在connectionDidFinishLoading中用initWithData创建的图片)
                        // 所以在这边我们先保存这个信息并在后面使用
                orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
            }

        }
        // 图片还未下载完成
        if (width + height > 0 && totalSize < self.expectedSize) {
            // 使用现有的数据创建图片对象,如果数据中存有多张图片,则取第一张
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#ifdef TARGET_OS_IPHONE
            // 适用于iOS变形图像的解决方案。
            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
            // 对图片进行缩放、解码操作
            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对象,开启了图片的下载,同时抛出一个下载开始的通知。start方法的具体实现如下:


- (void)start {
    @synchronized (self) {
        //管理下载状态,如果已取消,则重置当前下载并设置完成状态YES
        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)];
        // 如果设置了在后台执行,则进行后台执行
        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);
        }
        // 在主线程抛出下载开始通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
        // 确保我们的后台线程启动runloop,这样就能处理下载的数据。
        // Fail、Finish或者cancel的时候CFRunloopStop(CFRunloopGetCurrent())关闭runloop。如果不写 // CFRunloopRun(),根本不会执行NSURLConnection的代理方法的,因为该线程没开启runloop,马上就完了。 // runloop相当于子线程循环,可以灵活控制子线程的生命周期。
        if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
        }
        else {
            CFRunLoopRun();
        }

        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
}

当然,在下载完成或者下载失败后,需要停止当前线程的runloop,清除连接,并抛出下载停止的通知。如果下载成功,则会处理完整的图片数据,对其进行适当的缩放与解压缩操作,以提供给完成回调使用。具体可参考-connectionDidFinishLoading:与-connection:didFailWithError:的实现。

小结

下载的核心其实就是利用NSURLConnection对象来加载数据。每个图片的下载都由一个Operation操作来完成,并将这些操作放到一个操作队列中。这样可以实现图片的并发下载。






你可能感兴趣的:(iOS SDWebImage源码研究(三))