SDWebImage源碼解析

SDWebImage是一個(gè)開(kāi)源的第三方庫(kù),支持從遠(yuǎn)程服務(wù)器下載并緩存圖片的功能。它具有以下功能:

  • 提供UIImageView的一個(gè)分類,以支持網(wǎng)絡(luò)圖片的加載與緩存管理
  • 一個(gè)異步的圖片下載器
  • 一個(gè)圖片內(nèi)存緩存+異步的磁盤(pán)緩存
  • 支持GIF圖片
  • 支持WebP圖片
  • 后臺(tái)圖片解壓縮處理
  • 確保同一個(gè)URL的圖片不被下載多次
  • 確保虛假的URL不會(huì)被反復(fù)加載
  • 確保下載及緩存時(shí),主線程不被阻塞

UIImageView+WebCache.h

  1. SDWebImageCompat.h: 定義一些宏定義
  2. SDWebImageDefine.h:
    定義關(guān)于圖片操作的option(SDWebImageOptions ):圖片讀取操作、圖片下載優(yōu)先級(jí)、圖片后臺(tái)下載、圖片下載完成展示操作等
    key:
  3. UImageView+WebCache.h
    最上層對(duì)外接口,對(duì)ImageVIew的擴(kuò)展,提供圖片設(shè)置、占位圖設(shè)置、圖片操作、下載過(guò)程回調(diào)、下載完成回調(diào)操作

/// 異步下載和異步緩存圖片
/// @param url url
/// @param placeholder 展位圖
/// @param options 圖片操作,具體參考 SDWebImageOptions.h
/// @param context <#context description#>
/// @param progressBlock 圖片下載回調(diào),包括已下載大小、全部大小、url
/// @param completedBlock 圖片下載回調(diào),包括圖片、下載失敗錯(cuò)誤信息、圖片獲取類型(SDImageCacheType:內(nèi)存、硬盤(pán)、遠(yuǎn)程下載)
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
         options:(SDWebImageOptions)options
         context:(nullable SDWebImageContext *)context
        progress:(nullable SDImageLoaderProgressBlock)progressBlock
       completed:(nullable SDExternalCompletionBlock)completedBlock;

這個(gè)方法的內(nèi)部是跳轉(zhuǎn)調(diào)用UIView+WebCache.h內(nèi)部的方法

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock;
  1. UIView+WebCache.h內(nèi)部實(shí)現(xiàn)大致為以下省略版:
    最終會(huì)調(diào)用SDWebImageManager-loadImageWithURL: options:progress:completed
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock {se'
    
    //刪除該對(duì)象之前的下載操作
    [self sd_cancelImageLoadOperationWithKey:NSStringFromClass(self.class)];
    self.sd_imageURL = url;
    
    //展示占位圖 placeholder
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
    
    SDWebImageManager *manager = [SDWebImageManager sharedManager];
    if (url) {
        //url不為nil,進(jìn)一步獲取圖片處理,并返回一個(gè)NSOperation的對(duì)象
        @weakify(self);

        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            
            //url處理完成回調(diào)
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (completedBlock) {
                    completedBlock(image, data, error, cacheType, finished, url);
                }
            };
            
            //展示url下載的圖片
            UIImage *targetImage = image;
            NSData *targetData = data;
            dispatch_main_async_safe(^{
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];

                callCompletedBlockClojure();
            });
        }];
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        //url為nil回調(diào)
        dispatch_main_async_safe(^{
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
            }
        });
    }
}

類似的,SDWebImage也提供了 UIButton+WebCacheMKAnnotationView+WebCache,方便使用

SDWebImageManager.h

SDWebImageManager處理圖片url,獲取圖片后回調(diào),其內(nèi)部獲取圖片的過(guò)程大致為兩個(gè)步驟:

  • 首先會(huì)交由 SDImageCache處理, 從緩存中(內(nèi)存+本地硬盤(pán))查找圖片
  • 如果緩存中不存在圖片,再交由SDWebImageDownloader下載圖片
  • 圖片下載完成,緩存到到內(nèi)存和本地磁盤(pán)中

獲取圖片入口

- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock {
    ...
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;

    //信號(hào)量,開(kāi)啟線程保護(hù),防止多線程操作添加多個(gè)operation
    dispatch_semaphore_wait(self.runningOperationsLock, DISPATCH_TIME_FOREVER);
    [self.runningOperations addObject:operation];
    dispatch_semaphore_signal(self.runningOperationsLock)

    // 從緩存中查找 url 對(duì)應(yīng)的圖片
    [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
    return operation;
}

//緩存中查找圖片
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock {
   
    id<SDImageCache> imageCache = [SDImageCache sharedImageCache];;
    
    //url作為圖片的標(biāo)識(shí)符key
    NSString *key = [self cacheKeyForURL:url context:context];

    //在緩存中查找圖片是否下載過(guò)
    @weakify(operation);
    operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
        @strongify(operation);
        if (!operation || operation.isCancelled) {
            // 圖片下載操作被取消
            [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }
        //圖片未下載完畢,繼續(xù)下載
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];

     // 緩存中不存在,開(kāi)始下載圖片
    [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}

本地不存在圖片,開(kāi)始下載

- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                    url:(nonnull NSURL *)url
                                options:(SDWebImageOptions)options
                                context:(SDWebImageContext *)context
                            cachedImage:(nullable UIImage *)cachedImage
                             cachedData:(nullable NSData *)cachedData
                              cacheType:(SDImageCacheType)cacheType
                               progress:(nullable SDImageLoaderProgressBlock)progressBlock
                              completed:(nullable SDInternalCompletionBlock)completedBlock {
    
    id<SDImageLoader> imageLoader = self.imageLoader = [SDWebImageDownloader sharedDownloader];;
    
    // 當(dāng)本地沒(méi)有緩存的圖片,或者需要刷新緩存時(shí),下載新圖片
    BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly)
    & (!cachedImage || options & SDWebImageRefreshCached)
    & [imageLoader canRequestImageForURL:url];
    
    if (shouldDownload) {
        if (cachedImage && options & SDWebImageRefreshCached) {
            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
            SDWebImageMutableContext *mutableContext [context mutableCopy];
            mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
            context = [mutableContext copy];
        }
        
        @weakify(operation);
        // 1、下載圖片
        operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
            @strongify(operation);
            // 圖片下載過(guò)程異常的回調(diào)
            if (!operation || operation.isCancelled) { //用戶取消
                ...
            } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
                ...
            } else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
                ...
            } else if (error) {
                [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
            } else {
                // 2、存儲(chǔ)圖片到緩存
                [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
            }
             //從集合中移除下載操作
            if (finished) {
                [self safelyRemoveOperationFromRunning:operation];
            }
        }];
    } else if (cachedImage) {
        [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
        [self safelyRemoveOperationFromRunning:operation];
    } else {
        // Image not in cache and download disallowed by delegate
        [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
        [self safelyRemoveOperationFromRunning:operation];
    }
}

存儲(chǔ)圖片到緩存中

- (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 {
    // 圖片緩存key
    SDWebImageMutableContext *originContext = [context mutableCopy];
    NSString *key = [self cacheKeyForURL:url context:originContext];
    
    // 存儲(chǔ)圖片
    [self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:SDImageCacheTypeAll options:options context:context completion:^{
        ...
    }];
}

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)data
            forKey:(nullable NSString *)key
         cacheType:(SDImageCacheType)cacheType
           options:(SDWebImageOptions)options
           context:(nullable SDWebImageContext *)context
        completion:(nullable SDWebImageNoParamsBlock)completion {

    id<SDImageCache> imageCache = [SDImageCache sharedImageCache];
    [imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{
        if (completion) {
            completion();
        }
    }];
}

"SDWebImageError.h包含定義了錯(cuò)誤碼枚舉的文件:

typedef NS_ERROR_ENUM(SDWebImageErrorDomain, SDWebImageError) {}

圖片緩存讀取、存儲(chǔ)

SDImageCache + SDMemoryCache + SDDiskCache

SDMemoryCache初始化:

  • 創(chuàng)建串行隊(duì)列dispatch_queue_t,用于異步存儲(chǔ)、讀取本地磁盤(pán)圖片,避免阻塞主線程,同時(shí)保證圖片資源訪問(wèn)的線程安全
  • SDMemoryCache(繼承自NSCache),緩存內(nèi)存圖片,方便下次快速訪問(wèn)相同圖片
  • SDDiskCache緩存本地磁盤(pán)圖片
  • 注冊(cè)消息通知,在內(nèi)存警告或退到后臺(tái)的時(shí)候清理內(nèi)存圖片緩存,應(yīng)用結(jié)束的時(shí)候清理過(guò)期圖片。

圖片讀取、存儲(chǔ):

默認(rèn)情況下包括兩個(gè)過(guò)程,可根據(jù)需求進(jìn)行配置:

  • 內(nèi)存圖片讀取、存儲(chǔ)
  • 本地磁盤(pán)圖片讀取、存儲(chǔ),異步執(zhí)行
//存儲(chǔ)圖片
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
          toMemory:(BOOL)toMemory
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    // 1、存儲(chǔ)到內(nèi)存中
    if (toMemory) {
        NSUInteger cost = image.sd_memoryCost;
        [self.memoryCache setObject:image forKey:key cost:cost];
    }
    
    // 2、異步存儲(chǔ)到本地磁盤(pán)
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
            @autoreleasepool {
                [self _storeImageDataToDisk:imageData forKey:key];
            }
            
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}

//讀取圖片
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
    
    // 1、首先檢查內(nèi)存中是否存在相應(yīng)圖片
    UIImage *image;
    if (queryCacheType != SDImageCacheTypeDisk) {
        image = [self imageFromMemoryCacheForKey:key];
    }

    BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    
    // 2、檢查磁盤(pán)中會(huì)否存在圖片
    NSOperation *operation = [NSOperation new];
    BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                                (!image && options & SDImageCacheQueryDiskDataSync));
    void(^queryDiskBlock)(void) =  ^{

        @autoreleasepool {
            //從磁盤(pán)中查找圖片數(shù)據(jù)
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeNone;
            
            /*
             內(nèi)存中存在圖片,則取內(nèi)存圖片;
             內(nèi)存中不存在,則取磁盤(pán)圖片,同時(shí)添加到內(nèi)存中,方便下次快速訪問(wèn)此圖片
             */
            if (image) {
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                cacheType = SDImageCacheTypeDisk;
                diskImage = [self diskImageForKey:key data:diskData options:options context:context];
                if (diskImage) {
                    //圖片保存在內(nèi)存中
                    NSUInteger cost = diskImage.sd_memoryCost;
                    [self.memoryCache setObject:diskImage forKey:key cost:cost];
                }
            }
            
            if (doneBlock) {
                if (shouldQueryDiskSync) {
                    doneBlock(diskImage, diskData, cacheType);
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, cacheType);
                    });
                }
            }
        }
    };
    
    // 在ioQueue中進(jìn)行查詢以保持IO安全
    if (shouldQueryDiskSync) {
        dispatch_sync(self.ioQueue, queryDiskBlock);
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    
    return operation;
}
內(nèi)存圖片讀取、存儲(chǔ):

