SDWebImage源码学习

SDWebImage 这个框架,相信大家在开发过程中肯定都是使用过的,这篇文章就对自己在看源码过程中的一些记录。文章会根据设置一张图片所经历的 API 调用顺序来一步步往下走,旨在了解它的整个运行过程。

前言

SDWebImage 在功能实现上使用了面向协议的思想,通过协议 SDImageCache、SDImageLoader 来管理图片的加载,SDImageTransformer 来管理图片的变化,SDImageCoder 来处理图片的编解码。这样可以让上层无需关心下层的实现,只要其实现对应的协议,则可认为它可以提供完整的功能实现。这样也极大的增加了框架的灵活性,是功能组件可插拔、方便组装。

在 UI 的组件上,使用分类为控件增加设置图片的方法,让使用者只需要一行简单的代码,即可实现设置。缓存、解码等事则会由框架解决。

记录基于 SDWebImage 5.7.3。 记录过程中,也忽略了一些针对 Mac 的配置。

View Category

我们最常使用的 **- (void)sd_setImageWithURL:(nullable NSURL )url placeholderImage:(nullable UIImage )placeholder; 方法就定义在 UIImageView+WebCache 分类中。

这里只以 UIImageView+WebCache 为入口。 UIButton+WebCache 多了 ControlState 的设置,UIImageView+HighlightedWebCache 则是 UIImageView 的高亮状态的设置,原理都差不多。

所有控件设置图片的方法,最终都会来到 UIView+WebCache 分类下:

