SDWebImage源碼解讀

整體架構

按照分組方式,可以分為幾組

定義通用宏和方法

  • SDWebImageCompat: 宏定義和C語言的一些工具方法。
  • SDWebImageOperation:定義通用的Operation協議,主要就是一個方法,cancel。從而在cancel的時候,可以面向協議編程。

下載

  • SDWebImageDownloader:實際的下載功能和配置提供者,使用了單例的設計模式。
  • SDWebImageDownloaderOperation:繼承自NSOperation,是一個異步的NSOperation,封裝了NSURLSession進行實際的下載任務。

緩存處理

  • SDImageCache:繼承自NSCache,實際處理內存cache和磁盤cache。
  • SDImageCacheConfig:緩存處理的配置。
  • SDWebImageCoder:定義了編碼解碼的協議,從而可以實現面向協議編程。

功能類

  • SDWebImageManager:宏觀的從整體上管理整個框架的類。
  • SDWebImagePrefetcher:圖片的預加載管理。

加載GIF動圖

  • FLAnimatedImage:處理加載GIF動圖的邏輯。

圖片的編碼解碼處理

  • SDWebImageCodersManager:實際的編碼解碼功能處理,使用了單例模式。

Category

  • 類別用來為UIView和UIImageView等”添加”屬性來存儲必要的信息,同時暴露出接口,進行實際的操作。

用類別來提供接口往往是最方便的,因為用戶只需要import這個文件,就可以像使用原生SDK那樣去開發,不需要修改原有的什么代碼。
面向對象開發有一個原則是-單一功能原則,所以不管是在開發一個Lib或者開發App的時候,盡量保證各個模塊之前功能單一,這樣會降低耦合。

sd_setImageWithURL的加載邏輯

1.取消正在加載的圖片

[self sd_cancelImageLoadOperationWithKey:validOperationKey];

方法源代碼如下,這里的key是FLAnimatedImageView。

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        //使用NSMapTable存儲當前的operation
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;
        //使用@synchronized保證線程安全,后面會講到
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                //這里屬于面向協議編程,不關心具體的類,只關心遵守某個協議
                [operation cancel];
            }
            @synchronized (self) {
                //刪除對應的key
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}

我們看一下SDOperationsDictionary的數據結構

- (SDOperationsDictionary *)sd_operationDictionary {
    @synchronized(self) {
        SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
        if (operations) {
            return operations;
        }
        operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
        objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        return operations;
    }
}

SDOperationsDictionary通過runtime關聯對象機制來為UIView添加的屬性。它的數據結構是NSMapTable。后面會講到。

2. 如果有PlaceHolder,設置placeHolder

if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
}

dispatch_main_async_safe是一個宏定義,會判斷是否是并行隊列,不是的話異步切換到主隊列執行。

#ifndef dispatch_queue_async_safe
#define dispatch_queue_async_safe(queue, block)\
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(queue)) {\
        block();\
    } else {\
        dispatch_async(queue, block);\
    }
#endif

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block) dispatch_queue_async_safe(dispatch_get_main_queue(), block)
#endif

3. 根據SDImageCache來查緩存,看看是否有圖片

operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {//異步返回查詢的結果}

queryCacheOperationForKey返回一個NSOperation,之所以這樣,是因為從磁盤或者內存查詢的過程是異步的,后面可能需要cancel,所以這樣做。
我們再看看queryCacheOperationForKey這個方法是怎么實現的?

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    
    // First check the in-memory cache...
    //先從檢查內存緩存
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    //如果不需要檢查磁盤直接返回
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    //創建一個NSOperation,因為從磁盤查詢的過程是異步的,后面可能需要cancel
    NSOperation *operation = [NSOperation new];
    void(^queryDiskBlock)(void) =  ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }
        @autoreleasepool {
            //從磁盤中查詢
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeDisk;
            // 從磁盤讀的圖片要解碼
            diskImage = [self diskImageForKey:key data:diskData options:options context:context];
           if (diskImage && self.config.shouldCacheImagesInMemory) {
               NSUInteger cost = diskImage.sd_memoryCost;
               //從磁盤讀取圖片后要寫入內存中
               [self.memoryCache setObject:diskImage forKey:key cost:cost];
                }
            
            if (doneBlock) {
                //回歸到主線程行,進行doneBlock操作
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, cacheType);
                });
            }
        }
    };
    
    if (options & SDImageCacheQueryDiskSync) {
        queryDiskBlock();
    } else {//切換到ioQueue,進行異步磁盤查詢操作
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    return operation;
}