由于NSCahe類的緩存釋放時(shí)間完全由系統(tǒng)管理,我們無(wú)法得知NSCahe緩存的釋放時(shí)間,這樣可能在我們需要這個(gè)緩存的時(shí)候,系統(tǒng)卻已經(jīng)把這個(gè)緩存給釋放了。因此,子類SDMemoryCache初始化時(shí)維護(hù)了一個(gè)引用表 NSMapTable,強(qiáng)引用key,弱引用value,用來(lái)在需要某個(gè)緩存的時(shí)候,確定這個(gè)緩存不會(huì)被釋放。同時(shí)使用信號(hào)量保證多線程安全訪問(wèn)引用表NSMapTable。

self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];

self.weakCacheLock = dispatch_semaphore_create(1);

具體實(shí)現(xiàn)過(guò)程

  1. SDMemoryCache重寫(xiě)了NSCache的核心方法- setObject: forKey: cost:,存儲(chǔ)到系統(tǒng)管理的NSCache內(nèi)存時(shí),同時(shí)存儲(chǔ)一份引用到引用表NSMapTable
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
    [super setObject:obj forKey:key cost:g];
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    if (key && obj) {
        // Store weak cache
        SD_LOCK(self.weakCacheLock);
        [self.weakCache setObject:obj forKey:key];
        SD_UNLOCK(self.weakCacheLock);
    }
}
  1. 重載了NSCache的- objectForKey:,
    1)首先在系統(tǒng)的NSCache里查找,因?yàn)镹SCache的釋放完全由系統(tǒng)管理,所以取值的時(shí)候很可能value已經(jīng)被系統(tǒng)釋放了。
    2)如果NSCache內(nèi)存里沒(méi)有找到,或者已經(jīng)被系統(tǒng)釋放了,在弱引用表NSMapTable查找,如果有值,則添加到NSCache內(nèi)存中,這樣在下次再取值的時(shí)候,如果NSCache中的value沒(méi)有被釋放,我們就能直接拿來(lái)用

這樣保證了每次取value的時(shí)候,就算NSCache的那一份已經(jīng)釋放了,我們自己存的還能拿出來(lái)用。用內(nèi)存空間換取了查詢時(shí)間。盡可能提高效查詢效率的同時(shí),又保證了訪問(wèn)過(guò)的數(shù)據(jù)不被釋放。

- (id)objectForKey:(id)key {
    id obj = [super objectForKey:key];
    if (key && !obj) {
        // Check weak cache
        SD_LOCK(self.weakCacheLock);
        obj = [self.weakCache objectForKey:key];
        SD_UNLOCK(self.weakCacheLock);
        if (obj) {
            // Sync cache
            NSUInteger cost = 0;
            if ([obj isKindOfClass:[UIImage class]]) {
                cost = [(UIImage *)obj sd_memoryCost];
            }
            [super setObject:obj forKey:key cost:cost];
        }
    }
    return obj;
}

本地磁盤(pán)圖片讀取、存儲(chǔ):

圖片存儲(chǔ)的磁盤(pán)位置diskCachePath:沙盒->Library->Cache

NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject

圖片存儲(chǔ)的文件名稱url md5加密后的字符串

多線程安全:SDImageCache使用串行隊(duì)列調(diào)用SDDiskCache存儲(chǔ)、讀取磁盤(pán)圖片

使用NSFileManager文件管理器創(chuàng)建相應(yīng)文件目錄,將圖片數(shù)據(jù)寫(xiě)入目錄下

