一、概述
SDWebImage 是 iOS 开发中比较常用的一个开源库,支持加载网络图片到 UIImageView
/ UIButton
等 UI 控件上。
其使用方式也比较简单,以 UIImageView
加载网络图片为例:
1
| [self.imageView sd_setImageWithURL:self.imageURL placeholderImage:nil];
|
SDWebImage(3.8.3) 核心类可以按照下图进行分层:
各层职责:
UIImageView+WebCache
/ UIButton+WebCache
SDWebImageManager
SDWebImageDownloader
/ SDWebImageDownloaderOperation
SDImageCache
- 负责管理图像的内存缓存和磁盘缓存,提高图像加载的效率。
SDWebImageDecoder
- 将下载的图像数据解码为可以直接使用的 UIImage 对象。
SDWebImageCompat
/ SDWebImagePrefetcher
- 提供一些辅助功能和工具类,增强框架的功能性和灵活性。
SDWebImage 在 GitHub 中给出的时序图如下:
二、源码解读
以 UIImageView
加载网络图片为例:
1
| [self.imageView sd_setImageWithURL:self.imageURL placeholderImage:nil];
|
该方法最终调用的是如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock { [self sd_cancelCurrentImageLoad]; objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (!(options & SDWebImageDelayPlaceholder)) { dispatch_main_async_safe(^{ self.image = placeholder; }); } if (url) {
if ([self showActivityIndicatorView]) { [self addActivityIndicator]; }
__weak __typeof(self) wself = self; id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { [wself removeActivityIndicator]; if (!wself) return; dispatch_main_sync_safe(^{ if (!wself) return; if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) { completedBlock(image, error, cacheType, url); return; } else if (image) { wself.image = image; [wself setNeedsLayout]; } else { if ((options & SDWebImageDelayPlaceholder)) { wself.image = placeholder; [wself setNeedsLayout]; } } if (completedBlock && finished) { completedBlock(image, error, cacheType, url); } }); }]; [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; } else { dispatch_main_async_safe(^{ [self removeActivityIndicator]; if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}]; completedBlock(nil, error, SDImageCacheTypeNone, url); } }); } }
|
该方法主要逻辑如下:
- 取消当前正在进行的图片加载
- 调用
sd_cancelCurrentImageLoad
方法,确保当前没有正在进行的图片加载操作
- 设置占位图
- 图片下载
- 通过
SDWebImageManager
单例,执行图片下载的操作
- 图片下载完成
- 将下载完成的
UIImage
设置到当前 UIImageView
上
2、SDWebImageManager
SDWebImageManager
是个单例,主要负责协调下载、缓存等操作,是图像加载的核心调度层,其中持有了 SDImageCache
和 SDWebImageDownloader
,且两者都是单例:
1 2 3 4 5 6 7 8 9 10
| @interface SDWebImageManager : NSObject
@property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate;
@property (strong, nonatomic, readonly) SDImageCache *imageCache; @property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader;
@end
|
SDWebImageManager
还有个 delegate
属性,SDWebImageManagerDelegate
声明了两个可选实现的方法:
1 2 3 4 5
| - (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
|
前述的图片下载通过 SDWebImageManager
单例中的如下方法实现的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
NSString *key = [self cacheKeyForURL:url];
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { if (image && options & SDWebImageRefreshCached) { dispatch_main_sync_safe(^{ completedBlock(image, nil, cacheType, YES, url); }); }
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { __strong __typeof(weakOperation) strongOperation = weakOperation; if (!strongOperation || strongOperation.isCancelled) {
} else if (error) {
} else { BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) { BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage]; [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk]; }
dispatch_main_sync_safe(^{ if (strongOperation && !strongOperation.isCancelled) { completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url); } }); }); } else { if (downloadedImage && finished) { [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; }
dispatch_main_sync_safe(^{ if (strongOperation && !strongOperation.isCancelled) { completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url); } });
} }
}];
}
}];
return operation; }
|
该方法主要逻辑如下:
- 使用
URL
生成缓存 Key
[url absoluteString]
作为 key
- 使用缓存
Key
从 SDImageCache
中读取缓存
- 如果缓存存在,先将缓存的
UIImage
回调出去
- 判断是否需要下载
- 如果缓存中不存在图片或需要刷新,并且
delegate
允许下载图片,则通过 SDWebImageDownloader
下载,下载完成后更新到缓存并回调
- 如果缓存中存在图片且需要刷新,则先返回缓存中的图片,然后继续通过
SDWebImageDownloader
下载以刷新图片,下载完成后更新到缓存并回调
- 如果缓存中不存在图片且不允许下载,则空回调
3、SDImageCache
(1)缓存的查找
SDImageCache
中,缓存读取方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock { if (!doneBlock) { return nil; }
if (!key) { doneBlock(nil, SDImageCacheTypeNone); return nil; }
UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { doneBlock(image, SDImageCacheTypeMemory); return nil; }
NSOperation *operation = [NSOperation new]; dispatch_async(self.ioQueue, ^{ if (operation.isCancelled) { return; }
@autoreleasepool { UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); [self.memCache setObject:diskImage forKey:key cost:cost]; }
dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); }); } });
return operation; }
|
该方法主要逻辑如下:
- 先从内存中查找缓存
- 内存中如果存在缓存,则回调内存中的缓存
- 内存中不存在缓存,则从磁盘查找缓存
- 内存缓存使用
NSCache
存储
- 可以通过
SDImageCache
单例的 maxMemoryCountLimit
属性设置最大存储图片数量,默认为 0,即无限制。
- 可以通过
SDImageCache
单例的 maxMemoryCost
属性设置最大存储图片内存大小,默认为 0,即无限制,系统内存不足时系统自动清理。
- 再从磁盘中查找缓存
- 异步查找缓存
- 从磁盘查到缓存后,将缓存写入到内存缓存中一份
- 磁盘缓存使用
NSFileManager
实现,缓存到 Library/Caches
中
- 可以通过
SDImageCache
单例的 maxCacheAge
属性设置缓存有效期,默认有效期为 7 天。
- 可以通过
SDImageCache
单例的 maxCacheSize
属性设置最大存储图片内存大小,默认为 0,即无限制。
(2)磁盘缓存的清理
SDImageCache
实例初始化时,注册了三个通知用于缓存的清理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory { if ((self = [super init])) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clearMemory) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cleanDisk) name:UIApplicationWillTerminateNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundCleanDisk) name:UIApplicationDidEnterBackgroundNotification object:nil]; }
return self; }
|
即缓存清理时机如下:
- 内存警告时清理内存缓存
- 应用即将终止时清理磁盘缓存
- 应用进入后台创建后台任务清理缓存
磁盘清理具体逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock { dispatch_async(self.ioQueue, ^{ NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
NSDirectoryEnumerator *fileEnumerator = [self->_fileManager enumeratorAtURL:diskCacheURL includingPropertiesForKeys:resourceKeys options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge]; NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary]; NSUInteger currentCacheSize = 0;
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init]; for (NSURL *fileURL in fileEnumerator) { NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) { continue; }
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { [urlsToDelete addObject:fileURL]; continue; }
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize += [totalAllocatedSize unsignedIntegerValue]; [cacheFiles setObject:resourceValues forKey:fileURL]; } for (NSURL *fileURL in urlsToDelete) { [self->_fileManager removeItemAtURL:fileURL error:nil]; }
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) { const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) { return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]]; }];
for (NSURL *fileURL in sortedFiles) { if ([self->_fileManager removeItemAtURL:fileURL error:nil]) { NSDictionary *resourceValues = cacheFiles[fileURL]; NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
if (currentCacheSize < desiredCacheSize) { break; } } } }
if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(); }); } }); }
|
该方法主要逻辑如下:
- 按有效期
maxCacheAge
清理过期缓存
- 按大存储内存
maxCacheSize
清理缓存
- 按照文件最后修改时间的逆序,移除过早的缓存,仅保留
maxCacheSize
一半大小的缓存
(3)缓存的写入
在 SDWebImageManager
中,图片下载完成后会调用 SDImageCache
的如下方法将图片写入到缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk { if (!image || !key) { return; }
if (self.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(image); [self.memCache setObject:image forKey:key cost:cost]; }
if (toDisk) { dispatch_async(self.ioQueue, ^{ NSData *data = imageData;
if (image && (recalculate || !data)) { #if TARGET_OS_IPHONE int alphaInfo = CGImageGetAlphaInfo(image.CGImage); BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaNoneSkipFirst || alphaInfo == kCGImageAlphaNoneSkipLast); BOOL imageIsPng = hasAlpha;
if ([imageData length] >= [kPNGSignatureData length]) { imageIsPng = ImageDataHasPNGPreffix(imageData); }
if (imageIsPng) { data = UIImagePNGRepresentation(image); } else { data = UIImageJPEGRepresentation(image, (CGFloat)1.0); } #else data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil]; #endif }
[self storeImageDataToDisk:data forKey:key]; }); } }
|
该方法主要逻辑如下:
- 先存到内存缓存
- 再存到磁盘缓存(异步)
- 先检查图片是否是 PNG
- 先通过
CGImageGetAlphaInfo
获取 UIImage
的 Alpha
通道信息,推测是否是 PNG
- PNG 格式支持透明度,因此具有
Alpha
通道的图像通常被认为是 PNG
- 但这并不排除其他格式(如 TIFF 或 WebP)也可能支持透明度
- 如果
imageData
存在,则检查其 NSData
前缀以确定是否为 PNG 格式
- PNG 文件具有独特的签名(前八个字节),通过检查这些字节可以准确判断图像是否为 PNG 格式
- 根据是否是 PNG,决定是通过
UIImagePNGRepresentation
还是 UIImageJPEGRepresentation
将 UIImage
转成 NSData
- 使用
NSFileManager
将上一步得到的 NSData
写入沙盒(Library/Caches
)
4、SDWebImageDownloader/SDWebImageDownloaderOperation
SDWebImageDownloader
和 SDWebImageDownloaderOperation
共同协作来实现图片的下载。
SDWebImageDownloader
是一个负责管理图片下载任务的类。它提供了一个全局的入口来启动、取消和管理图片下载请求。主要职责包括:
- 管理下载队列
SDWebImageDownloader
维护一个下载队列,用于管理所有的下载任务。
- 配置下载选项、
- 处理下载回调
- 创建下载操作
- 为每个下载请求创建一个
SDWebImageDownloaderOperation
实例,并将其添加到下载队列中。
SDWebImageDownloaderOperation
是具体执行图片下载任务的类。它继承自 NSOperation
,因此可以利用 NSOperationQueue
来管理并发下载。主要职责包括:
- 执行下载任务
- 具体的图片下载逻辑在这个类中实现,包括发起网络请求、处理响应数据等。
- 管理下载状态
- 维护下载任务的状态,如正在进行、已完成、已取消等。
- 处理下载回调
- 在下载过程中,通过回调将进度、完成、失败等信息传递给
SDWebImageDownloader
。
SDWebImageManager
中调用了 SDWebImageDownloader
的如下方法进行图片的下载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
| - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { __block SDWebImageDownloaderOperation *operation; __weak __typeof(self)wself = self;
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{ NSTimeInterval timeoutInterval = wself.downloadTimeout; if (timeoutInterval == 0.0) { timeoutInterval = 15.0; }
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); request.HTTPShouldUsePipelining = YES; if (wself.headersFilter) { request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]); } else { request.allHTTPHeaderFields = wself.HTTPHeaders; }
operation = [[wself.operationClass alloc] initWithRequest:request inSession:self.session options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; }); for (NSDictionary *callbacks in callbacksForURL) { dispatch_async(dispatch_get_main_queue(), ^{ SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; if (callback) callback(receivedSize, expectedSize); }); } } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_barrier_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; if (finished) { [sself.URLCallbacks removeObjectForKey:url]; } }); for (NSDictionary *callbacks in callbacksForURL) { SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; if (callback) callback(image, data, error, finished); } } cancelled:^{ SDWebImageDownloader *sself = wself; if (!sself) return; dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url]; }); }]; operation.shouldDecompressImages = wself.shouldDecompressImages;
if (wself.urlCredential) { operation.credential = wself.urlCredential; } else if (wself.username && wself.password) { operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]; }
if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; }
[wself.downloadQueue addOperation:operation]; if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } }];
return operation; }
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback { if (url == nil) { if (completedBlock != nil) { completedBlock(nil, nil, nil, NO); } return; }
dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; }
NSMutableArray *callbacksForURL = self.URLCallbacks[url]; NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; [callbacksForURL addObject:callbacks]; self.URLCallbacks[url] = callbacksForURL;
if (first) { createCallback(); } }); }
|
在该方法中,可以看出是使用 NSURLSession
实现图片的下载。
其中,下载回调是保存在 NSMutableDictionary
中:
key
为 URL
value
是一个数组,数组元素为字典,字典中保存了 progressBlock
、completedBlock
两个回调
针对每一个 URL
,只会在这个 URL
首次下载时才会真正执行下载,其余的只会将回调保存到字典中。在对应阶段,使用 URL
取出保存的回调数组,遍历执行回调,并使用 dispatch_barrier_sync
确保线程安全。
5、SDWebImageDecoder
SDWebImageDecoder
主要负责图片的解码和解压缩,SDWebImageDecoder
中只提供了如下一个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| + (UIImage *)decodedImageWithImage:(UIImage *)image { if (image == nil) { return nil; } @autoreleasepool { if (image.images != nil) { return image; } CGImageRef imageRef = image.CGImage; CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef); BOOL anyAlpha = (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast); if (anyAlpha) { return image; } CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef)); CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef); BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown || imageColorSpaceModel == kCGColorSpaceModelMonochrome || imageColorSpaceModel == kCGColorSpaceModelCMYK || imageColorSpaceModel == kCGColorSpaceModelIndexed); if (unsupportedColorSpace) { colorspaceRef = CGColorSpaceCreateDeviceRGB(); } size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); NSUInteger bytesPerPixel = 4; NSUInteger bytesPerRow = bytesPerPixel * width; NSUInteger bitsPerComponent = 8;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, bitsPerComponent, bytesPerRow, colorspaceRef, kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast); CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context); UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation]; if (unsupportedColorSpace) { CGColorSpaceRelease(colorspaceRef); } CGContextRelease(context); CGImageRelease(imageRefWithoutAlpha); return imageWithoutAlpha; } }
|
decodedImageWithImage:
方法主要是通过 CGBitmapContextCreate
进行图片的解压缩,该方法均是在子线程被调用。
decodedImageWithImage:
方法调用时机如下:
SDWebImageDownloaderOperation
中图片下载完成后
SDImageCache
中从磁盘读取缓存后
decodedImageWithImage:
方法的入参已经是 UIImage
了,为什么还需要对 UIImage
进行解压缩呢?
UIImage
展示到 UIImageView
上,要经过以下流程:
- 将
UIImage
赋值给 UIImageView
- 将
UIImage
对象赋值给 UIImageView
,准备显示图像。
- 图片解码(
CPU
)
- 解码即解压缩,将压缩的图片数据(如 JPEG、PNG)解码为未压缩的位图数据。
- 解码是一个非常耗时的
CPU
操作,默认情况下在主线程中执行。
- 图片绘制(
GPU
)
- 解码后的位图数据提交给
GPU
进行渲染。
GPU
负责将位图数据渲染到帧缓冲区。
- 显示到屏幕(
GPU
)
GPU
渲染完成后,将渲染后的位图放到帧缓冲区。
- 帧缓冲区的内容被视频控制器读取并显示到屏幕上。
而 decodedImageWithImage:
的作用就是进行 UIImage
的解压缩操作,在图片下载完成或从磁盘缓存读取后,先利用 CGBitmapContextCreate
在子线程提前对 UIImage
进行解压缩,这样就避免了主线程的解压缩的操作,加快了图片的显示速度和流畅度,尤其是在快速滑动的列表中。