美文网首页
每日一问25——SDWebImage(下载)

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

作者: 巫师学徒 | 来源:发表于2017-11-02 15:07 被阅读57次

    前言

    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<NSString *, NSString *> SDHTTPHeadersDictionary;
    typedef NSMutableDictionary<NSString *, NSString *> 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<NSURL *, SDWebImageDownloaderOperation *> *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,下图是这个类型的数据结构:

    03.png

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

    SDWebImageDownloaderOperation类

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

    @interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperationInterface, SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
    

    并且这个类遵循了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 (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
                    if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
                        [((id<SDWebImageProgressiveCoder>)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(下载)

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