- (void)setData:(NSData *)data forKey:(NSString *)key {
    if (![self.fileManager fileExistsAtPath:self.diskCachePath]) {
        [self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // 對(duì)url進(jìn)行md5加密,作為圖片路徑名稱
    NSString *cachePathForKey = [self cachePathForKey:key];
    
    //存儲(chǔ)到本地磁盤(pán)
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    [data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
    
    // 不要被備份到iClound
    if (self.config.shouldDisableiCloud) {
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}
- (NSData *)dataForKey:(NSString *)key {
    NSParameterAssert(key);
    NSString *filePath = [self cachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
    if (data) {
        return data;
    }
    data = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
    if (data) {
        return data;
    }
    
    return nil;
}

注意:存儲(chǔ)在本地的圖片格式為PNG、JPG等,這是經(jīng)過(guò)編碼壓縮后的圖片數(shù)據(jù),不是位圖,要把它們渲染到屏幕前就需要進(jìn)行解碼轉(zhuǎn)成位圖數(shù)據(jù)(因?yàn)槲粓D體積很大,所以磁盤(pán)緩存不會(huì)直接緩存位圖數(shù)據(jù),而是編碼壓縮后的PNG或JPG數(shù)據(jù)),而這個(gè)解碼操作比較耗時(shí),iOS默認(rèn)是在主線程解碼,所以SDWebImage將這個(gè)過(guò)程放到子線程了。具體的過(guò)程下面會(huì)說(shuō)明。

清除緩存圖片:

  • 內(nèi)存圖片清除:SDMemoryCache繼承自NSCache,自動(dòng)響應(yīng)內(nèi)存警告,實(shí)現(xiàn)緩存圖片清除
  • 本地磁盤(pán)圖片清除:SDImageCache注冊(cè)了通知監(jiān)聽(tīng),當(dāng)應(yīng)用結(jié)束運(yùn)行時(shí),使用UIApplication的如下方法,使用應(yīng)用繼續(xù)運(yùn)行,調(diào)用SDDiskCache刪除過(guò)期文件方法清除磁盤(pán)緩存圖片
- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler;

調(diào)用SDDiskMemory刪除過(guò)期文件:

  1. 刪除過(guò)期文件
  2. 如果磁盤(pán)圖片緩存依然大于限制的大小,繼續(xù)清除最舊的圖片
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        [self.diskCache removeExpiredData];
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}
- (void)removeExpiredData {
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    
    // Compute content date key to be used for tests
    NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
    switch (self.config.diskCacheExpireType) {
        case SDImageCacheConfigExpireTypeAccessDate:
            cacheContentDateKey = NSURLContentAccessDateKey;
            break;
        case SDImageCacheConfigExpireTypeModificationDate:
            cacheContentDateKey = NSURLContentModificationDateKey;
            break;
        case SDImageCacheConfigExpireTypeCreationDate:
            cacheContentDateKey = NSURLCreationDateKey;
            break;
        case SDImageCacheConfigExpireTypeChangeDate:
            cacheContentDateKey = NSURLAttributeModificationDateKey;
            break;
        default:
            break;
    }
    
    NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
    
    // This enumerator prefetches useful properties for our cache files.
    NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                               includingPropertiesForKeys:resourceKeys
                                                                  options:NSDirectoryEnumerationSkipsHiddenFiles
                                                             errorHandler:NULL];
    
    NSDate *expirationDate = (self.config.maxDiskAge < 0) ? nil: [NSDate dateWithTimeIntervalSinceNow:-self.config.maxDiskAge];
    NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
    NSUInteger currentCacheSize = 0;
    
     /*
        枚舉緩存目錄中的所有文件。 這個(gè)循環(huán)有兩個(gè)目的:
        1.刪除過(guò)期日期之前的文件。
        2.存儲(chǔ)基于大小的清理過(guò)程的文件屬性。
    */
    NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
    for (NSURL *fileURL in fileEnumerator) {
        NSError *error;
        NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
        
        // Skip directories and errors.
        if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
            continue;
        }
        
        // 刪除過(guò)期文件
        NSDate *modifiedDate = resourceValues[cacheContentDateKey];
        if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
            [urlsToDelete addObject:fileURL];
            continue;
        }
        
        // Store a reference to this file and account for its total size.
        NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
        currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
        cacheFiles[fileURL] = resourceValues;
    }
    
    for (NSURL *fileURL in urlsToDelete) {
        [self.fileManager removeItemAtURL:fileURL error:nil];
    }
    
    // 如果剩余的磁盤(pán)緩存超過(guò)配置的最大大小,請(qǐng)執(zhí)行基于大小的清理操作 我們先刪除最舊的文件
    NSUInteger maxDiskSize = self.config.maxDiskSize;
    if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
        // Target half of our maximum cache size for this cleanup pass.
        const NSUInteger desiredCacheSize = maxDiskSize / 2;
        
        // 以最新修改時(shí)間重新排序剩余文件
        NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                 usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                     return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
                                                                 }];
        
        // Delete files until we fall below our desired cache size.
        for (NSURL *fileURL in sortedFiles) {
            if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
                NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
                
                if (currentCacheSize < desiredCacheSize) {
                    break;
                }
            }
        }
    }
}


圖片下載

SDWebImageDownloader + SDWebImageDownloaderOperation

SDWebImageDownloader:

實(shí)現(xiàn)異步圖片下載,SDWebImageDownloader初始化包括:

  • 下載配置SDWebImageDownloaderConfig下載超時(shí)時(shí)間15s、最大并發(fā)數(shù)6、FIFO的圖片下載順序
  • 線程下載隊(duì)列NSOperationQueue,防止阻塞主線程
  • NSMutableDictionary哈希表管理下載操作NSOperation,以u(píng)rl為key,避免同一個(gè)URL的圖片下載多次
  • 下載會(huì)話管理NSURLSession

圖片下載最終調(diào)用- downloadImageWithURL:options:context:progress:completed:

  1. 信號(hào)量dispatch_semaphore_t開(kāi)啟線程保護(hù)
  2. 以u(píng)rl為key在查詢哈希表NSMutableDictionary,查找對(duì)應(yīng)的下載操作NSOperation
  3. 哈希表中不存在相應(yīng)的NSOperation,就創(chuàng)建一個(gè) SDWebImageDownloaderOperation(繼承自NSOperation)的下載操作
  4. 緩存 operation 到哈希表中,下次查詢使用,避免同一個(gè)url下載多次
  5. operation添加到 NSOperationQueue 開(kāi)始下載

