每日一問(wèn)25——SDWebImage(下載)

前言

SDWebImage是我們開(kāi)發(fā)iOS APP中最常使用到的一個(gè)第三方框架,它提供對(duì)圖片的異步下載與緩存功能。無(wú)疑,通過(guò)閱讀這類(lèi)優(yōu)秀開(kāi)源框架的源碼對(duì)我們以后的開(kāi)發(fā)也有很大幫助。這篇文章主要記錄一下SDWebImage中下載相關(guān)的實(shí)現(xiàn)分析。

Downloader

SDWebImage中,實(shí)現(xiàn)異步下載圖片的文件主要是

  • SDWebImageDownloader
  • SDWebImageDownloaderOperation
    下面我們就分別對(duì)這兩個(gè)類(lèi)的設(shè)計(jì)與方法實(shí)現(xiàn)進(jìn)行分析。
SDWebImageDownloader

先從.h文件看起,SDWebImageDownloader提供了下載相關(guān)的一些枚舉,屬性還有回調(diào)使用的通知和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,
};
  • 下載隊(duì)列的執(zhí)行策略枚舉,提供隊(duì)列和棧兩種策略
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    /**
     * 默認(rèn),隊(duì)列方式
     */
    SDWebImageDownloaderFIFOExecutionOrder,

    /**
     * 棧方式
     */
    SDWebImageDownloaderLIFOExecutionOrder
};
  • 回調(diào)相關(guān)的通知和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已經(jīng)更新使用NSURLSession進(jìn)行請(qǐng)求下載。所以SDWebImageDownloader類(lèi)提供了session相關(guān)的屬性和設(shè)置。例如下面的最大并發(fā)數(shù),當(dāng)前下載數(shù),超時(shí)時(shí)間和HTTP頭的設(shè)置等。

@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;

然后來(lái)看看.m,SDWebImageDownloader的內(nèi)部私有屬性包括以下

@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(下載隊(duì)列),barrierQueue(gcd隊(duì)列,用戶(hù)讓圖片依次下載)

核心下載方法
downloadImageWithURL函數(shù)

SDWebImageDownloader中方法,大部分都是設(shè)置或讀取參數(shù)。核心下載方法只有

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

來(lái)看看這個(gè)方法的具體實(shí)現(xiàn)分析:

__weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        /******
        *這個(gè)block中只是在構(gòu)造一個(gè)SDWebImageDownloaderOperation的實(shí)例,提供給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;
    }];

我們可以看到,這個(gè)方法其實(shí)就是調(diào)用另一個(gè)addProgressCallback方法,并返回了一個(gè)SDWebImageDownloadToken類(lèi)型的對(duì)象。而addProgressCallback方法中可能需要block返回一個(gè)SDWebImageDownloaderOperation類(lèi)型的對(duì)象。關(guān)于SDWebImageDownloaderOperation類(lèi)將在下面講到。這里我們只需要知道這個(gè)方法需要構(gòu)造一個(gè)SDWebImageDownloaderOperation類(lèi)型的對(duì)象給內(nèi)部使用。

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

    __block SDWebImageDownloadToken *token = nil;

    #使用dispatch_barrier_sync,保證同一時(shí)間只有一個(gè)線程能對(duì) URLCallbacks 進(jìn)行操作
    dispatch_barrier_sync(self.barrierQueue, ^{

        //使用url保存下載任務(wù),如果這個(gè)URL被多次下載,也不會(huì)創(chuàng)建額外的任務(wù)
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        if (!operation) {

            //若該URL沒(méi)有被下載,則使用外部創(chuàng)建的operation實(shí)例。
            operation = createCallback();
            self.URLOperations[url] = operation;

            //保存這個(gè)operation下載任務(wù)
            __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;

這個(gè)函數(shù)中主要目的就是查找或構(gòu)造一個(gè)SDWebImageDownloaderOperation類(lèi)型的實(shí)例,并保存到self.URLOperations中。這里還用到一個(gè)addHandlersForProgress函數(shù),將progressBlock和completedBlock傳到operation實(shí)例中保存起來(lái)用戶(hù)后面的下載回調(diào)。

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;

返回一個(gè)SDCallbacksDictionary類(lèi)型的callbacks對(duì)象,里面存儲(chǔ)了這個(gè)任務(wù)用到的回調(diào)block,下圖是這個(gè)類(lèi)型的數(shù)據(jù)結(jié)構(gòu):

03.png

上面內(nèi)容中我們多次提到了SDWebImageDownloaderOperation類(lèi),接下來(lái)我們就對(duì)每一個(gè)下載任務(wù)進(jìn)行分析。

SDWebImageDownloaderOperation類(lèi)

SDWebImageDownloaderOperation是NSOperation的子類(lèi)。也就是說(shuō)它本身就是一個(gè)可以執(zhí)行在子線程的任務(wù)。(參考:每日一問(wèn)13——多線程之NSOperation與NSOperationQueue

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

并且這個(gè)類(lèi)遵循了NSURLSession相關(guān)的協(xié)議。猜測(cè)這個(gè)類(lèi)的實(shí)例對(duì)象應(yīng)該是處理用于單張圖片的下載任務(wù)。果然,我們可以看到里面重寫(xiě)了start方法。撇開(kāi)后臺(tái)相關(guān)的代碼不看,我們可以看到這個(gè)任務(wù)其實(shí)就是使用NSURLSession請(qǐng)求了一個(gè)request。

重寫(xiě)Start方法,創(chuàng)建下載任務(wù)
//管理下載狀態(tài),如果已取消,則重置當(dāng)前下載并設(shè)置完成狀態(tài)為YES
@synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

//是否有緩存結(jié)果
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            // Grab the cached data for later check
            NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request];
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        
//檢查外部是否創(chuàng)建了session對(duì)象,如果沒(méi)有則自己重新創(chuàng)建
        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];

//通知外部開(kāi)始下載該URL的任務(wù)
    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"}]];
    }

處理返回結(jié)果

通過(guò)NSURLSeesion的代理獲取返回結(jié)果。

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

任務(wù)接受到響應(yīng)時(shí)的回調(diào),在這里面可以判斷本次請(qǐng)求是否成功,并將預(yù)計(jì)文件數(shù)據(jù)大小,下載的URL等信息回調(diào)給外部。

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

//獲取預(yù)估大小,回調(diào)給進(jìn)度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);
        }
        
//由于圖片數(shù)據(jù)是分批返回的,這里先為imagedata分配一塊大小合適的內(nèi)存空間,并通知外部接受到了成功的返回
        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;

//失敗情況則停止本次下載任務(wù),并通知外部該任務(wù)已被取消        
        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];
    }

在知道本次請(qǐng)求成功后,就可以開(kāi)始處理返回的數(shù)據(jù)了。通過(guò)代理方法:

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

這個(gè)代理會(huì)執(zhí)行很多次,每一次都會(huì)返回一部分?jǐn)?shù)據(jù)回來(lái),下面看看這里面具體是怎么處理的:

//將本次返回的數(shù)據(jù)添加到imageData中
[self.imageData appendData:data];

//判斷是否需要進(jìn)度
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {

//獲取當(dāng)前數(shù)據(jù)總大小,與預(yù)估大小比較,判斷是否下載完畢
        // 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格式的話就不作處理,其他格式圖片則新創(chuàng)建一個(gè)支持SDWebImageProgressiveCoder協(xié)議的對(duì)象。
        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;
                }
            }
        }
        
//對(duì)數(shù)據(jù)進(jìn)行強(qiáng)制解壓,生成位圖返回給外部,可以做到部分顯示變?nèi)?        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];
        }
    }

//返回當(dāng)前進(jìn)度,和預(yù)估總進(jìn)度,外部可以用來(lái)生成進(jìn)度條
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }

當(dāng)所有數(shù)據(jù)接收完畢后,會(huì)執(zhí)行完成回調(diào):

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

在這個(gè)回調(diào)中,主要就是通知外部本次請(qǐng)求結(jié)束,然后將結(jié)果回調(diào)出去

@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) {

//檢測(cè)時(shí)候開(kāi)啟緩存策略,并且當(dāng)前下載的圖片和緩存的圖片是否一致
                if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {

//是緩存過(guò)的圖片,則直接回調(diào)nil
                    [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
                } else {

//沒(méi)有緩存過(guò),則對(duì)圖片進(jìn)行解壓,然后回調(diào)給外部
                    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 {

//回調(diào)失敗信息
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
            }
        }
    }

//結(jié)束本次下載任務(wù)
    [self done];

小結(jié):關(guān)于SDWebImage下載模塊主要需要注意的就是任務(wù)的緩存處理,合理的創(chuàng)建下載資源,使用NSOperation創(chuàng)建并發(fā)任務(wù),將每一個(gè)下載任務(wù)分配到operation中。

相關(guān)文章

SDWebImage源碼閱讀筆記
SDWebImage源碼閱讀
SDWebImage 源碼閱讀筆記(三)
SDWebImage源碼閱讀筆記

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容