每日一问25——SDWebImage(下载)

前言

SDWebImage是我们开发iOS APP中最常使用到的一个第三方框架,它提供对图片的异步下载与缓存功能。无疑,通过阅读这类优秀开源框架的源码对我们以后的开发也有很大帮助。这篇文章主要记录一下SDWebImage中下载相关的实现分析。

Downloader

SDWebImage中,实现异步下载图片的文件主要是

  • SDWebImageDownloader
  • SDWebImageDownloaderOperation
    下面我们就分别对这两个类的设计与方法实现进行分析。
SDWebImageDownloader

先从.h文件看起,SDWebImageDownloader提供了下载相关的一些枚举,属性还有回调使用的通知和block。

  • 下载的策略枚举:
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0,
    SDWebImageDownloaderProgressiveDownload = 1 << 1,

    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    
    SDWebImageDownloaderContinueInBackground = 1 << 4,

    SDWebImageDownloaderHandleCookies = 1 << 5,

    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    SDWebImageDownloaderHighPriority = 1 << 7,
 
    SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};
  • 下载队列的执行策略枚举,提供队列和栈两种策略
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    /**
     * 默认,队列方式
     */
    SDWebImageDownloaderFIFOExecutionOrder,

    /**
     * 栈方式
     */
    SDWebImageDownloaderLIFOExecutionOrder
};
  • 回调相关的通知和block
FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStartNotification;
FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStopNotification;

typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL);

typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished);

typedef NSDictionary SDHTTPHeadersDictionary;
typedef NSMutableDictionary SDHTTPHeadersMutableDictionary;

typedef SDHTTPHeadersDictionary * _Nullable (^SDWebImageDownloaderHeadersFilterBlock)(NSURL * _Nullable url, SDHTTPHeadersDictionary * _Nullable headers);

新版的SDWebImage已经更新使用NSURLSession进行请求下载。所以SDWebImageDownloader类提供了session相关的属性和设置。例如下面的最大并发数,当前下载数,超时时间和HTTP头的设置等。

@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
@property (assign, nonatomic) NSTimeInterval downloadTimeout;

- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field;

- (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field;

然后来看看.m,SDWebImageDownloader的内部私有属性包括以下

@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
@property (weak, nonatomic, nullable) NSOperation *lastAddedOperation;
@property (assign, nonatomic, nullable) Class operationClass;
@property (strong, nonatomic, nonnull) NSMutableDictionary *URLOperations;
@property (strong, nonatomic, nullable) SDHTTPHeadersMutableDictionary *HTTPHeaders;
// This queue is used to serialize the handling of the network responses of all the download operation in a single queue
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue;

// The session in which data tasks will run
@property (strong, nonatomic) NSURLSession *session;

比较重要的就是downloadQueue(下载队列),barrierQueue(gcd队列,用户让图片依次下载)

核心下载方法
downloadImageWithURL函数

SDWebImageDownloader中方法,大部分都是设置或读取参数。核心下载方法只有

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

来看看这个方法的具体实现分析:

__weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        /******
        *这个block中只是在构造一个SDWebImageDownloaderOperation的实例,提供给addProgressCallback方法使用
        ******/
                                ————————————————华丽的分割线————————————————
        __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];
        
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }

        [sself.downloadQueue addOperation:operation];
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];

我们可以看到,这个方法其实就是调用另一个addProgressCallback方法,并返回了一个SDWebImageDownloadToken类型的对象。而addProgressCallback方法中可能需要block返回一个SDWebImageDownloaderOperation类型的对象。关于SDWebImageDownloaderOperation类将在下面讲到。这里我们只需要知道这个方法需要构造一个SDWebImageDownloaderOperation类型的对象给内部使用。

addProgressCallback函数
if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }

    __block SDWebImageDownloadToken *token = nil;

    #使用dispatch_barrier_sync,保证同一时间只有一个线程能对 URLCallbacks 进行操作
    dispatch_barrier_sync(self.barrierQueue, ^{

        //使用url保存下载任务,如果这个URL被多次下载,也不会创建额外的任务
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        if (!operation) {

            //若该URL没有被下载,则使用外部创建的operation实例。
            operation = createCallback();
            self.URLOperations[url] = operation;

            //保存这个operation下载任务
            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
                dispatch_barrier_sync(self.barrierQueue, ^{
                    SDWebImageDownloaderOperation *soperation = woperation;
                    if (!soperation) return;
                    if (self.URLOperations[url] == soperation) {
                        [self.URLOperations removeObjectForKey:url];
                    };
                });
            };
        }
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });

    return token;

这个函数中主要目的就是查找或构造一个SDWebImageDownloaderOperation类型的实例,并保存到self.URLOperations中。这里还用到一个addHandlersForProgress函数,将progressBlock和completedBlock传到operation实例中保存起来用户后面的下载回调。

SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    dispatch_barrier_async(self.barrierQueue, ^{
        [self.callbackBlocks addObject:callbacks];
    });
    return callbacks;

返回一个SDCallbacksDictionary类型的callbacks对象,里面存储了这个任务用到的回调block,下图是这个类型的数据结构:

每日一问25——SDWebImage(下载)_第1张图片
03.png

上面内容中我们多次提到了SDWebImageDownloaderOperation类,接下来我们就对每一个下载任务进行分析。

SDWebImageDownloaderOperation类