代碼大致如下:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    //1. 信號(hào)量多線程保護(hù)
    SD_LOCK(self.operationsLock);
    id downloadOperationCancelToken;
    
    //2.哈希表中以u(píng)rl為key獲取緩存的下載操作NSOperation
    NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
    
    //哈希表不存在相應(yīng)下載操作Operation,或者操作已被取消,重新創(chuàng)建一個(gè)SDWebImageDownloaderOperation(繼承NSOperation),添加到隊(duì)列中開(kāi)始下載,并緩存到哈希表中方便查詢
    if (!operation || operation.isFinished || operation.isCancelled) {
        //3.創(chuàng)建一個(gè)Operation
        operation = [self createDownloaderOperationWithUrl:url options:options context:context];

        //Operation執(zhí)行完畢后從哈希表URLOperations中移除
        @weakify(self);
        operation.completionBlock = ^{
            @strongify(self);
            SD_LOCK(self.operationsLock);
            [self.URLOperations removeObjectForKey:url];
            SD_UNLOCK(self.operationsLock);
        };
        //4.緩存到哈希表中
        self.URLOperations[url] = operation;
        
       // 該方法將 progressBlock 和 completedBlock 緩存到 operation 內(nèi)部定義的哈希表,以便在下載的時(shí)候執(zhí)行相應(yīng)的回調(diào)操作
        downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        
        //5.添加下載操作operation到隊(duì)列NSOperationQueue中,開(kāi)始下載
        [self.downloadQueue addOperation:operation];
    }
    SD_UNLOCK(self.operationsLock);
    
    //返回一個(gè)包含operatin的管理實(shí)例,用于SDWebImageManager取消下載時(shí)使用
    SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
    token.url = url;
    token.request = operation.request;
    token.downloadOperationCancelToken = downloadOperationCancelToken;
    
    return token;
}

其中創(chuàng)建SDWebImageDownloaderOperation的方法createDownloaderOperationWithUrl:options:context:大體為:

  1. 創(chuàng)建NSURLRequest,配置請(qǐng)求頭allHTTPHeaderFields、緩存策略cachePolicy、cookis、請(qǐng)求管線等
  2. 創(chuàng)建SDWebImageDownloaderOperation
- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
                                                                                  options:(SDWebImageDownloaderOptions)options
                                                                                  context:(nullable SDWebImageContext *)context {

    // 1.創(chuàng)建NSURLRequest,進(jìn)行配置
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
    //配置NSURLRequest
    ...

    // 2.創(chuàng)建SDWebImageDownloaderOperation
    NSOperation<SDWebImageDownloaderOperation> *operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request inSession:self.session options:options context:context];
    //配置SDWebImageDownloaderOperation
    ...

    return operation;
}

上面創(chuàng)建的SDWebImageDownloadOperation(繼承自NSOperation)添加到線程隊(duì)列NSOperationQueue后開(kāi)始執(zhí)行對(duì)應(yīng)的系統(tǒng)方法-start,SDWebImageDownloadOperation重載了-start實(shí)現(xiàn):

SDWebImageDownloadOperation:

初始化包括:

  • 線程下載隊(duì)列NSOperationQueue,最大并發(fā)數(shù)1,用于異步解碼圖片數(shù)據(jù)
  • Bool值存儲(chǔ)執(zhí)行狀態(tài)、圖片下載完成狀態(tài)
    等等

創(chuàng)建 NSURLSessionDataTask,開(kāi)始執(zhí)行圖片下載

- (void)start {
    
    // 1、線程保護(hù),創(chuàng)建NSURLSessionDataTask
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

        NSURLSession *session = self.unownedSession;
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }

    // 2、開(kāi)始執(zhí)行下載任務(wù)
    [self.dataTask resume];
    
    // 3、執(zhí)行對(duì)應(yīng)的 progressBlock 回調(diào)操作
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
    }
}

圖片下載過(guò)程調(diào)用NSURLSession的代理方法,一般情況下代理對(duì)象為SDWebImageDownloader,進(jìn)而在持有的線程隊(duì)列downloadQueue(NSOperationQueue)中查找相應(yīng)NSURLDataTask的SDWebImageDownloaderOperation,交由其處理。如果代理對(duì)象SDWebImageDownloaderOperation,直接調(diào)用相應(yīng)代理實(shí)現(xiàn):

以下載完成的代理執(zhí)行為例,過(guò)程如下,可忽略:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = nil;
    for (NSOperation<SDWebImageDownloaderOperation> *operation in self.downloadQueue.operations) {
        if ([operation respondsToSelector:@selector(dataTask)]) {
            // 線程安全保護(hù)
            NSURLSessionTask *operationTask;
            @synchronized (operation) {
                operationTask = operation.dataTask;
            }
            if (operationTask.taskIdentifier == task.taskIdentifier) {
                dataOperation = operation;
                break;
            }
        }
    }
    if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
        [dataOperation URLSession:session task:task didCompleteWithError:error];
    }
}

圖片下載完成后,在線程隊(duì)列中異步解碼圖片

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    // 發(fā)送通知圖片下載完成
    ...
    
    // 圖片解碼
    if (!error) {
        NSData *imageData = [self.imageData copy];
        if (imageData) {
           //  異步解碼圖片數(shù)據(jù)
            [self.coderQueue cancelAllOperations];
            [self.coderQueue addOperationWithBlock:^{
                //圖片解碼
                UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);

                //回調(diào)操作block,結(jié)束任務(wù)、清除sessiond索引、清除回調(diào)block索引
                [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
                [self done];
            }];
        }
    }
}

圖片解碼

