iOS 第三方框架iOS DeveloperiOS UI开发

SDWebImage源码解析(四)——SDWebImage图片下

2017-05-25  本文已影响220人  SHY圆圆圈圈圆圆

第四篇的写在前面

本篇文章为SDWebImage源码阅读解析的最后一篇文章,主要介绍SDWebImage的图片下载功能。主要涉及两个重要的类——SDWebImageDownloaderSDWebImageDownloaderOperation。在第一篇介绍的SDWebImageManager类中持有SDWebImageDownloader属性,通过loadImageWithURL()方法调用SDWebImageDownloader中的downloadImageWithURL()方法对网络图片进行下载。
本模块的设计设计NSURLSession的使用,如果对这个类不熟悉的话,可以参考官方开发者文档URL Session Programming Guide

//使用更新的downloaderOptions开启下载图片任务
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
      //do something...
            }];  
           

SDWebImageDownloader的设计

在SDWebImage中,SDWebImageDownloader被设计为一个单例,与SDImageCacheSDWebImageManagerd类似。

用于管理NSURLRequest对象请求头的封装、缓存、cookie的设置,加载选项的处理等功能。管理Operation之间的依赖关系。SDWebImageDownloaderOperation是一个自定义的并行Operation子类。这个类主要实现了图片下载的具体操作、以及图片下载完成以后的图片解压缩、Operation生命周期管理等。

初始化方法和相关变量

SDWebImageDownloader提供了一个全能初始化方法,在里面对一些属性和变量做了初始化工作:

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
    if ((self = [super init])) {
        _operationClass = [SDWebImageDownloaderOperation class];
        //默认需要对图片进行解压
        _shouldDecompressImages = YES;
        //默认的任务执行方式为FIFO队列
        _executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
        _downloadQueue = [NSOperationQueue new];
        //默认最大并发任务的数目为6个
        _downloadQueue.maxConcurrentOperationCount = 6;
        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
        _URLOperations = [NSMutableDictionary new];
        //设置默认的HTTP请求头
#ifdef SD_WEBP
        _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
        _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
        _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
        //设置默认的请求超时
        _downloadTimeout = 15.0;

        sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;

        /**
         *初始化session,delegateQueue设为nil因此session会创建一个串行任务队列来处理代理方法
        *和请求回调。
        */ 
        self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                     delegate:self
                                                delegateQueue:nil];
    }
    return self;
}

downloadImageWithURL()方法

downloadImageWithURL ()是下载器的核心方法。


/**
 *  通过创建异步下载器实例来根据url下载图片
 *
 *  当图片下载完成后或者有错误产生时将通知代理对象
 *
 *
 * @param url            The URL to the image to download
 * @param options        The options to be used for this download
 * @param progressBlock  当图片在下载时progressBlock会被反复调用以通知下载进度,该block在后台队列执行
 * @param completedBlock 图片下载完成后执行的回调block
 *
 * @return A token (SDWebImageDownloadToken) 返回的token可以被用在 -cancel 方法中取消任务
 */
- (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 *{
        
        __strong __typeof (wself) sself = wself;
        //1. 设置超时
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }
        //2. 关闭NSURLCache,防止重复缓存图片请求
        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        NSURLRequestCachePolicy cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
        //如果options中设置了使用NSURLCache则开启NSURLCache,默认关闭
        if (options & SDWebImageDownloaderUseNSURLCache) {
            if (options & SDWebImageDownloaderIgnoreCachedResponse) {
                cachePolicy = NSURLRequestReturnCacheDataDontLoad;
            } else {
                cachePolicy = NSURLRequestUseProtocolCachePolicy;
            }
        }
        //3. 初始化URLRequest
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
        
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        //4. 添加请求头
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        //5. 初始化operation 对象
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        //6. 指定验证方式
        if (sself.urlCredential) {
            //SSL验证
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            //Basic验证
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        //7. 将当前operation添加到下载队列
        [sself.downloadQueue addOperation:operation];
        
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            //为operation添加依赖关系      
            //模拟栈的数据结构 先进后出
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

downloadImageWithURL方法中,直接返回了一个名为addProgressCallback的方法,并将其中的代码保存到block中传给这个方法。

addProgressCallback方法

addProgressCallback方法主要用于设置一些回调并且保存,并且执行downloadImage中保存的代码将返回的operation添加到数组中保存。block保存的数据结构如下图:

SDURLCallBacks.png
/** 为callbackBlocks添加callbackBlock->callbackBlock中包含:completedBlock,progressBlock
 *
 * @return SDWebImageDownloadToken
 */
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())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.
    //URL用于给 callbakcs 字典 设置键,因此不能为nil
    //如果url == nil 直接执行completedBlock回调
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    // 初始化 token 其实就是一个标记
    __block SDWebImageDownloadToken *token = nil;
    // 在barrierQueue中同步执行
    dispatch_barrier_sync(self.barrierQueue, ^{
        //1. 根据url获取operation
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        if (!operation) {
            //2. operation不存在
            //执行operationCallback回调的代码 初始化SDWebImageDownloaderOperation
            operation = createCallback();
            //3. 保存operation
            self.URLOperations[url] = operation;

            __weak SDWebImageDownloaderOperation *woperation = operation;
            //4. 保存完成的回调代码
           operation.completionBlock = ^{
              //下载完成后在字典中移除operation
              SDWebImageDownloaderOperation *soperation = woperation;
              if (!soperation) return;
              if (self.URLOperations[url] == soperation) {
                  [self.URLOperations removeObjectForKey:url];
              };
            };
        }
        //5. 保存将回调保存到operation中的callbackBlocks数组 注意这是属于operation的对象方法
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        //6. 设置token的属性
        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });

    return token;
}