1
2
3
4
5
6
7
- (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. 利用 copy 将 SDWebImageContext 复制并转换为不可变类型。 validOperationKey 值作为校验 id,默认值为当前 view 的类名。

  2. sd_cancelImageLoadOperationWithKey:取消上一次任务,保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突。保证当前的控件上有且只有一个最新的任务。

    • 根据传入的 context(字典) 找到当前 validOperationKey,一般 context 为 nil,会自动创建。然后会将当前实例的类名作为 validOperationKey。
    • 在 UIView+WebCacheOperation 分类中,设置了一个关联属性 SDOperationsDictionary。它会存储当前实例的所有 operation 操作。
    • 在实例开始真正的图片请求操作之前,会根据 validOperationKey 获取 operation 操作,如果之前有操作存在,则会取消之前的操作,保证当前实例执行的是最新的 operation。
  3. 设置占位图。

  4. 重置 NSProgress、 设置 SDWebImageIndicator,并判断是否开启。

  5. 初始化 SDWebImageManager 、SDImageLoaderProgressBlock。

  6. 利用 SDWebImageManager 开启下载 loadImageWithURL: 并将返回的 SDWebImageOperation 存入 sd_operationDictionary,key 为 validOperationKey。

  7. 取到图片后,停止 indicator。调用 sd_setImage: 同时为新的 image 添加 Transition 过渡动画。

说明

SDOperationsDictionary 是一个 strong——weak 的 NSMapTable,对 operation 拥有一个弱引用,方便 cancel。其强引用由 SDWebImageManager 的 runningOperations 保持。

1
2
3
4
5
typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;

[[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory
valueOptions:NSPointerFunctionsWeakMemory
capacity:0];

SDImageManager

属性介绍

SDImageManager 是整个框架的中心,所有的处理逻辑都在这里面进行组装、分发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@property (nonatomic, class, readonly, nonnull) SDWebImageManager *sharedManager;

@property (weak, nonatomic, nullable) id <SDWebImageManagerDelegate> delegate;

@property (strong, nonatomic, readonly, nonnull) id<SDImageCache> imageCache;

@property (strong, nonatomic, readonly, nonnull) id<SDImageLoader> imageLoader;

@property (strong, nonatomic, nullable) id<SDImageTransformer> transformer;

@property (nonatomic, strong, nullable) id<SDWebImageCacheKeyFilter> cacheKeyFilter;

@property (nonatomic, strong, nullable) id<SDWebImageCacheSerializer> cacheSerializer;

@property (nonatomic, strong, nullable) id<SDWebImageOptionsProcessor> optionsProcessor;

@property (nonatomic, assign, readonly, getter=isRunning) BOOL running;

@property (nonatomic, class, nullable) id<SDImageCache> defaultImageCache;

@property (nonatomic, class, nullable) id<SDImageLoader> defaultImageLoader;
  • delegate
1
2
3
4
5
6
7
8
9
/**
判断当前 url 是否需要下载。默认为 true。
*/
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nonnull NSURL *)imageURL;

/**
当下载失败之后,如果实现了这个代理,则将失败的 url 处理逻辑交给代理处理。
*/
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldBlockFailedURL:(nonnull NSURL *)imageURL withError:(nonnull NSError *)error;
  • imageCache

缓存处理。

  • imageLoader

图片下载器。

  • transformer

用于在图像加载完成后进行图像变换,并将变换后的图像存储到缓存中。

  • cacheKeyFilter

默认情况下,是把 URL.absoluteString 作为 cacheKey ,而如果设置了 fileter 则会对通过 cacheKeyForURL: 对 cacheKey 拦截并进行修改。

  • cacheSerializer

默认情况下,ImageCache 会直接将 downloadData 进行缓存,而当我们使用其他图片格式进行传输时,例如 WEBP 格式的,那么磁盘中的存储则会按 WEBP 格式来。这会产生一个问题,每次当我们需要从磁盘读取 image 时都需要进行重复的解码操作。而通过 CacheSerializer 可以直接将 downloadData 转换为 JPEG/PNG 的格式的 NSData 缓存,从而提高访问效率。

  • optionsProcessor

用于全局控制当前管理器的 SDWebImageOptions 和 SDWebImageContext 中的参数。

  • running

标识当前 manager 是否有 operation 正在运行。内部维护了 runningOperations 集合,当数量大于 0 时,说明有操作在执行。

  • defaultImageCache

默认使用 SDImageCache.sharedImageCache。

  • defaultImageLoader

默认使用 SDWebImageDownloader.sharedDownloader。

主要方法

入口

通过上层 Category 的封装之后,最终图片的加载逻辑会来到 SDWebImageManager 的这个方法:

1
2
3
4
5
- (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock;

预备知识

1
2
3
4
@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
@property (strong, nonatomic, nonnull) dispatch_semaphore_t failedURLsLock; // a lock to keep the access to `failedURLs` thread-safe
@property (strong, nonatomic, nonnull) NSMutableSet<SDWebImageCombinedOperation *> *runningOperations;
@property (strong, nonatomic, nonnull) dispatch_semaphore_t runningOperationsLock; // a lock to keep the access to `runningOperations` thread-safe

这四个是在 SDWebImageManager 的 .m 文件中的 Extension 中声明的。

  1. failedURLs: 保存了失败的请求 url。

  2. runningOperations:会将在上面的方法中会生成的一个 SDWebImageCombinedOperation 实例,保存在集合中。图片加载存在两种情况,一种是直接在缓存中获取,一种是通过网络在下载,都会返回一个 NSOperation 对象,所以 SDWebImageCombinedOperation 实例中有两个属性与之一一对应,方便对两种加载图片的方式进行管理。

  3. 利用信号量 dispatch_semaphore_t 防止多线程竞争。

方法的执行的流程:

  1. url 合法性判断。因为,这里的 url 是 nullable 的。如果是 NSString 还会将其转换为 NSURL。

  2. 生成 SDWebImageCombinedOperation 实例对象。

  3. failedURLs 集合查询。

    • 若命中,且 options 不为 SDWebImageRetryFailed,则直接返回 operation 并 return。
    • 若未命中,或者 options 为 SDWebImageRetryFailed。则将 operation 存入 runningOperations。
  4. 将 options 和 imageContext 封装为 SDWebImageOptionsResult。

  5. 开始缓存查询。

缓存查询

1
2
3
4
5
6
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;

方法的执行的流程:

  1. 确定用于查找缓存的实例对象。默认的 [SDImageCache sharedImageCache] 还是由 context 传入 SDWebImageContextImageCache。

  2. 根据 options 参数确定是否需要查找缓存。 SDWebImageFromLoaderOnly

  3. 根据 context 参数 SDWebImageContextQueryCacheType 确定缓存查找的范围。默认为 SDImageCacheTypeAll。

  4. 需要查找缓存。

    • 根据 url 确定最终查找时使用的 key 值。可能由 cacheKeyFilter 进行变换。开始查找缓存。
    • 缓存查询结束后。
      • 判断 operation 是否被 cancel。如果是返回错误并结束。
      • operation 正常,进入下载。
  5. 不需要查找缓存,直接进入下载。

内存缓存 SDMemoryCache

  1. 继承自 NSCache 实现内存缓存。通过双向链表及字典实现 LRU 的缓存策略。内存清理策略:对象数量 count、对象大小 cost 。
  2. 维护了一个 NSMapTable 类型的 weakCache(strong-weak)又存储了一份缓存。

外部传入一个需要缓存的对象时,其引用计数为 1,SDMemoryCache 对其进行缓存时,会强引用被缓存的对象,使它的引用计数变为 2。此时,若 SDMemoryCache 清理了缓存,被缓存对象的引用计数减一,但是它还在内存中,但是,从 SDMemoryCache 中已经取不到这个对象了。为了解决这个问题,SDMemoryCache 在继承自 NSCache 的基础上,维护了一个 NSMapTable 属性 weakCache(stong-weak cache),它会弱引用被缓存对象,当缓存被清理之后,我们还可以在 weakCache 中获取到被缓存对象,就算对象被释放,因为弱引用也不会造成野指针问题。这是典型的 “空间换时间” 的思想。当然,针对 weakCache 的读写安全,也使用了 weakCacheLock (dispatch_semaphore_t)线程锁。

磁盘缓存

  1. 当内存中未命中缓存,则在一个串行队列 ioQueue 中同步或者异步地执行磁盘查询。
1
2
3
4
5
6
// 串行队列
_ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL);

// 判断是同步查询还是异步查询
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
  • 因为磁盘缓存读取时,会产生许多临时变量,为了避免内存过高,使用 @autoreleasepool 包裹磁盘读取的代码。
  • 只有当从磁盘取到缓存时,才会对图片进行解码。
  • 利用这个全局声明的变量 SDImageCacheDecodeImageData,进行了图片解码的处理。
    • 在磁盘中根据 filePath 取出 imageData。
    • 利用 CGImageSourceCreateWithData 将 imageData 转换为 image。
    • 利用 SDImageCoderHelper 将 image 强制解码并返回解码后的图片。
  • 将解码后的图片缓存到内存缓存中,然后通过 block 回调到 SDWebImageManager。
1
2
3
4
UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, 
NSString * _Nonnull cacheKey,
SDWebImageOptions options,
SDWebImageContext * _Nullable context);