入口:SDImageLoader.h、SDImageCacheDefine.h
提供了圖片二進(jìn)制數(shù)據(jù)解碼為圖片的方法

SDImageLoader:

UIImage SDImageLoaderDecodeImageData(NSData *imageData, NSURL * imageURL, SDWebImageOptions options, SDWebImageContext * context) 

SDImageCacheDefine:

UIImage * SDImageCacheDecodeImageData(NSData * imageData, NSString * cacheKey, SDWebImageOptions options, SDWebImageContext * context);

兩者的實(shí)現(xiàn)基本上一致,區(qū)別只是SDImageLoader需要將imageURL轉(zhuǎn)化為圖片對(duì)應(yīng)的標(biāo)識(shí)cachekey。
它們都只是提供了圖片解碼的入口,實(shí)現(xiàn)過(guò)程分為兩步:
1.二進(jìn)制數(shù)據(jù)解碼為圖片:具體的實(shí)現(xiàn)交由 SDImageCodersManager管理的解碼器實(shí)現(xiàn)

  1. 圖片解碼為位圖:由 SDImageCoderHelper實(shí)現(xiàn)
UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
    NSCParameterAssert(imageData);
    NSCParameterAssert(imageURL);
    
    UIImage *image;
    NSString *cacheKey = imageURL.absoluteString;
    BOOL decodeFirstFrame = NO;
    CGFloat scale = 1;
    BOOL shouldScaleDown = NO;
    
    SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
    mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
    mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
    mutableCoderOptions[SDImageCoderWebImageContext] = context;
    SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
    
    // Grab the image coder
    id<SDImageCoder> imageCoder = [SDImageCodersManager sharedManager];
    
    if (!decodeFirstFrame) {
        // check whether we should use `SDAnimatedImage`
        Class animatedImageClass = context[SDWebImageContextAnimatedImageClass];
        if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) {
            image = [[animatedImageClass alloc] initWithData:imageData scale:scale options:coderOptions];
            if (image) {
                // Preload frames if supported
                if (options & SDWebImagePreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) {
                    [((id<SDAnimatedImage>)image) preloadAllFrames];
                }
            } else {
                // Check image class matching
                if (options & SDWebImageMatchAnimatedImageClass) {
                    return nil;
                }
            }
        }
    }
    
    // 交由 SDImageCodersManager 進(jìn)行圖片解碼
    if (!image) {
        image = [imageCoder decodedImageWithData:imageData options:coderOptions];
    }
    
    //是否需要解碼為位圖
    if (image) {
        
        BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
        if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
            shouldDecode = NO;
        } else if (image.sd_isAnimated) {
            shouldDecode = NO;
        }
        if (shouldDecode) {
            //轉(zhuǎn)化為位圖
            image = [SDImageCoderHelper decodedImageWithImage:image];
        }
    }
    
    return image;
}

管理:SDImageCodersManager
目前該管理默認(rèn)提供三種解碼器進(jìn)行二進(jìn)制數(shù)據(jù)解碼,也可以自定義解碼器添加到該類中:

  • SDImageIOCoder:其他圖片類型
  • SDImageGIFCoder(繼承自SDImageIOAnimatedCoder):解碼gif圖片
  • SDImageAPNGCoder(繼承自SDImageIOAnimatedCoder):解碼png圖片

當(dāng)調(diào)用SDImageCodersManager進(jìn)行解碼時(shí),根據(jù)圖片類型,選用對(duì)應(yīng)的解碼器

圖片類型

通過(guò)圖片數(shù)據(jù)的第一字節(jié)、數(shù)據(jù)大小以及特定標(biāo)識(shí)符判斷圖片類型:

+ (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 getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return SDImageFormatJPEG;
        case 0x89:
            return SDImageFormatPNG;
        case 0x47:
            return SDImageFormatGIF;
        case 0x49:
        case 0x4D:
            return SDImageFormatTIFF;
        case 0x52: {
            if (data.length >= 12) {
                //RIFF....WEBP
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
                if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                    return SDImageFormatWebP;
                }
            }
            break;
        }
        case 0x00: {
            if (data.length >= 12) {
                //....ftypheic ....ftypheix ....ftyphevc ....ftyphevx
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding];
                if ([testString isEqualToString:@"ftypheic"]
                    || [testString isEqualToString:@"ftypheix"]
                    || [testString isEqualToString:@"ftyphevc"]
                    || [testString isEqualToString:@"ftyphevx"]) {
                    return SDImageFormatHEIC;
                }
                //....ftypmif1 ....ftypmsf1
                if ([testString isEqualToString:@"ftypmif1"] || [testString isEqualToString:@"ftypmsf1"]) {
                    return SDImageFormatHEIF;
                }
            }
            break;
        }
        case 0x25: {
            if (data.length >= 4) {
                //%PDF
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(1, 3)] encoding:NSASCIIStringEncoding];
                if ([testString isEqualToString:@"PDF"]) {
                    return SDImageFormatPDF;
                }
            }
        }
        case 0x3C: {
            if (data.length > 100) {
                // Check end with SVG tag
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(data.length - 100, 100)] encoding:NSASCIIStringEncoding];
                if ([testString containsString:kSVGTagEnd]) {
                    return SDImageFormatSVG;
                }
            }
        }
    }
    return SDImageFormatUndefined;
}
二進(jìn)制圖片數(shù)據(jù)解碼為圖片

