SDWebImage源码解读之SDWebImageDownloa

作者: 老马的春天 | 来源:发表于2017-01-04 12:34 被阅读985次

    第七篇

    前言

    本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中。下载数据并不难,如何对下载这一系列的任务进行设计,就很难了。接下来我们一步一步的分析作者在开发中的思路和使用到的技术细节。

    NSOperation

    NSOperation想必大家都知道,为了让程序执行的更快,我们用多线程异步的方式解决这个问题,GCDNSOperation都能实现多线程,我们这里只介绍NSOperation。如果大家想了解更多NSOperation的知识,我觉得这篇文章写得挺好:多线程之NSOperation简介

    我们把NSOperation最核心的使用方法总结一下:

    1. NSOperation有两个方法:main()start()。如果想使用同步,那么最简单方法的就是把逻辑写在main()中,使用异步,需要把逻辑写到start()中,然后加入到队列之中。
    2. 大家有没有想过NSOperation什么时候执行呢?按照正常想法,难道要我们自己手动调用main()start()吗?这样肯定也是行的。当手动调用start()或者main()方法的时候,和调用普通的方法没什么区别,当被加入operationQueue中后,情况不同,operationQueue中所有的NSOperation都是异步执行的,也就是说start()会在子线程执行,至于是串行还是并发,都由maxConcurrentOperationCount控制,当maxConcurrentOperationCount == 1时,相当于串行了。另外一种方法就是加入到operationQueue中,operationQueue会尽快执行NSOperation,如果operationQueue是同步的,那么它会等到NSOperation的isFinished等于YES后,在执行下一个任务,如果是异步的,通过设置maxConcurrentOperationCount来控制同事执行的最大操作,某个操作完成后,继续其他的操作。
    3. 并不是调用了canche就一定取消了,如果NSOperation没有执行,那么就会取消,如果执行了,只会将isCancelled设置为YES。所以,在我们的操作中,我们应该在每个操作开始前,或者在每个有意义的实际操作完成后,先检查下这个属性是不是已经设置为YES。如果是YES,则后面操作都可以不用在执行了。

    能够引起思考的地方就是,比如说我有一系列的任务要执行,我有两种选择,一种是通过数组控制数据的取出顺序,另外一种就是使用队列

    通知

    extern NSString * _Nonnull const SDWebImageDownloadStartNotification;
    extern NSString * _Nonnull const SDWebImageDownloadReceiveResponseNotification;
    extern NSString * _Nonnull const SDWebImageDownloadStopNotification;
    extern NSString * _Nonnull const SDWebImageDownloadFinishNotification;
    

    SDWebImageDownloaderOperation有四种情况会发送通知:

    1. 任务开始
    2. 接收到数据
    3. 暂停
    4. 完成

    不知道大家发现没有,在设计一个功能的时候,作者都会用通知的形式暴露出关键的节点。不管使用者需不需要使用这些通知。这是一个很好地方法,可以在自定义控件的时候参考这个设计。

    SDWebImageDownloaderOperationInterface

    /**
     Describes a downloader operation. If one wants to use a custom downloader op, it needs to inherit from `NSOperation` and conform to this protocol
     */
    @protocol SDWebImageDownloaderOperationInterface<NSObject>
    
    - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                                  inSession:(nullable NSURLSession *)session
                                    options:(SDWebImageDownloaderOptions)options;
    
    - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
    
    - (BOOL)shouldDecompressImages;
    - (void)setShouldDecompressImages:(BOOL)value;
    
    - (nullable NSURLCredential *)credential;
    - (void)setCredential:(nullable NSURLCredential *)value;
    
    @end
    

    按照作者的注释,如果我们想要实现一个自定义的下载操作,就必须继承自NSOperation,同时实现SDWebImageDownloaderOperationInterface这个协议,我们不去看其他的代码,只做一个简单的猜测:很可能在别的类中,只使用SDWebImageDownloaderOperationInterfaceNSOperation中的方法和属性。

    • 使用NSURLRequest,NSURLSessionSDWebImageDownloaderOptions初始化
    • 可以为每一个NSOperation自由的添加相应对象
    • 设置是否需要解压图片
    • 设置是否需要设置凭证

    @interface SDWebImageDownloaderOperation : NSOperation

    关于SDWebImageDownloaderOperation.h的设计,有几点值得我们注意,首先它是遵守SDWebImageDownloaderOperationInterface协议的,所以上一节的那些方法,都必须实现,我们在设计这个.h的时候呢,可以把协议中的方法再次写到这个.h中,这样别人在使用的时候,就会更加直观

    /**
     * The credential used for authentication challenges in `-connection:didReceiveAuthenticationChallenge:`.
     *
     * This will be overridden by any shared credentials that exist for the username or password of the request URL, if present.
     */
    @property (nonatomic, strong, nullable) NSURLCredential *credential;
    

    通过声明一个属性,就实现了SDWebImageDownloaderOperationInterface协议中的

    - (nullable NSURLCredential *)credential;
    - (void)setCredential:(nullable NSURLCredential *)value;
    

    一般情况下,我们在主动指明初始化方法的时候,肯定会为初始化方法设定几个参数。那么这些参数就应该以只读的方式暴露给他人。比如:

    初始化方法:

    /**
     *  Initializes a `SDWebImageDownloaderOperation` object
     *
     *  @see SDWebImageDownloaderOperation
     *
     *  @param request        the URL request
     *  @param session        the URL session in which this operation will run
     *  @param options        downloader options
     *
     *  @return the initialized instance
     */
    - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                                  inSession:(nullable NSURLSession *)session
                                    options:(SDWebImageDownloaderOptions)options NS_DESIGNATED_INITIALIZER;
    

    只读的属性:

    /**
    * The request used by the operation's task.
    */
    @property (strong, nonatomic, readonly, nullable) NSURLRequest *request;
    
    /**
    * The operation's task
    */
    @property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask;
    /**
    * The SDWebImageDownloaderOptions for the receiver.
    */
    @property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options;
    

    其他的属性:

    /**
     * The expected size of data.
     */
    @property (assign, nonatomic) NSInteger expectedSize;
    
    /**
     * The response returned by the operation's connection.
     */
    @property (strong, nonatomic, nullable) NSURLResponse *response;
    

    取消方法:

    /**
     *  Cancels a set of callbacks. Once all callbacks are canceled, the operation is cancelled.
     *
     *  @param token the token representing a set of callbacks to cancel
     *
     *  @return YES if the operation was stopped because this was the last token to be canceled. NO otherwise.
     */
    - (BOOL)cancel:(nullable id)token;
    

    这个方法不是取消任务的,而是取消任务中的响应,当时当任务中没有响应者的时候,任务也会被取消。

    SDWebImageDownloaderOperation.m

    我们的目的是下载一张图片,那么我们最核心的逻辑是什么呢?

    1. 初始化一个task
    2. 添加响应者
    3. 开启下载任务
    4. 处理下载过程和结束后的事情

    也就是说.m中所有的代码,都是围绕着上边4点来设计的。 那么我们就详细的对每一步进行分析:

    1.初始化一个task

    - (nonnull instancetype)init {
        return [self initWithRequest:nil inSession:nil options:0];
    }
    
    - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                                  inSession:(nullable NSURLSession *)session
                                    options:(SDWebImageDownloaderOptions)options {
        if ((self = [super init])) {
            _request = [request copy];
            _shouldDecompressImages = YES;
            _options = options;
            _callbackBlocks = [NSMutableArray new];
            _executing = NO;
            _finished = NO;
            _expectedSize = 0;
            _unownedSession = session;
            responseFromCached = YES; // Initially wrong until `- URLSession:dataTask:willCacheResponse:completionHandler: is called or not called
            _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
        }
        return self;
    }
    
    - (void)dealloc {
        SDDispatchQueueRelease(_barrierQueue);
    }
    

    这个初始化方法里边有很多我们在.h没有见过的属性。我们有必要在此做一些解释,在接下来的解读中,就不会再做出解释了。

    • _callbackBlocks @property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks; 是一个数组,数组中存放的是SDCallbacksDictionary类型的数据,那么这个SDCallbacksDictionary其实就是一个字典,typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;,key是一个字符串,这个字符串有两种情况:kProgressCallbackKeykCompletedCallbackKey,也就是说进度和完成的回调都是放到一个数组中的。那么字典的值就是回调的block了。
    • _unownedSession @property (weak, nonatomic, nullable) NSURLSession *unownedSession;,这个属性是我们初始化时候传进来的参数,作者提到。这个参数不一定是可用的。也就是说是不安全的。当出现不可用的情况的时候,就需要使用@property (strong, nonatomic, nullable) NSURLSession *ownedSession;.
    • responseFromCached 用于设置是否需要缓存响应,默认为YES
    • _barrierQueue 队列,这个会在后边的使用中讲解到

    2.添加响应者

    - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
        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;
    }
    

    看这段代码,也很好理解。就是把字典添加到数组中去,但是这里边有一个很重要的知识点:dispatch_barrier_async,我们做一个简单的介绍。

    我们可以创建两种类型的队列,串行和并行,也就是DISPATCH_QUEUE_SERIAL,DISPATCH_QUEUE_CONCURRENT。那么dispatch_barrier_async和dispatch_barrier_sync究竟有什么不同之处呢?

    barrier这个词是栅栏的意思,也就是说是用来做拦截功能的,上边的这另种都能够拦截任务,换句话说,就是只有我的任务完成后,队列后边的任务才能完成。

    不同之处就是,dispatch_barrier_sync控制了任务往队列添加这一过程,只有当我的任务完成之后,才能往队列中添加任务。dispatch_barrier_async不会控制队列添加任务。但是只有当我的任务完成后,队列中后边的任务才会执行。

    那么在这里的任务是往数组中添加数据,对顺序没什么要求,我们采取dispatch_barrier_async就可以了,已经能保证数据添加的安全性了。

    - (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
        __block NSMutableArray<id> *callbacks = nil;
        dispatch_sync(self.barrierQueue, ^{
            // We need to remove [NSNull null] because there might not always be a progress block for each callback
            callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
            [callbacks removeObjectIdenticalTo:[NSNull null]];
        });
        return [callbacks copy];    // strip mutability here
    }
    

    这个方法是根据key取出所有符合key的block,这里采用了同步的方式,相当于加锁。比较有意思的是[self.callbackBlocks valueForKey:key]这段代码,self.callbackBlocks是一个数组,我们假定他的结构是这样的:

    @[@{@"completed" : Block1}, 
    @{@"progress" : Block2}, 
    @{@"completed" : Block3}, 
    @{@"progress" : Block4}, 
    @{@"completed" : Block5}, 
    @{@"progress" : Block6}]
    

    调用[self.callbackBlocks valueForKey:@"progress"]后会得到[Block2, Block4, Block6].
    removeObjectIdenticalTo:这个方法会移除数组中指定相同地址的元素。

    - (BOOL)cancel:(nullable id)token {
        __block BOOL shouldCancel = NO;
        dispatch_barrier_sync(self.barrierQueue, ^{
            [self.callbackBlocks removeObjectIdenticalTo:token];
            if (self.callbackBlocks.count == 0) {
                shouldCancel = YES;
            }
        });
        if (shouldCancel) {
            [self cancel];
        }
        return shouldCancel;
    }
    

    这个函数,就是取消某一回调。使用了dispatch_barrier_sync,保证,必须该队列之前的任务都完成,且该取消任务结束后,在将其他的任务加入队列。

    3.开启下载任务

    - (void)start {
        @synchronized (self) {
            if (self.isCancelled) {
                self.finished = YES;
                [self reset];
                return;
            }
    
    #if SD_UIKIT
            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
            NSURLSession *session = self.unownedSession;
            if (!self.unownedSession) {
                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.
                 */
                self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                                  delegate:self
                                                             delegateQueue:nil];
                session = self.ownedSession;
            }
            
            self.dataTask = [session dataTaskWithRequest:self.request];
            self.executing = YES;
        }
        
        [self.dataTask resume];
    
        if (self.dataTask) {
            for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
                progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
            }
            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
    }
    

    这一块也分为几个子任务

    • 如果该任务已经被设置为取消了,那么就无需开启下载任务了。并重置。别忘了设置finished为YES

        - (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方法的目的只是开启下载任务,它所要保证的就是调用start时,任务能够开启,至于是否下载成功,那不是start 应该关心的事情。

        #if SD_UIKIT
                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
        
        开启后,确保关闭后台任务
        
        #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
      
    • task开启前的准备工作

         NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            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.
             */
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
      
    • 开启task 并处理回调

       [self.dataTask resume];
      
        if (self.dataTask) {
            for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
                progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
            }
            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"}]];
        }
      

    4.处理下载过程和结束后的事情

    - (void)URLSession:(NSURLSession *)session
              dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveResponse:(NSURLResponse *)response
     completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
        
        //'304 Not Modified' is an exceptional one
        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;
            for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
                progressBlock(0, expected, self.request.URL);
            }
            
            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;
            
            //This is the case when server returns '304 Not Modified'. It means that remote image is not changed.
            //In case of 304 we need just cancel the operation and return cached image from the cache.
            if (code == 304) {
                [self cancelInternal];
            } else {
                [self.dataTask cancel];
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
            });
            
            [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];
    
            [self done];
        }
        
        if (completionHandler) {
            completionHandler(NSURLSessionResponseAllow);
        }
    }
    

    上边的代码,处理了当收到响应后要做的事情。我们规定,把没有收到响应码或者响应码小于400认定为正常的情况,其中304比较特殊,因为当stateCode为304的时候,便是这个响应没有变化,可以再缓存中读取。那么其他的情况,就可以认定为错误的请求。

    当一切顺利的时候,基本上就是给早已定义的属性赋值,上边的代码逻辑比较简单,在这里就不做介绍了。

    - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
        [self.imageData appendData:data];
    
        if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
            // 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
            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);
    
                    // 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 didCompleteWithError.) So save it here and pass it on later.
    #if SD_UIKIT || SD_WATCH
                    orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
    #endif
                }
            }
    
            if (width + height > 0 && totalSize < self.expectedSize) {
                // Create the image
                CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
    
    #if SD_UIKIT || SD_WATCH
                // 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
    
                if (partialImageRef) {
    #if SD_UIKIT || SD_WATCH
                    UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
    #elif SD_MAC
                    UIImage *image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
    #endif
                    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);
                    
                    [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
                }
            }
    
            CFRelease(imageSource);
        }
    
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
        }
    }
    

    上边的方法处理了接收到数据的逻辑。按照正常想法,当我们接受到数据的时候,只要把收据拼接起来,根据设置选项,调用process回调就行了。那么为什么这个方法中用了如此大的篇幅来处理图片数据呢?

    答案就是,即使图片没有下载完,我们也能根据已经获取的图片数据,来显示一张数据不完整的图片。 通过这样一个细节,我想到了很多应用场景,比如说,之前看到过一个场景,通过滑动slider 自上而下的显示一张图片的部分内容,我们完全可以通过上边的代码来实现。根据slider的value来控制整个NSData的大小,来合成图片。当然这也跟图片的组成有关。

    - (void)URLSession:(NSURLSession *)session
              dataTask:(NSURLSessionDataTask *)dataTask
     willCacheResponse:(NSCachedURLResponse *)proposedResponse
     completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
    
        responseFromCached = NO; // If this method is called, it means the response wasn't read from cache
        NSCachedURLResponse *cachedResponse = proposedResponse;
    
        if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
            // Prevents caching of responses
            cachedResponse = nil;
        }
        if (completionHandler) {
            completionHandler(cachedResponse);
        }
    }
    

    该方法用于响应的缓存设置,如果把回调的参数设置为nil,那么就不会缓存响应,总之,真正缓存的数据就是回调中的参数。

    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
        @synchronized(self) {
            self.dataTask = nil;
            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
                if (!error) {
                    [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
                }
            });
        }
        
        if (error) {
            [self callCompletionBlocksWithError:error];
        } else {
            if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
                /**
                 *  See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
                 *  Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
                 *    and images for which responseFromCached is YES (only the ones that cannot be cached).
                 *  Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
                 */
                if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
                    // hack
                    [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
                } else if (self.imageData) {
                    UIImage *image = [UIImage sd_imageWithData:self.imageData];
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    image = [self scaledImageForKey:key image:image];
                    
                    // Do not force decoding animated GIFs
                    if (!image.images) {
                        if (self.shouldDecompressImages) {
                            if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
    #if SD_UIKIT || SD_WATCH
                                image = [UIImage decodedAndScaledDownImageWithImage:image];
                                [self.imageData setData:UIImagePNGRepresentation(image)];
    #endif
                            } else {
                                image = [UIImage decodedImageWithImage:image];
                            }
                        }
                    }
                    if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                    } else {
                        [self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
                    }
                } else {
                    [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
                }
            }
        }
        [self done];
    }
    

    该方法是处理了图片下载完成之后的逻辑,也没有很特别的东西,比较复杂的是对完成后的数据的处理更加完善。要做到这一点,确实需要N多知识的积累啊。

    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
        
        NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        __block NSURLCredential *credential = nil;
        
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
                disposition = NSURLSessionAuthChallengePerformDefaultHandling;
            } else {
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                disposition = NSURLSessionAuthChallengeUseCredential;
            }
        } else {
            if (challenge.previousFailureCount == 0) {
                if (self.credential) {
                    credential = self.credential;
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
                }
            } else {
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        }
        
        if (completionHandler) {
            completionHandler(disposition, credential);
        }
    }
    

    这个方法跟HTTPS有点关系,要想说明白这个方法究竟干了什么事? 需要对验证有点了解才行。

    当我们发出了一个请求,这个请求到达服务器后,假定服务器设置了需要验证。那么这个方法就会被调用。服务器会返回去一个NSURLAuthenticationChallenge。通过NSURLAuthenticationChallengeprotectionSpace,获取授权method。如果这个metho是服务器信任的, 那么我们就可以直接使用服务器返回的证书,当然,我们也可以使用自己的证书,其他情况都会被认为验证失败,当前请求将会被取消。当有了证书后,客户端就可以使用证书中的公钥对数据进行加密了。

    其他的方法:

    #if SD_UIKIT || SD_WATCH
    + (UIImageOrientation)orientationFromPropertyValue:(NSInteger)value {
        switch (value) {
            case 1:
                return UIImageOrientationUp;
            case 3:
                return UIImageOrientationDown;
            case 8:
                return UIImageOrientationLeft;
            case 6:
                return UIImageOrientationRight;
            case 2:
                return UIImageOrientationUpMirrored;
            case 4:
                return UIImageOrientationDownMirrored;
            case 5:
                return UIImageOrientationLeftMirrored;
            case 7:
                return UIImageOrientationRightMirrored;
            default:
                return UIImageOrientationUp;
        }
    }
    #endif
    
    - (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
        return SDScaledImageForKey(key, image);
    }
    
    - (BOOL)shouldContinueWhenAppEntersBackground {
        return self.options & SDWebImageDownloaderContinueInBackground;
    }
    
    - (void)callCompletionBlocksWithError:(nullable NSError *)error {
        [self callCompletionBlocksWithImage:nil imageData:nil error:error finished:YES];
    }
    
    - (void)callCompletionBlocksWithImage:(nullable UIImage *)image
                                imageData:(nullable NSData *)imageData
                                    error:(nullable NSError *)error
                                 finished:(BOOL)finished {
        NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
        dispatch_main_async_safe(^{
            for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
                completedBlock(image, imageData, error, finished);
            }
        });
    }
    

    总结

    SDWebImageDownloaderOperation提供了下载单张图片的能力,在真实开发中。图片往往都是一组一组的出现的。那么该如何管理这些组图片呢?下一篇文章将会揭晓答案。

    由于个人知识有限,如有错误之处,还望各路大侠给予指出啊

    1. SDWebImage源码解读 之 NSData+ImageContentType 简书 博客园
    2. SDWebImage源码解读 之 UIImage+GIF 简书 博客园
    3. SDWebImage源码解读 之 SDWebImageCompat 简书 博客园
    4. SDWebImage源码解读 之SDWebImageDecoder 简书 博客园
    5. SDWebImage源码解读 之SDWebImageCache(上) 简书 博客园
    6. SDWebImage源码解读之SDWebImageCache(下) 简书 博客园

    相关文章

      网友评论

      • 游龙飞雪:确定main是在同步执行,start是异步吗?

        把操作op添加到队列queue的时候会默认自动调用main方法,是异步的;
        给操作op发送start消息时事同步的。
      • LEORIDER1:@[@{@"completed" : Block1},
        @{@"progress" : Block2},
        @{@"completed" : Block3},
        @{@"progress" : Block4},
        @{@"completed" : Block5},
        @{@"progress" : Block6}] 这个地方有点小问题,

        应该是
        @[@{@"completed" : Block1, @"progress", Block2},
        @{@"completed" : Block3, @"progress", Block4},
        @{@"completed" : Block5, @"progress", Block6}]
      • Noah1985:即使是通过mainQueue获取主队列,它的任务也还是异步调度的。
      • Noah1985:NSOperationQueue本身调度Operation就是异步的,所以并不存在同步这个东西!我想你是将串行和同步搞混了。它有一个最大并发数,通过这个数值来控制当前并发任务的数量。设为1即并发数只有1个,相当于串行。实际上它比GCD更抽象,将串行或并发的概念模糊掉了。
        游龙飞雪:@老马的春天 把操作op添加到队列queue的时候会默认自动调用main方法,是异步的;
        给操作op发送start消息时是同步的。
        老马的春天::+1: 说的对啊 。我修改了原文:

        当手动调用start()或者main()方法的时候,和调用普通的方法没什么区别,当被加入operationQueue中后,情况不同,operationQueue中所有的`NSOperation`都是异步执行的,也就是说start()会在子线程执行,至于是串行还是并发,都由maxConcurrentOperationCount控制,当maxConcurrentOperationCount == 1时,相当于串行了

        如有错误,还望指正啊。有些地方理解的也不是很透彻。
      • Noah1985:我想知道下面这句有没有经过验证?

        “当调用start()的时候,默认的是在当前线程执行同步操作,如果是在主线程调用了,那么必然会导致程序死锁。”

      本文标题:SDWebImage源码解读之SDWebImageDownloa

      本文链接:https://www.haomeiwen.com/subject/mxpovttx.html