SDWebImageDownloaderOperation

Downloader部分的第二个类就是SDWebImageDownloaderOperation在上面的部分也已经用到过不少。它是NSOperation的子类,如果对NSOperation不熟悉的话,可以参考这篇文章
内部自定义了以下几个通知:

NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
NSString *const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification";
NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";

NSOperation进行自定义需要对以下几个方法进行重载:

/**
 * 重写NSOperation的start方法 在里面做处理
 */
- (void)start {
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if SD_UIKIT
        //1. 进行后台任务的处理
        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
        //2. 初始化session
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            //如果Downloader没有传入session(self 对 unownedSession弱引用,因为默认该变量为downloader强引用)
            //使用defaultSessionConfiguration初始化session
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            /**
             *  Create the session for this task
             *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
             *  method calls and completion handler calls.
             */
            /**
             * 初始化自己管理的session
             * delegate 设为 self 即需要自动实现代理方法对任务进行管理
             * delegateQueue设为nil, 因此session会创建一个串行的任务队列处理代理方法和回调
             */
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        //使用request初始化dataTask
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    //开始执行网络请求
    [self.dataTask resume];

    if (self.dataTask) {
        //对callbacks中的每个progressBlock进行调用,并传入进度参数
        //#define NSURLResponseUnknownLength ((long long)-1)
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        //主队列通知SDWebImageDownloadStartNotification
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
    } else {
        //连接不成功
        //执行回调输出错误信息
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
    }

#if SD_UIKIT
    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
}
/**
 * 重写NSOperation的cancel方法
 */
- (void)cancel {
    @synchronized (self) {
        [self cancelInternal];
    }
}
/**
* 内部方法cancel
*/
- (void)cancelInternal {
    if (self.isFinished) return;
    [super cancel];

    if (self.dataTask) {
        [self.dataTask cancel];
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });

        // As we cancelled the connection, its callback won't be called and thus won't
        // maintain the isFinished and isExecuting flags.
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }

    [self reset];
}
/**
* 重设operation
*/
- (void)reset {
    dispatch_barrier_async(self.barrierQueue, ^{
        [self.callbackBlocks removeAllObjects];
    });
    self.dataTask = nil;
    self.imageData = nil;
    if (self.ownedSession) {
        [self.ownedSession invalidateAndCancel];
        self.ownedSession = nil;
    }
}

重点看start方法。NSOperation中需要执行的代码需要写在start方法中。

让一个NSOperation操作开始,你可以直接调用-start,或者将它添加到NSOperationQueue中,添加之后,它会在队列排到它以后自动执行。

类的内部定义了两个属性作为任务的标记:

@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;

需要注意到的是,如果我们在声明属性的时候使用了getter =的语义,则需要自己手动写getter,编译器不会帮我们合成。源码中手动声明了getter方法,并实现KVO。


- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

在初始化NSURLSession的时候,SDWebImageDownloaderOperation将自己声明为delegate

self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];

因此,需要实现NSURLSessionDelegate代理方法。具体的实现这里不赘述,主要是针对各种情况进行处理。但是需要注意到的是,在SDWebImageDownloader中同样遵守了NSURLSessionDelegate代理的方法,但是在downloader中只是简单的把operation数组中与task对应的operation取出,然后把对应的参数传入到SDWebImageDownloaderOperation中的对应的方法进行处理。例如:

// 接收到服务器的响应
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
    //将参数传入dataOperation中进行处理
    [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}

/**
* 根据task取出operation
*/
- (SDWebImageDownloaderOperation *)operationWithTask:(NSURLSessionTask *)task {
    SDWebImageDownloaderOperation *returnOperation = nil;
    for (SDWebImageDownloaderOperation *operation in self.downloadQueue.operations) {
        if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
            returnOperation = operation;
            break;
        }
    }
    return returnOperation;
}

结尾

对于SDWebImage的主要功能在这四篇解析文章中大致的分析,除此以外的功能可能还需要对源码进行更深入的阅读分析才能有更深的了解。由于笔者水平有限,未免出现有分析不准确或者有不到位的地方,请见谅。
源码阅读是一个需要耐心的过程,尽管在途中会遇到一些困难,但是只要多查资料多思考,就会有收获。

参考文献:

  1. SDWebImage源码阅读
  2. iOS中使用像素位图(CGImageRef)对图片进行处理
  3. 一张图片引发的深思
  4. SDWebImage源码解读_之SDWebImageDecoder
  5. Difference between [UIImage imageNamed…] and [UIImage imageWithData…]?
  6. How-is-SDWebImage-better-than-X?
  7. Understanding SDWebImage - Decompression
  8. why decode image after [UIImage initwithData:] ?
  9. Image Resizing Techniques
  10. 多线程之NSOperation简介
  11. SDWebImage源码阅读笔记
  12. URL Session Programming Guide
  13. SDWebImage源码解析
  14. Quartz 2D Programming Guide
  15. Avoiding Image Decompression Sickness
  16. NSOperation
上一篇下一篇

猜你喜欢

热点阅读