你好,歡迎來到IOS教程網

 Ios教程網 >> IOS使用技巧 >> IOS技巧綜合 >> SDWebImage4.0.0 源碼解析

SDWebImage4.0.0 源碼解析

編輯:IOS技巧綜合
[摘要]本文是對SDWebImage4.0.0 源碼解析的講解,對學習IOS蘋果軟件開發有所幫助,與大家分享。

在開發iOS的客戶端應用時,經常需要從服務器下載圖片,雖然系統提供了下載工具:NSData、NSURLSession等等方法,但是考慮到圖片下載過程中,需要考慮的因素比較多,比如:異步下載、圖片緩存、錯誤處理、編碼解碼等,以及實際需要中根據不同網絡加載不同畫質的圖片等等需求,因此下載操作不是一個簡單的下載動作就可以解決。

針對上述問題,目前常用的開源庫就是SDWebImage,它很好的解決了圖片的異步下載、圖片緩存、錯誤處理等問題,得到了廣泛的應用,使得設置UIImageViewUIButton對象的圖片十分方便。本文就從源碼的角度,剖析一下這款優秀的開源庫的具體實現。

類結構圖

SDWebImage的源碼的類結構圖和下載流程圖在官方的說明文檔裡有介紹,通過UML類結構圖詳細的介紹了該框架的內部結構,以及通過流程圖介紹了具體的下載過程。

下圖是我總結的SDWebImage的結構圖,簡單的把SDWebImage源碼文件按照功能進行了劃分,方便在閱讀源碼時,能快速的對源碼有一個總體的認識,加快閱讀效率。

![](http://upload-images.jianshu.io/upload_images/1843940-c51585b28704fae9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

關鍵類功能簡介:

    1.下載類

SDWebImageDownloader:提供下載的方法給SDWebImageManager使用,提供了最大並發量的下載控制、超時時間、取消下載、下載掛起、是否解壓圖片等等功能。同時,還提供了開始下載和停止下載的通知,給使用者監測下載狀態,如果使用者不用監測下載狀態,就不用監測該通知,這種設計模式很靈活,給使用者提供了更方便的選擇。

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

SDWebImageDownloaderOperation:繼承自NSOperation,是圖片下載的具體實現類,通過加入到NSOperationQueue中,然後在start方法中來開啟下載操作。

    2.圖片緩存

SDImageCacheConfig:主要提供緩存的配置信息,如:是否解壓圖片、是否緩存到內存、最大緩存時間(默認是一周)和最大緩存的字節數等等。

SDImageCache:緩存實現類,提供最大緩存字節、最大緩存條目的控制,以及緩存到內存及磁盤、從內存或磁盤刪除、查詢檢索和查詢緩存信息等功能。

    3.分類

UIImageView+WebCache:UIImageView的分類,提供了設置UIImageView對象圖片的多種方法,下面的方法可以說是SDWebImage框架中最常用的方法。

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options;

// 帶完成block的賦值方法                   
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

UIButton+WebCache:UIButton的分類,提供了設置按鈕圖片和按鈕背景圖片的功能

- (void)sd_setImageWithURL:(nullable NSURL *)url
                  forState:(UIControlState)state
          placeholderImage:(nullable UIImage *)placeholder;
    4.工具類

SDWebImageDecoder:圖像解碼的工具類,通過imageNames:加載圖片會立即進行解碼,而通過imageWithContentsOfFile:則不會

SDWebImagePrefetcher:批量圖像下載工具,針對UI界面中需要下載多個圖片時,又要在滑動中保持流暢體驗,則可以使用該工具類批量下載圖片,然後在給具體的UI控件設置圖片時,就會直接從緩存中取

SDWebImageManager:下載管理類工具,是SDWebImage的核心類,從官方文檔的類圖中也可以看出,提供了查看圖片是否已經緩存、下載圖片、緩存圖片、取消所有的下載等等功能

    5.圖片格式類

NSData+ImageContentType:根據圖片數據的第一個字節來獲取圖片的格式,可以區分PNGJPEGGIFTIFFWebP

以上只是對SDWebImage類結構圖的簡單分析,如果需要進一步了解各個類的具體實現,請參考文末的資料,已有人詳細的介紹了各個類的功能實現原理或方法。

應用

下面介紹一個在應用SDWebImage設置UI圖片的源碼實現過程

在UIImageView上的應用

設置圖片

通過設置URL、占位圖片、圖片配置、圖片下載進度回調和設置完成回調來給UIImageView對象設置圖片

// ViewController.m
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://rescdn.qqmail.com/dyimg/20140302/73EB27F4A350.jpg"] placeholderImage:[UIImage imageNamed:@"gift-icon"] options:0 progress:nil completed:nil];

上述代碼調用UIImageView+WebCache.m裡的方法

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
                       setImageBlock:nil
                            progress:progressBlock
                           completed:completedBlock];
}

然後調用UIView+WebCache.m中的方法獲取圖片,然後根據option的類型進行不同的設置