這里使用到了@autoreleasepool,后面講解。
可以看到從緩存中讀取圖片首先從內存讀,內存沒有再去磁盤中讀,磁盤讀到后要去解碼,然后再將圖片寫入內存緩存中。
解壓圖片:

- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options context:(SDWebImageContext *)context {
    if (data) {
        UIImage *image = SDImageCacheDecodeImageData(data, key, [[self class] imageOptionsFromCacheOptions:options], context);
}}

4. 創建下載任務

//downloadToken用于取消下載的操作
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
//下載完成
if (downloadedImage && finished) {
                            //是否需要序列化成data
                            if (self.cacheSerializer) {                             dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                    @autoreleasepool {
                                        NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
                                        //保存至內存或磁盤
                                        [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                                    }
                                });
                            } else {
                                //如果不需要保存序列化,直接保存至內存或磁盤
                                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                        }
                        //回歸到主線程行,進行completedBlock操作
                        [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];

                if (finished) {//完成之后要記得移除這個operation
                    [self safelyRemoveOperationFromRunning:strongSubOperation];
                }
}

safelyRemoveOperationFromRunning:為了保證多線程安全,移除的時候加上鎖,這個鎖是通過信號量實現的。這個方法的源代碼如下:

- (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
    if (!operation) {
        return;
    }
    LOCK(self.runningOperationsLock);
    [self.runningOperations removeObject:operation];
    UNLOCK(self.runningOperationsLock);
}

#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);

self.runningOperations的數據結構是NSMutableSet。里面都是SDWebImageCombinedOperation對象

@property (strong, nonatomic, nonnull) NSMutableSet<SDWebImageCombinedOperation *> *runningOperations;