二進(jìn)制圖片解碼最終實(shí)現(xiàn)都是調(diào)用 SDImageIOAnimatedCoder 內(nèi)部實(shí)現(xiàn)方法:
解碼圖片第一幀,或者全部幀數(shù)

- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
    if (!data) { return nil; }
    CGFloat scale = 1;
    CGSize thumbnailSize = CGSizeZero;
    BOOL preserveAspectRatio = YES;

    //創(chuàng)建圖片數(shù)據(jù)源
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    if (!source) { return nil; }
    size_t count = CGImageSourceGetCount(source);
    UIImage *animatedImage;
    
    BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
    
    if (decodeFirstFrame || count <= 1) {
        //解碼圖片第一幀
        animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
    } else {
        
        //解碼圖片,遍歷圖片所有幀數(shù),并合并動(dòng)態(tài)圖
        NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
        
        for (size_t i = 0; i < count; i++) {
            UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
            if (!image) { continue; }
            NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];
            SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
            [frames addObject:frame];
        }
        
        NSUInteger loopCount = [self.class imageLoopCountWithSource:source];
        
        animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
        animatedImage.sd_imageLoopCount = loopCount;
    }
    animatedImage.sd_imageFormat = self.class.imageFormat;
    CFRelease(source);
    
    return animatedImage;
}

創(chuàng)建圖片源的某一幀的圖片

+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize options:(NSDictionary *)options {
    // Some options need to pass to `CGImageSourceCopyPropertiesAtIndex` before `CGImageSourceCreateImageAtIndex`, or ImageIO will ignore them because they parse once :)
    // Parse the image properties
    NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
    NSUInteger pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] unsignedIntegerValue];
    NSUInteger pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] unsignedIntegerValue];
    CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue];
    if (!exifOrientation) {
        exifOrientation = kCGImagePropertyOrientationUp;
    }
    
    NSMutableDictionary *decodingOptions = options ? [NSMutableDictionary dictionaryWithDictionary:options] :  [NSMutableDictionary dictionary];
    ...
    //根據(jù)圖片進(jìn)行圖片進(jìn)行尺寸、像素分辨率設(shè)置
    CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
    if (!imageRef) { return nil; }
    UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
    UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:imageOrientation];
    CGImageRelease(imageRef);
    return image;
}

圖片解碼為位圖

為什么要對(duì)UIImage進(jìn)行解碼呢?難道不能直接使用嗎?
當(dāng)UIImage對(duì)象賦值給UIImageView即在屏幕上渲染圖像時(shí),首先需要對(duì)其進(jìn)行解碼,但這是由Core Animation在主隊(duì)列上發(fā)生的。當(dāng)在主線程調(diào)用了大量的圖片賦值后,就會(huì)產(chǎn)生卡頓了。

為了解決這個(gè)問(wèn)題SDWebImage將圖片解碼為位圖的操作放在異步線程實(shí)現(xiàn),雖然需要消耗額外內(nèi)存,但不會(huì)阻塞主線程。

簡(jiǎn)單解碼圖片:

  1. 已經(jīng)解碼、動(dòng)態(tài)圖、矢量圖都不需要解碼
  2. 創(chuàng)建位圖畫(huà)布
  3. 將圖片數(shù)據(jù)繪制到畫(huà)布上
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    
    //已經(jīng)解碼、動(dòng)態(tài)圖、矢量圖都不需要解碼
    if (image == nil || image.sd_isDecoded || image.sd_isAnimated || image.sd_isVector) {
        return image;
    }
    
    //解碼圖片為位圖
    CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
    if (!imageRef) { return image; }

    //生成uiimage,并返回圖片
    UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(imageRef);
    SDImageCopyAssociatedObject(image, decodedImage);
    decodedImage.sd_isDecoded = YES;
    return decodedImage;
}

+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
    return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp];
}

+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
    if (!cgImage) {
        return NULL;
    }
    
    //根據(jù)需要的圖片方向,設(shè)置圖片寬搞
    size_t width = CGImageGetWidth(cgImage);
    size_t height = CGImageGetHeight(cgImage);
    if (width == 0 || height == 0) return NULL;
    size_t newWidth;
    size_t newHeight;
    switch (orientation) {
        case kCGImagePropertyOrientationLeft:
        case kCGImagePropertyOrientationLeftMirrored:
        case kCGImagePropertyOrientationRight:
        case kCGImagePropertyOrientationRightMirrored: {
            // These orientation should swap width & height
            newWidth = height;
            newHeight = width;
        }
            break;
        default: {
            newWidth = width;
            newHeight = height;
        }
            break;
    }
    
    //創(chuàng)建沒(méi)有透明因素的位圖畫(huà)布上下文
    CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
    BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                      alphaInfo == kCGImageAlphaNoneSkipFirst ||
                      alphaInfo == kCGImageAlphaNoneSkipLast);

    CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
    bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
    
    //創(chuàng)建位圖繪制上下文
    CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
    if (!context) {
        return NULL;
    }
    
    // 根據(jù)方向,對(duì)位圖進(jìn)行調(diào)整
    CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
    CGContextConcatCTM(context, transform);
    
    //將圖片數(shù)據(jù)繪制到畫(huà)布上
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
    //生成位圖
    CGImageRef newImageRef = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    
    return newImageRef;
}

解碼圖片,支持壓縮圖片(默認(rèn)圖片最大不得超過(guò)60M)。