下载数据

1
2
3
4
5
6
7
8
9
- (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;

方法的执行的流程:

  1. 确定用于下载的实例对象。默认的 [SDWebImageDownloader sharedDownloader] 还是 由 context 传入 SDWebImageContextImageLoader。

  2. 检查是否需要开启下载。

1
2
3
4
BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
shouldDownload &= [imageLoader canRequestImageForURL:url];
  • 检查 options 值是否为 SDWebImageFromCacheOnly 或 SDWebImageRefreshCached。
  • 由代理决定是否需要新建下载任务。
  • 通过 imageLoader 控制能否支持下载任务。
  1. 如果 shouldDownload 为 NO,则结束下载并调用 callCompletionBlockForOperation 与 safelyRemoveOperationFromRunning。此时如果存在 cacheImage 则会随 completionBlock 一起返回。

  2. 如果 shouldDownload 为 YES,新建下载任务并将其保存在 combineOperation 的 loaderOperation。在新建任务前,如有取到 cacheImage 且 SDWebImageRefreshCached 为 YES,会将其存入 imageContext (没有则创建 imageContext)。

    • SDWebImageDownloader 中,维护了一个 NSOperationQueue 实例 _downloadQueue,默认的最大并发数为 6。还维护了可变字典 _URLOperations,key 为下载 url,value 为下载的 NSOperation 实例。
      • _downloadQueue 中利用 NSOperationQueue 的 addDependency 方法,使原队列中 operations 依赖于最新加入的 operation。实现了一个 LILO (后进先出) 的操作队列。
    • 在 _URLOperations 中,根据下载 url 获取 operation。
      • 如果 (operation == nil || operation.isFinished || operation.isCancelled) 则会创建一个新的 operation。 利用 @synchronized 为 operation 添加 block 回调(progressBlock, completedBlock),然后,将 operation 加入到 _URLOperations 字典中。
      • 否则,重用之前的 operation,利用 @synchronized 为 operation 添加 block 回调(progressBlock, completedBlock),并设置当前 operation 的操作优先级。
    • 根据获取到的 operation 生成 SDWebImageDownloadToken 实例并返回。在 SDWebImageDownloaderOperation 的完成回调中,可以看到也使用了 SDImageLoaderDecodeImageData 对图片进行了子线程强制解码并将解码后的 image 返回。
1
2
3
4
UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, 
NSURL * _Nonnull imageURL,
SDWebImageOptions options,
SDWebImageContext * _Nullable context);
  1. 下载结束后回到 callBack,这里会先处理几种情况:

    • operation 被 cancel 则抛弃下载的 image、data ,callCompletionBlock 结束下载。
    • reqeust 被 cancel 导致的 error,callCompletionBlock 结束下载。
    • imageRefresh 后请求结果仍旧命中了 NSURLCache 缓存,则不会调用 callCompletionBlock。
    • errro 出错,callCompletionBlockForOperation 并将 url 添加至 failedURLs。
    • 均无以上情况,如果是通过 retry 成功的,会先将 url 从 failedURLs 中移除,调用 storeCacheProcess。
    • 最后会对标记为 finished。执行 safelyRemoveOperation。