SDWebImageDownloaderOperation是NSOperation的子类。也就是说它本身就是一个可以执行在子线程的任务。(参考:每日一问13——多线程之NSOperation与NSOperationQueue)

@interface SDWebImageDownloaderOperation : NSOperation 

并且这个类遵循了NSURLSession相关的协议。猜测这个类的实例对象应该是处理用于单张图片的下载任务。果然,我们可以看到里面重写了start方法。撇开后台相关的代码不看,我们可以看到这个任务其实就是使用NSURLSession请求了一个request。

重写Start方法,创建下载任务
//管理下载状态,如果已取消,则重置当前下载并设置完成状态为YES
@synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

//是否有缓存结果
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            // Grab the cached data for later check
            NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request];
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        
//检查外部是否创建了session对象,如果没有则自己重新创建
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
 
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    
    [self.dataTask resume];

//通知外部开始下载该URL的任务
    if (self.dataTask) {
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
        });
    } else {
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
    }

处理返回结果

通过NSURLSeesion的代理获取返回结果。

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler

任务接受到响应时的回调,在这里面可以判断本次请求是否成功,并将预计文件数据大小,下载的URL等信息回调给外部。

//只要返回response的code<400并且不是304那么就认为是成功的
    if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) {

//获取预估大小,回调给进度block
        NSInteger expected = (NSInteger)response.expectedContentLength;
        expected = expected > 0 ? expected : 0;
        self.expectedSize = expected;
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, expected, self.request.URL);
        }
        
//由于图片数据是分批返回的,这里先为imagedata分配一块大小合适的内存空间,并通知外部接受到了成功的返回
        self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
        self.response = response;
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:weakSelf];
        });
    } else {

        NSUInteger code = ((NSHTTPURLResponse *)response).statusCode;

//失败情况则停止本次下载任务,并通知外部该任务已被取消        
        if (code == 304) {
            [self cancelInternal];
        } else {
            [self.dataTask cancel];
        }
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
        });
        
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];

        [self done];
    }

在知道本次请求成功后,就可以开始处理返回的数据了。通过代理方法:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data

这个代理会执行很多次,每一次都会返回一部分数据回来,下面看看这里面具体是怎么处理的:

//将本次返回的数据添加到imageData中
[self.imageData appendData:data];

//判断是否需要进度
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {

//获取当前数据总大小,与预估大小比较,判断是否下载完毕
        // Get the image data
        NSData *imageData = [self.imageData copy];
        // Get the total bytes downloaded
        const NSInteger totalSize = imageData.length;
        // Get the finish status
        BOOL finished = (totalSize >= self.expectedSize);
        
//这里先判断下载图片格式,如果是webP格式的话就不作处理,其他格式图片则新创建一个支持SDWebImageProgressiveCoder协议的对象。
        if (!self.progressiveCoder) {
            // We need to create a new instance for progressive decoding to avoid conflicts
            for (idcoder in [SDWebImageCodersManager sharedInstance].coders) {
                if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
                    [((id)coder) canIncrementallyDecodeFromData:imageData]) {
                    self.progressiveCoder = [[[coder class] alloc] init];
                    break;
                }
            }
        }
        
//对数据进行强制解压,生成位图返回给外部,可以做到部分显示变全
        UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
        if (image) {
            NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
            image = [self scaledImageForKey:key image:image];
            if (self.shouldDecompressImages) {
                image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
            }
            
            [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
        }
    }

//返回当前进度,和预估总进度,外部可以用来生成进度条
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }

当所有数据接收完毕后,会执行完成回调:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error

在这个回调中,主要就是通知外部本次请求结束,然后将结果回调出去

@synchronized(self) {
        self.dataTask = nil;
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
            if (!error) {
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:weakSelf];
            }
        });
    }
    
    if (error) {
        [self callCompletionBlocksWithError:error];
    } else {
        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
            /**
             *  If you specified to use `NSURLCache`, then the response you get here is what you need.
             */
            NSData *imageData = [self.imageData copy];
            if (imageData) {

//检测时候开启缓存策略,并且当前下载的图片和缓存的图片是否一致
                if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {

//是缓存过的图片,则直接回调nil
                    [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
                } else {

//没有缓存过,则对图片进行解压,然后回调给外部
                    UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    image = [self scaledImageForKey:key image:image];
                    
                    BOOL shouldDecode = YES;
                    // Do not force decoding animated GIFs and WebPs
                    if (image.images) {
                        shouldDecode = NO;
                    } else {
#ifdef SD_WEBP
                        SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                        if (imageFormat == SDImageFormatWebP) {
                            shouldDecode = NO;
                        }
#endif
                    }
                    
                    if (shouldDecode) {
                        if (self.shouldDecompressImages) {
                            BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
                            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
                        }
                    }
                    if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                    } else {
                        [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
                    }
                }
            } else {

//回调失败信息
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
            }
        }
    }

//结束本次下载任务
    [self done];

小结:关于SDWebImage下载模块主要需要注意的就是任务的缓存处理,合理的创建下载资源,使用NSOperation创建并发任务,将每一个下载任务分配到operation中。

相关文章

SDWebImage源码阅读笔记
SDWebImage源码阅读
SDWebImage 源码阅读笔记(三)
SDWebImage源码阅读笔记

你可能感兴趣的:(每日一问25——SDWebImage(下载))