原理: 首先定義一個(gè)大小固定的方塊,然后把原圖按照方塊的大小進(jìn)行分割,最后把每個(gè)方塊中的數(shù)據(jù)畫(huà)到目標(biāo)畫(huà)布上,這樣就能得到目標(biāo)圖像了。接下來(lái)我們做出相信的解釋。

有幾個(gè)點(diǎn)需要清楚:

  • 圖像在iOS設(shè)備上是以像素為單位顯示的
  • 每一個(gè)像素由RGBA組成,即R(紅色)G(綠色)B(藍(lán)色)A(透明度)4個(gè)模塊,每個(gè)模塊由8bit組成(16進(jìn)制00~FF),所以每個(gè)像素大小 = 8bit * 4 = 32bit = 4字節(jié)
+ (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes;
  1. 檢查圖像應(yīng)不應(yīng)該壓縮,原則是:如果圖像大于目標(biāo)尺寸才需要壓縮,否則調(diào)用 -decodedImageWithImage返回位圖
    if (![self shouldDecodeImage:image]) {
        return image;
    }
    
    if (![self shouldScaleDownImage:image limitBytes:bytes]) {
        return [self decodedImageWithImage:image];
    }

+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image limitBytes:(NSUInteger)bytes {
    BOOL shouldScaleDown = YES;
    
    CGImageRef sourceImageRef = image.CGImage;
    CGSize sourceResolution = CGSizeZero;
    sourceResolution.width = CGImageGetWidth(sourceImageRef);
    sourceResolution.height = CGImageGetHeight(sourceImageRef);
    
    //圖片的像素總數(shù) = 長(zhǎng) * 寬
    float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
    if (sourceTotalPixels <= 0) { return NO; }
    
    //壓縮的圖片像素大小
    CGFloat destTotalPixels = bytes = 0 ? bytes : kDestImageLimitBytes;
    destTotalPixels = bytes / kBytesPerPixel;
    if (destTotalPixels <= kPixelsPerMB) { return NO; }
    
    //圖片的像素總數(shù) vs 壓縮的圖片像素大小
    float imageScale = destTotalPixels / sourceTotalPixels;
    if (imageScale < 1) {
        shouldScaleDown = YES;
    } else {
        shouldScaleDown = NO;
    }
    
    return shouldScaleDown;
}

2.目標(biāo)像素 destResolution

    CGFloat destTotalPixels;
    if (bytes == 0) {
        bytes = kDestImageLimitBytes;
    }
    destTotalPixels = bytes / kBytesPerPixel;

其中 kBytesPerPixel為常量:每M的字節(jié)數(shù)

static const CGFloat kBytesPerMB = 1024.0f * 1024.0f
static CGFloat kDestImageLimitBytes = 60.f * kBytesPerMB;
  1. 原圖分辨率sourceResolution、原圖總像素 sourceResolution
CGImageRef sourceImageRef = image.CGImage;
        
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;

4.計(jì)算圖壓縮比imageScale、目標(biāo)圖標(biāo)分辨率destResolution

CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width * imageScale);
destResolution.height = (int)(sourceResolution.height * imageScale);

5.計(jì)算第一個(gè)原圖方塊 sourceTile,這個(gè)方塊的寬度同原圖一樣,高度根據(jù)方塊容量計(jì)算
容量 = 目標(biāo)總像素 / 3

CGFloat tileTotalPixels = destTotalPixels / 3;

CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
sourceTile.size.height = (int)(tileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;

6.計(jì)算目標(biāo)圖像方塊 destTile

CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;

7.計(jì)算原圖像塊與目標(biāo)方塊重疊的像素大小 sourceSeemOverlap

// 重疊的像素?cái)?shù)量
static const CGFloat kDestSeemOverlap = 2.0f;

float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);

8.計(jì)算原圖像需要被分割成多少個(gè)方塊 iterations:
原圖分辨率高度 ? 原圖塊高度,無(wú)法整除,加1處理

int iterations = (int)( sourceResolution.height / sourceTile.size.height );
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) { iterations++; }

9.創(chuàng)建位圖上下文 destContext,并設(shè)置圖形上下文的插值質(zhì)量級(jí)別

destContext = CGBitmapContextCreate(NULL,
                                    destResolution.width,
                                    destResolution.height,
                                    kBitsPerComponent,
                                    0,
                                    colorspaceRef,
                                    bitmapInfo);

if (destContext == NULL) {
    return image;
}
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);

10.根據(jù)重疊像素計(jì)算原圖方塊的大小后,獲取原圖中該方塊內(nèi)的數(shù)據(jù),把該數(shù)據(jù)寫(xiě)入到相對(duì)應(yīng)的目標(biāo)方塊中

float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
for( int y = 0; y < iterations; ++y ) {
    @autoreleasepool {
        sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
        destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
        sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
        if( y == iterations - 1 && remainder ) {
            float dify = destTile.size.height;
            destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
            dify -= destTile.size.height;
            destTile.origin.y += dify;
        }
        CGContextDrawImage( destContext, destTile, sourceTileImageRef );
        CGImageRelease( sourceTileImageRef );
    }
}

11.返回目標(biāo)圖像

CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
if (destImageRef == NULL) { return image; }
UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if (destImage == nil) { return image; }
SDImageCopyAssociatedObject(image, destImage);
destImage.sd_isDecoded = YES;
return destImage;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。