接下來,我們來看看實際的下載operation是什么樣子的
也就是這個方法:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`.
    if (!operation || operation.isFinished) {
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);

    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

這個方法之所以返回SDWebImageDownloadToken,應該主要是為了返回后面取消下載操作用的。
URLOperations的數據結構是一個NSMutableDictionary,key是圖片url,value是一個operation

4.1由于有各種各樣的block回調,例如下載進度的回調,完成的回調,所以需要一個數據結構來存儲這些回調

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock

其中,用來存儲回調的數據結構是一個NSMutableDictionary,其中key是圖片的url,value是回調的數組
舉個例子,存儲后應該是這樣的,

@{
        @"http://imageurl":[
                            @{
                                @"progress":progressBlock1,
                                @"completed":completedBlock1,
                            },
                            @{
                                @"progress":progressBlock2,
                                @"completed":completedBlock2,
                              },
                           ],
            //其他
}

如何做到url防護的

BOOL isFailedUrl = NO;
    if (url) {
        LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        UNLOCK(self.failedURLsLock);
    }
//如果不是有效的url就直接返回
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }

failedURLs是個NSMutableSet類型的,里面存放著請求失敗的url。所以每次在請求之前先去failedURLs檢查是否包含這個url

4.3如何保證同一個url不被下載兩次:

在創建操作之前,先去URLOperations,如果取不到或者已經完成,再去創建。因為同一個url對應的operation就只有一個

NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    // 去URLOperations去取operation,如果取不到或者已經完成才去創建operation
    if (!operation || operation.isFinished) {
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            //完成之后移除operation
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        [self.downloadQueue addOperation:operation];
    }

這樣的話可以保證一個URL在多次下載的時候,只進行多次回調,而不會進行多次網絡請求

4.4 對于同一個url,在第一次調用sd_setImage的時候進行,創建網絡請求SDWebImageDownloaderOperation。

[[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];

在看看Progress回調:

if (!self.imageData) {
        self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
    }
    [self.imageData appendData:data];

    //漸進式下載
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
        // Get the image data
        __block NSData *imageData = [self.imageData copy];
        // Get the total bytes downloaded
        const NSInteger totalSize = imageData.length;
        // Get the finish status
        BOOL finished = (totalSize >= self.expectedSize);
        
        if (!self.progressiveCoder) {
            // 創建漸進解碼實例
            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;
                }
            }
        }
        
        //在coderQueue隊列解碼圖片
        dispatch_async(self.coderQueue, ^{
            @autoreleasepool {
                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:&imageData 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);
    }

completion回調:

@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];
            }
        });
    }
    
    //保證可以取到下載完成的block
    if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
        //用__block來修飾imageData,保證在能在block中修改這個變量
        __block NSData *imageData = [self.imageData copy];
        if (imageData) {
            // 在coderQueue隊列解碼圖片
            dispatch_async(self.coderQueue, ^{
                @autoreleasepool {
                    //圖片解碼
                    UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    image = [self scaledImageForKey:key image:image];
                    
                    BOOL shouldDecode = YES;
                    // 不強制解壓GIF和webp
                    if (image.images) {
                        shouldDecode = NO;
                    }
                    //解壓圖片
                    if (shouldDecode) {
                        if (self.shouldDecompressImages) {
                            BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
                            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
                        }
                    }
                    CGSize imageSize = image.size;
                    if (imageSize.width == 0 || imageSize.height == 0) {
                        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                    } else {
                        [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
                    }
                    [self done];
                }
            });
            
        }
    }

4.5 下載圖片完成后,根據需要圖片解碼和處理圖片格式,回調給Imageview

//圖片解碼
                    UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    image = [self scaledImageForKey:key image:image];
                    
                    BOOL shouldDecode = YES;
                    // 不強制解壓GIF和webp
                    if (image.images) {
                        shouldDecode = NO;
                    }
                    
                    if (shouldDecode) {
                        if (self.shouldDecompressImages) {
                            BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
                            //解壓圖片
                            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
                        }
                    }

更新

基于最新的版本:
當下載完成后,會將圖片存入內存:

[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];

再看一下這個方法callStoreCacheProcessForOperation:

// Store cache process
- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                      url:(nonnull NSURL *)url
                                  options:(SDWebImageOptions)options
                                  context:(SDWebImageContext *)context
                          downloadedImage:(nullable UIImage *)downloadedImage
                           downloadedData:(nullable NSData *)downloadedData
                                 finished:(BOOL)finished
                                 progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                completed:(nullable SDInternalCompletionBlock)completedBlock {
    // the target image store cache type
    SDImageCacheType storeCacheType = SDImageCacheTypeAll;
    if (context[SDWebImageContextStoreCacheType]) {
        storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
    }

這里注意一下緩存策略,默認是內存和磁盤都存的

總結下整個調用過程

  • 取消上一次調用
  • 設置placeHolder
  • 保存此次operation
  • cache查詢是否已經下載過了,先檢查內存,后檢查磁盤(從磁盤讀取后會解碼)
  • 利用NSURLSession來下載圖片,根據需要解碼,回調給imageview,存儲到緩存(包括內存和磁盤)

線程管理

整個SDWebImage一共有四個隊列

  • Main queue,主隊列,在這個隊列上進行UIKit對象的更新,發送notification
  • ioQueue,用在圖片的磁盤操作
  • downloadQueue(NSOperationQueue),用來全局的管理下載的任務
  • coderQueue專門復雜解壓圖片的隊列。
    注意:barrierQueue已經被廢掉了,統一使用信號量來確保線程的安全。

圖片解碼

傳統的UIImage進行解碼都是在主線程上進行的,比如

UIImage * image = [UIImage imageNamed:@"123.jpg"]
self.imageView.image = image;

在這個時候,圖片其實并沒有解碼。而是,當圖片實際需要顯示到屏幕上的時候,CPU才會進行解碼,繪制成紋理什么的,交給GPU渲染。這其實是很占用主線程CPU時間的,而眾所周知,主線程的時間真的很寶貴

現在,我們看看SDWebImage是如何在后臺進行解碼的 :
在coderQueue進行異步解壓圖片,解壓成功后切換到主線程上回調給調用方。
注意解碼操作是在一個單獨的隊列coderQueue里面處理的

//在coderQueue隊列解壓圖片
        dispatch_async(self.coderQueue, ^{
            @autoreleasepool {
                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:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
                    }
                    //異步切換到主線程上進行回調
                    [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
                }
            }
        });

incrementallyDecodedImageWithData方法:

- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
    if (!_imageSource) {
        _imageSource = CGImageSourceCreateIncremental(NULL);
    }
    UIImage *image;
    
    // Update the data source, we must pass ALL the data, not just the new bytes
    CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
    
    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);
            
            _orientation = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:orientationValue];

        }
    }
    
    if (_width + _height > 0) {
        // Create the image
        CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
        
        if (partialImageRef) {
            image = [[UIImage alloc] initWithCGImage:partialImageRef scale:1 orientation:_orientation];
            CGImageRelease(partialImageRef);
            image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
        }
    }
    
    if (finished) {
        if (_imageSource) {
            CFRelease(_imageSource);
            _imageSource = NULL;
        }
    }
    
    return image;
}

解壓圖片

//解壓圖片
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
    
    // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
    // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
    @autoreleasepool{
        
        CGImageRef imageRef = image.CGImage;
        // device color space
        CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
        //是否有alpha通道
        BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
        // iOS display alpha info (BRGA8888/BGRX8888)
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        
        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
        // to create bitmap graphics contexts without alpha info.
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     kBitsPerComponent,
                                                     0,
                                                     colorspaceRef,
                                                     bitmapInfo);
        if (context == NULL) {
            return image;
        }
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        //解壓圖片
        UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}

緩存處理(SDImageCache)

緩存處理 包含兩塊:

  • 內存緩存(SDMemoryCache):
  • 磁盤緩存
    內存緩存集成自NSCache。添加了在收到內存警告通知UIApplicationDidReceiveMemoryWarningNotification的時候自動removeAllObjects。

再看看磁盤緩存是如何做的?
磁盤緩存是基于文件系統NSFileManager對象的,也就是說圖片是以普通文件的方式存儲到沙盒里的。

下面看一下這幾個問題:

1.磁盤緩存的默認路徑是啥:

/Library/Caches/default/com.hackemist.SDWebImageCache.default/

2.SDWebImage 緩存圖片的名稱如何 避免重名?
緩存圖片的名稱是對key做了一次md5加密處理

- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
    const char *str = key.UTF8String;
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSURL *keyURL = [NSURL URLWithString:key];
    NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
    return filename;
}

3.SDWebImage Disk默認緩存時長? Disk清理操作時間點? Disk清理原則?
默認緩存時長一周:

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

在App關閉的時候或者app退到后臺的時候(后臺清理):

//在App關閉的時候清除過期圖片
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
        //在App進入后臺的時候,后臺處理過期圖片
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];

Disk清理原則:

1.獲取文件的modify時間,然后比較下過期時間,如果過期了就刪除。
2.當磁盤緩存超過閾值后,根據最后訪問的時間排序,刪除最老的訪問圖片。

SDWebImage 如何 區分圖片格式?

將數據data轉為十六進制數據,取第一個字節數據進行判斷。

//根據data獲取圖片格式:
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    
    // File signatures table: http://www.garykessler.net/library/file_sigs.html
    uint8_t c;
    //將數據data轉為十六進制數據,取第一個字節數據進行判斷。
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return SDImageFormatJPEG;
        case 0x89:
            return SDImageFormatPNG;
        case 0x47:
            return SDImageFormatGIF;
        case 0x49:
        case 0x4D:
            return SDImageFormatTIFF;
        
    }
    return SDImageFormatUndefined;
}

SDWebImageDownloader的最大并發數和超時時長

_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";

/**
 *  The timeout value (in seconds) for the download operation. Default: 15.0.
 */
@property (assign, nonatomic) NSTimeInterval downloadTimeout;

最大并發下載量6個。下載超時時長15s。

NSMapTable

NSMapTable類似于NSDictionary,但是NSDictionary只提供了key->value的映射。NSMapTable還提供了對象->對象的映射。
NSDictionary的局限性:
NSDictionary 中存儲的 object 位置是由 key 來索引的。由于對象存儲在特定位置,NSDictionary 中要求 key 的值不能改變(否則 object 的位置會錯誤)。為了保證這一點,NSDictionary 會始終復制 key 到自己私有空間。限制:

  • 你只能使用 OC 對象作為 NSDictionary 的 key,并且必須支持 NSCopying 協議。
  • Key必須小而高效,以保證拷貝復制的時候不會造成CPU和內存的負擔。(因此key最好是值類型,最好是NSNumber或者NSString作為NSDictionary的Key)
  • 會保持對Object的強引用,即Object的引用計數+1。

NSMapTable優勢:

  • 能夠處理obj->obj的映射
  • 能夠對Key和value保持弱引用,當key或者value被釋放的時候,此entry對會自動從NSMapTable中移除。
  • 能夠以包含任意指針對象

NSMapTable對象到對象的映射:
比如一個 NSMapTable 的構造如下:

NSMapTable *keyToObjectMapping =
    [NSMapTable mapTableWithKeyOptions:NSMapTableCopyIn
                          valueOptions:NSMapTableStrongMemory];

這將會和 NSMutableDictionary 用起來一樣一樣的,復制 key,并對它的 object 引用計數 +1。
NSPointerFunctionsOptions:

NSMapTableCopyIn
NSMapTableStrongMemory
NSPointerFunctionsWeakMemory

可以通過設置NSPointerFunctionsOptions來指定的對象的內存管理方式。

我們看看SDWebImage怎么使用的NSMapTable:

        // Use a strong-weak maptable storing the secondary cache. Follow the doc that NSCache does not copy keys
        // This is useful when the memory warning, the cache was purged. However, the image instance can be retained by other instance such as imageViews and alive.
        // At this case, we can sync weak cache back and do not need to load from disk cache
        self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];

key的內存管理方式是NSPointerFunctionsStrongMemory,當一個對象添加到NSMapTable中后,key的引用技術+1。
value內存管理方式是NSPointerFunctionsWeakMemory,當一個對象添加到NSMapTable中后,key的引用技術不會+1。

這樣使用的意義在哪呢:
1.遵循NSCache不復制key的文檔。
2.當收到內存警告,緩存被清理的時候,可以保存image實例。這個時候我們可以同步弱緩存表,不需要從磁盤加載。

@autoreleasepool

@autoreleasepool {
            //從磁盤中查詢
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeDisk;
            diskImage = [self diskImageForKey:key data:diskData options:options];
            
            if (doneBlock) {
                //回歸到主線程行,進行doneBlock操作
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, cacheType);
                });
            }
        }

如果不使用autoreleasepool,已經創建的臨時變量就無法釋放,要等到下次runloop結束時,才會清空系統的自動釋放池中的臨時變量,但是這個時間是不確定的,這就會導致內存爆發式的增長。如果autoreleasepool,等待autoreleasepool結束時,里面的臨時變量就會釋放。因為autoreleasepool的作用就是加速局部變量的釋放。
具體可以看一下我這篇文章autoreleasepool

讀取Memory和Disk的時候如何保證線程安全

Memory是通過信號量。

LOCK(self.weakCacheLock);
obj = [self.weakCache objectForKey:key];
UNLOCK(self.weakCacheLock);

Disk操作是在一個單獨的IO 隊列去處理的。
存圖片至磁盤

- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
    if (!imageData || !key) {
        return;
    }
    dispatch_sync(self.ioQueue, ^{
        [self _storeImageDataToDisk:imageData forKey:key];
    });
}

取圖片:

dispatch_sync(self.ioQueue, ^{
        imageData = [self diskImageDataBySearchingAllPathsForKey:key];
    });

dispatch_semaphore_t信號量

通過宏定義使用信號量(創建,提高,降低)

#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);

使用

- (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];
    LOCK(self.callbacksLock);
    [self.callbackBlocks addObject:callbacks];
    UNLOCK(self.callbacksLock);
    return callbacks;
}

更多信號量可以看這篇文章

FOUNDATION_EXPORT

.h文件中聲明

FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStartNotification;

.m文件中是這樣實現的

NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";

如以上代碼所示,FOUNDATION_EXPORT是用來定義常量的,眾所周知,#define也可以定義常量。
他們的主要區別在哪呢?

  • 使用FOUNDATION_EXPORT定義常量在檢測字符串的值是否相等的時候效率更快,可以直接使用(myString == SDWebImageDownloadStartNotification)來比較。而define定義的常量如果要比較的話,就得使用[myString isEqualToString:SDWebImageDownloadStartNotification],效率更低一點,因為前者是比較指針地址,后者是比較每一個字符。
  • FOUNDATION_EXPORT是可以兼容c++編程的。
  • 過多的使用宏定義會產生過多的二進制文件。

@synchronized

1.為啥要引入@synchronized
Objective-C支持程序中的多線程。這就意味著兩個線程有可能同時修改同一個對象,這將在程序中導致嚴重的問題。為了避免這種多個線程同時執行同一段代碼的情況,Objective-C提供了@synchronized()指令。

2.參數
指令@synchronized()需要一個參數。該參數可以使任何的Objective-C對象,包括self。這個對象就是互斥信號量。他能夠讓一個線程對一段代碼進行保護,避免別的線程執行該段代碼。針對程序中的不同的關鍵代碼段,我們應該分別使用不同的信號量。只有在應用程序編程執行多線程之前就創建好所有需要的互斥信號量對象來避免線程間的競爭才是最安全的。

- (void)cancel {
    @synchronized (self) {
        [self cancelInternal];
    }
}
  • @synchronized 的作用是創建一個互斥鎖,保證此時沒有其它線程對self對象進行修改。這個是objective-c的一個鎖定令牌,防止self對象在同一時間內被其它線程訪問,起到線程的保護作用。
  • @synchronized 主要用于多線程的程序,這個指令可以將{ } 內的代碼限制在一個線程執行,如果某個線程沒有執行完,其他的線程如果需要執行就得等著。

@synthesize:

ios6之后 LLVM 編譯器會新增加一個技術,叫自動合成技術,會給每個屬性添加@synthesize,即

@synthesize propertyName = _propertyName;

也就是說會自動生成一個帶下劃線的實例變量,同時為屬性生成gettersetter方法。當然這些都是默認實現的。
如果我們不想使用編譯器為我們生成的實例變量,我們就可以在代碼中顯示的起一個別名:

@synthesize propertyName = _anotherPropertyName;

如果我們想要阻止編譯器自動合成,可以使用@dynamic,使用場景就是你想自己實現gettersetter方法。
5.#pragma clang diagnostic push與#pragma clang diagnostic pop

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
        if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
            if (self.options & SDWebImageDownloaderHighPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityHigh;
            } else if (self.options & SDWebImageDownloaderLowPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityLow;
            }
        }
#pragma clang diagnostic pop

表示在這個區間里忽略一些特定的clang的編譯警告,因為SDWebImage作為一個庫被其他項目引用,所以不能全局忽略clang的一些警告,只能在有需要的時候局部這樣做。

NS_ENUM && NS_OPTIONS

NS_ENUM多用于一般枚舉,NS_OPTIONS多用于同一個枚舉變量可以同時賦值多個枚舉成員的情況。

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    SDWebImageRetryFailed = 1 << 0,
    SDWebImageLowPriority = 1 << 1,
    SDWebImageCacheMemoryOnly = 1 << 2,
    SDWebImageProgressiveDownload = 1 << 3,
}

這里的NS_OPTIONS是用位運算的方式定義的。


//用“或”運算同時賦值多個選項
SDWebImageOptions option = SDWebImageRetryFailed | SDWebImageLowPriority | SDWebImageCacheMemoryOnly | SDWebImageProgressiveDownload;
 
//用“與”運算取出對應位
if (option & SDWebImageRetryFailed) {
    NSLog(@"SDWebImageRetryFailed");
}
if (option & SDWebImageLowPriority) {
    NSLog(@"SDWebImageLowPriority");
}
if (option & SDWebImageCacheMemoryOnly) {
    NSLog(@"SDWebImageCacheMemoryOnly");
}
if (option & SDWebImageProgressiveDownload) {
    NSLog(@"SDWebImageProgressiveDownload");
}

這樣,用位運算,就可以同時支持多個值。

相關面試題

假如一個界面里面有很多圖片,如何優先下載最大的圖片。

方法1:

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

可以將options設置成SDWebImageDownloaderHighPriority。就是說將這個操作設成高優先級的。

方法2:給大的圖片做標記,讓它先sd_setImage

dispatch_queue_get_label

獲取隊列標簽,經常用戶判斷是否是當前隊列:

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

判斷當前隊列是否是主隊列,如果是就直接在該隊列執行,如果不是,異步回到主隊列執行

判斷當前隊列還有一種方式:
dispatch_queue_set_specificdispatch_queue_set_specific,比如:

static void * JDHybridQueueKey = &JDHybridQueueKey;
dispatch_queue_set_specific(dispatch_get_global_queue(0, 0), JDHybridQueueKey, JDHybridQueueKey, NULL);
// 判斷是否在當前隊列上
if (dispatch_get_specific(JDHybridQueueKe y) == JDHybridQueueKey) {
}

參考鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容