缓存数据

1
2
3
4
5
6
7
8
9
- (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;

方法的执行的流程:

  1. 先从 imageContext 中取出 storeCacheType、originalStoreCacheType、transformer、cacheSerializer,判断是否需要存储转换后图像数据、原始数据、等待缓存存储结束。
  2. 检查是否需要缓存原始数据 shouldCacheOriginal。
    • shouldCacheOriginal = YES:先确认存储类型是否为原始数据,存储时如果 cacheSerializer 存在则会先转换数据格式,最终都调用 [self stroageImage:] 将数据存入缓存,并进入 image transformer。
    • shouldCacheOriginal = NO:直接进入 image transformer。

图片变换

1
2
3
4
5
6
7
8
9
- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
originalImage:(nullable UIImage *)originalImage
originalData:(nullable NSData *)originalData
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;

方法的执行的流程:

  1. 先判断是否需要变换。
1
2
3
BOOL shouldTransformImage = originalImage && transformer;
shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage));
  1. 需要转换的话。转换成功后,会依据 cacheData = [cacheSerializer cacheDataWithImage: originalData: imageURL:]; 进行 [self storageImage:]存储图片。存储结束后 callCompletionBlock。
1
2
3
4
5
6
7
8
9
10
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
if (transformedImage && finished) {
// 存入缓存
} else {
// 回调结束 block
}
}
});
  1. 无需转换直接回调结束的 callCompletionBlock。

SDImageCoder

图片解码会在两种情况下发生:

  • 从磁盘缓存取到缓存的图片数据 —— SDImageCacheDecodeImageData

会在一个串行队列中,根据 options 配置同步或异步去读取磁盘缓存,读取到缓存数据之后,进行解码操作。

1
2
3
4
5
// SDImageCache.m

@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue;

_ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL);
  • 下载时
    1. 边下载边解码的渐进式解码 —— SDImageLoaderDecodeProgressiveImageData
    2. 下载完成之后的普通解码 —— SDImageLoaderDecodeImageData

会在一个串行队列异步执行解码操作,优先级则会根据 options 配置来设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 串行队列
_coderQueue = [NSOperationQueue new];
_coderQueue.maxConcurrentOperationCount = 1

// 调整任务优先级
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
} else {
self.dataTask.priority = NSURLSessionTaskPriorityDefault;
self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
}
1
2
3
4
5
6
7
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
// use SDImageLoaderDecodeImageData
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
// use SDImageLoaderDecodeProgressiveImageData
}

这里只对普通解码的过程做了一个讲解,这也是日常开发中用的最多的一种方式。磁盘缓存读取的图片数据解码方式也属于普通解码范围。

下面就以 SDImageLoaderDecodeImageData 做例子进行讲解。

SDImageLoaderDecodeImageData

1. 根据 cacheKeyFilter 处理缓存的 key 值。

1
2
3
4
5
6
7
id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *cacheKey;
if (cacheKeyFilter) {
cacheKey = [cacheKeyFilter cacheKeyForURL:imageURL];
} else {
cacheKey = imageURL.absoluteString;
}

2. 根据 options 设置是否只解码第一帧图片 SDWebImageDecodeFirstFrameOnly。

1
BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);

3. 屏幕倍数因子确定。

  1. context 中取 context[SDWebImageContextImageScaleFactor]。取到大于 1,则使用,否则下一步。
  2. SDImageScaleFactorForKey(cacheKey)
    • 如果 key 是普通名字,判断 key 包不包含”@2x.”或者”@3x.”,包含就返回这个倍数因子。
    • 如果 key 是一个 URL,百分号编码的下的 @ = %40,判断 key 包不包含”%402x.”或者”%403x.”。
1
2
3
NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];

CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);

4. 根据 options 判断是否需要对图片做大小的转换。

1
2
3
4
5
6
7
8
9
10
11
NSValue *thumbnailSizeValue;
BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages);
if (shouldScaleDown) {
// 60M 所拥有的像素数量
CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4;
CGFloat dimension = ceil(sqrt(thumbnailPixels));
thumbnailSizeValue = @(CGSizeMake(dimension, dimension));
}
if (context[SDWebImageContextImageThumbnailPixelSize]) {
thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
}

5. 配置解码参数。SDImageCoderMutableOptions

1
2
3
4
5
6
7
SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
mutableCoderOptions[SDImageCoderWebImageContext] = context;
SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];

6. 解码器确认。

1
2
3
4
5
6
id<SDImageCoder> imageCoder;
if ([context[SDWebImageContextImageCoder] conformsToProtocol:@protocol(SDImageCoder)]) {
imageCoder = context[SDWebImageContextImageCoder];
} else {
imageCoder = [SDImageCodersManager sharedManager];
}

7. 如果不是“仅解码第一帧”(decodeFirstFrame = NO)并且是动图。则装载动图的所有帧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (!decodeFirstFrame) {
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;
}
}
}
}

8. 如果是“仅解码第一帧”(decodeFirstFrame = YES),或者不是动图。则会利用上面的确定的 imageCoder 解码图片。

1
2
3
4
5
/*
正常使用情况下:
imageCoder = [SDImageCodersManager sharedManager];
*/
image = [imageCoder decodedImageWithData:imageData options:coderOptions];
  1. 取出(加锁) _imageCoders, SDImageIOCoder, SDImageGIFCoder, SDImageAPNGCoder;

  2. 反转遍历 _imageCoders ,顺序为 SDImageAPNGCoder -> SDImageGIFCoder -> SDImageIOCoder

  3. 每个 coder 都调用 canDecodeFromData: 方法,判断是否是自己可以解码的格式。

+[NSData(ImageContentType) sd_imageFormatForImageData:] 获取 data 中包含的图片格式。
-[NSData getBytes:length:] 获取的第一个字节可以区分图片格式。

  1. 如果返回值为 YES 则 coder 调用 decodedImageWithData:options: 方法,输出 image。

此处并不是解码的地方,只是将压缩的图片二进制流利用 ImageIO 读取到 UIImage 中。

9. 动图不解码。SDWebImageAvoidDecodeImage 未设置的话,直接解码图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
// MARK : 动图不解码
if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}

if (shouldDecode) {
image = [SDImageCoderHelper decodedImageWithImage:image];
}
  1. +[SDImageCoderHelper shouldDecodeImage:]:已解码过(判断关联属性 sd_isDecoded)的不再解码;图片为nil不解码;动图不解码;

  2. +[SDImageCoderHelper CGImageCreateDecoded:]

  3. +[SDImageCoderHelper CGImageCreateDecoded:orientation:]:输入 image.CGImage,输出解码的 CGImageRef; 核心解码函数:CGContextDrawImage()

    1. 获取图片的宽高,如果方向是左、右旋转则交换宽高数据。

    2. 判断是否含有 alpha 信息。

    3. 获取 32 位字节顺序(kCGBitmapByteOrder32Host 这个宏避免考虑大小端问题),保存到位图信息 bitmapInfo。

    4. bitmapInfo 按位或添加像素格式(alpha 信息)。有 alpha 选择 kCGImageAlphaPremultipliedFirst(BGRA8888), 无 alpha 选择 kCGImageAlphaNoneSkipFirst。

    5. 调用 +[SDImageCoderHelper colorSpaceGetDeviceRGB] 获取颜色空间。

    6. 调用 CGBitmapContextCreate() 函数创建位图上下文。

    7. 调用 SDCGContextTransformFromOrientation() 获取方向旋转的 CGAffineTransform,调用 CGContextConcatCTM 关联到位图上下文,坐标系转换。

    8. 解码:CGContextDrawImage()。

    9. 获取位图:CGBitmapContextCreateImage(),输入 context,输出解码后的 CGImageRef。

    10. 释放上下文context

  4. -[UIImage initWithCGImage:scale:orientation:],CGImage 转为 UIImage。

  5. UIImage 关联属性赋值:decodedImage.sd_isDecoded = YES; decodedImage.sd_imageFormat = image.sd_imageFormat;