// UIView+WebCache.m
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock 
{
    ...
    if (url) {
        ...
        
        __weak __typeof(self)wself = self;
        // 開始加載圖片
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            ...
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    // 把圖片放到completedBlock裡處理,一般是手動設置圖片,因為這樣可以對圖片做進一步處理
                    completedBlock(image, error, cacheType, url);
                    return;
                } else if (image) {
                    // 設置圖片
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];
                } else {
                    // 延遲加載占位圖(獲取圖片之後)
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                
                // 回調完成block,如果是nil,則不調用
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
    } else {
         // 處理url為nil的情況
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

加載圖片的具體實現代碼在SDWebImageManager裡面,先從緩存中取圖片,如果緩存中沒有圖片,就從網絡下載,然後設置圖片,最後再緩存該圖片

// SDWebImageManager.m
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock 
{
    ...
    // 從緩存中取圖片
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        if (operation.isCancelled) {
            // 如果操作被取消,就從runningOperations操作數組從把該操作刪除
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }

        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            if (cachedImage && options & SDWebImageRefreshCached) {
                // 如果options設置為更新緩存,那麼就需要從服務器從新下載圖片,然後更新本地緩存
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }
            ...
            // 創建下載器,從服務器下載圖片
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                ...
                else {
                    // 設置了options為失敗了重試,則會把失敗的url加入failedURLs數組
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    ...
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        // 對圖片進行transform操作
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        // 緩存圖片,有緩存到內存和磁盤兩種方式
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                        // 回調完成的block
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                if (finished) {
                    // 下載完成,就從runningOperations數組中刪除操作
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            // 設置取消下載的回調
            operation.cancelBlock = ^{
                [self.imageDownloader cancel:subOperationToken];
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                [self safelyRemoveOperationFromRunning:strongOperation];
            };
        } else if (cachedImage) {
            // 從緩存在取到圖片,回調完成block
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
        ...
    }];

    return operation;
}

從緩存中取圖片,是先從內存中取,如果在內存中取到,就在當前線程中直接回調doneBlock;如果內存中沒有,就開子線程從磁盤中取,如果取到圖片,就回調doneBlock

// SDImageCache.m
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    ...
    // First check the in-memory cache...
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        NSData *diskData = nil;
        if ([image isGIF]) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }

        @autoreleasepool {
            // 從磁盤中取圖片的data
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            // 從磁盤中直接取圖片
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                // 緩存到內存中
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });

    return operation;
}

圖片的下載過程是在SDWebImageDownloader.m中進行的,實質是通過SDWebImageDownloaderOperation(繼承自NSOperation)對象,把該對象加入到downloadQueue裡,然後在start方法裡通過NSURLSession來下載圖片。(其中,NSOperation有兩個方法:mainstart,如果想使用同步,那麼最簡單方法的就是把邏輯寫在main()中,使用異步,需要把邏輯寫到start()中,然後加入到隊列之中)

// SDWebImageDownloader.m
- (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;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        // 設置超時時間為15s
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) 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;
        
        ...
        // 加入操作隊列,開始下載
        [sself.downloadQueue addOperation:operation];
        ...

        return operation;
    }];
}

SDWebImageDownloaderOperation對象加入到操作隊列,就開始調用該對象的start方法。

// SDWebImageDownloaderOperation.m
- (void)start {
    // 如果操作被取消,就reset設置
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

        ...
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            // 創建session的配置
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            // 創建session對象
            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"}]];
    }

    ...
}

在下載過程中,會涉及鑒權、響應的statusCode判斷(404304等等),以及收到數據後的進度回調等等,在最後的didCompleteWithError裡做最後的處理,然後回調完成的block,下面僅分析一下didCompleteWithError的方法

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    ...
    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]) {
                // 如果options是忽略緩存,而圖片又是從緩存中取的,就給回調傳入nil
                [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) {
                    // 不是Gif圖像
                    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)) {
                    // 下載是圖片大小的0
                    [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"}]];
            }
        }
    }
    ...
}

以上就是給UIImageView對象設置圖片的過程,可以看出還是比較復雜的,考慮的情況也比較多,不得不佩服作者的編碼能力。至於UIButton的圖片設置過程,分析情況類似,在此不做分析。

SDWebImage的源碼中在設置圖片的過程中,還應用了多種技術:GCD的線程組、鎖機制、並發控制、隊列、圖像解碼、緩存控制等等,是一個綜合性十分強的項目了,通過閱讀源碼,對這些技術的使用也有了進一步的認知,對作者的編程功力的深厚深深折服。


SDWebImage的解析到此結束,本文只是簡單的從源碼結構、UIImageView的使用角度進行了簡單的分析,希望對閱讀源碼的朋友有一些幫助,如果文中有不足之處,還望不吝指出,互相學習。

參考資料

SDWebImage源碼

SDWebImage源碼解讀

SDWebImage源碼(一)——SDWebImage概覽

iOS開發——你真的會用SDWebImage?

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved