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
- SDWebImageCompat.h: 定義一些宏定義
- SDWebImageDefine.h:
定義關(guān)于圖片操作的option(SDWebImageOptions ):圖片讀取操作、圖片下載優(yōu)先級(jí)、圖片后臺(tái)下載、圖片下載完成展示操作等
key: -
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;
-
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+WebCache 和 MKAnnotationView+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ò)程
- 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);
}
}
- 重載了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ò)期文件:
- 刪除過(guò)期文件
- 如果磁盤(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:
- 信號(hào)量
dispatch_semaphore_t
開(kāi)啟線程保護(hù) - 以u(píng)rl為key在
查詢哈希表
NSMutableDictionary,查找對(duì)應(yīng)的下載操作NSOperation - 若
哈希表中不存在
相應(yīng)的NSOperation,就創(chuàng)建一個(gè) SDWebImageDownloaderOperation
(繼承自NSOperation)的下載操作 -
緩存 operation 到哈希表
中,下次查詢使用,避免同一個(gè)url下載多次 - 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:大體為:
- 創(chuàng)建NSURLRequest,配置請(qǐng)求頭allHTTPHeaderFields、緩存策略cachePolicy、cookis、請(qǐng)求管線等
- 創(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)
-
圖片解碼為位圖
:由 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)單解碼圖片:
- 已經(jīng)解碼、動(dòng)態(tài)圖、矢量圖都不需要解碼
- 創(chuàng)建位圖畫(huà)布
- 將圖片數(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;
- 檢查圖像應(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;
- 原圖分辨率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;