了解 SDWebImage 框架

2,766 阅读27分钟

1、问题

两个不知道来自哪里的面试题:

  • 1、在 tableViewCell 中使用 SDWebImage 获取图片时,SDWebImage 是怎么实现 cell 划出屏幕外停止对图片的下载的?

答案:3.4 小节 取消加载任务 sd_cancelImageLoadOperationWithKey()

  • 2、有一个 GIF 图的 URLSDAnimatedImageView 是怎么让 GIF 动起来的?

答案:3.11 小节 获取动图每一帧信息 createFrameAtIndex()3.12 小节 SDAnimatedImageView 类的学习

2、SDWebImage 框架结构图

请允许我从 SDWebImage 官网盗两张图 ^_^。

图 1

image.png 图 2

image.png

当我看 SDWebImage 的源码已经眼花缭乱的时候,看了这张让人眼花缭乱的图忽然不那么眼花缭乱了。(ps:朋友说:你搁这套娃呢。我:哈哈哈哈哈哈哈哈。)

好了,进入正题!

为什么要先放两个框架结构图?

原因很简单,首先是要对 SDWebImage 的设计有一定的了解,其次是方便在看源码的时候不会因为多次代码调用跳转导致思路错乱而忘记了代码的调用流程,最后也是为了自己后续回顾框架方便。

2.1 图 1 的说明

图 1 是对整个框架调用过程的一个简述。根据图上方法旁边的编号,分别说明:

  • 1、 Others object(ImageView 对象) 调用了 UIImagView+WebCachesd_setImageWithURL() (UIImagView+WebCache 提供了多个方法使用);

  • 2、因为多个类的分类存在 sd_setImageWithURL 方法 (比如 SDAnimatedImageViewUIImageViewUIButton),但是仅仅是参数不同,所以就再封装 UIView+WebCache,调用其 sd_internalSetImageWithURL() 方法进入网络图片加载流程 ;

  • 3、UIView+WebCache 分类中调用 SDWebImageManagerloadImageWithURL(url,options.progress,completed) 开始请求网络图片;

  • 4、请求图片前调用 SDWebImageCachequeryImageForKey(key,options,context,completionBlock) 方法查找本地缓存;

  • 5、返回 cache 的结果;

  • 6、如果缓存不存在或者超时(默认:7x24x60x60 = 7天),由loadImageWithURL(url,options.progress,completed) 创建遵守 SDWebImageDownloader 协议的下载器对象进行下载;

  • 7、返回下载结果;

  • 8、调用 SDWebImageCachestore(image,imageData,key,toDisk,completionBlock) 固化图片;

  • 9、 返回 image 对象给 UIView+WebCache

  • 10、set imageimageView

2.2 图 2

图 2 的话,有点复杂,一时半会说不完,看完下方源码的分析就明白了。

3、SDWebImage 框架源码

SDWebImage 源码太多了,这里只分析一些代码片段。

3.1 Demo 示例

只加载一个 GIF, 这是 GIF 链接 assets.sbnation.com/assets/2512…

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    // ...
    cell.customImageView.sd_imageTransition = SDWebImageTransition.fadeTransition;
    
    __weak SDAnimatedImageView *imageView = cell.customImageView;
    [imageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]]
                 placeholderImage:placeholderImage
                          options:0
                          context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(180, 120))}
                         progress:nil
                        completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
        SDWebImageCombinedOperation *operation = [imageView sd_imageLoadOperationForKey:imageView.sd_latestOperationKey];
        SDWebImageDownloadToken *token = operation.loaderOperation;
        if (@available(iOS 10.0, *)) {
            NSURLSessionTaskMetrics *metrics = token.metrics;
            if (metrics) {
                printf("Metrics: %s download in (%f) seconds\n", [imageURL.absoluteString cStringUsingEncoding:NSUTF8StringEncoding], metrics.taskInterval.duration);
            }
        }
    }];
    return cell;
}

3.2 sd_setImageWithURL (SDAnimatedImageView+WebCache)

因为 Demo 中使用了 SDAnimatedImageView 所以调用了 SDAnimatedImageView+WebCache 分类。

因为 SDAnimatedImageView 是动画 ImageView, 所以 context 里面多添加了 keySDWebImageContextAnimatedImageClass ,ValueSDAnimatedImage.class 的图像类信息。

SDWebImage 的解释:用于框架内部检查图像类使用,但是大多会走默认值。

- (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 {
    
    // SDAnimatedImageView 是动画 ImageView 字典里面多加了一个 key: SDWebImageContextAnimatedImageClass ,Value: SDAnimatedImage.class
    Class animatedImageClass = [SDAnimatedImage class];
    SDWebImageMutableContext *mutableContext;
    if (context) {
        mutableContext = [context mutableCopy];
    } else {
        mutableContext = [NSMutableDictionary dictionary];
    }
    mutableContext[SDWebImageContextAnimatedImageClass] = animatedImageClass;
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                             context:mutableContext
                       setImageBlock:nil
                            progress:progressBlock
                           completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
                               if (completedBlock) {
                                   completedBlock(image, error, cacheType, imageURL);
                               }
                           }];
}

3.3 sd_internalSetImageWithURL (UIView+SDWebCache)

到这里,SDWebImage 才算正式开始。

- (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 {
    // copy 一下,防止改动
    if (context) {
        // copy to avoid mutable object
        context = [context copy];
    } else {
        context = [NSDictionary dictionary];
    }
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    if (!validOperationKey) {
        // pass through the operation key to downstream, which can used for tracing operation or image view class
        // 将操作键传递到下游,可用于跟踪操作或图像查看类
        
        /**
         * 其实这里的解释很简单
         * 1、如果是一个UIButton+WebImage, 那么可能有好几种状态,(.normal,.highlight, .selected) , validOperationKey用于标识需要取消和开启下载任务的 NSOperation 对象。
         * 2、如果没有设置就使用类名,后续框架用于图像类匹配,防止错误
         */

        validOperationKey = NSStringFromClass([self class]);
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        context = [mutableContext copy];
    }
    
    self.sd_latestOperationKey = validOperationKey;
    
    // 取消加载任务, 防止重用cell出现问题,保证了划出屏幕外,不再进行下载
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    
    // 保存图像 url
    self.sd_imageURL = url;
    
    
    SDWebImageManager *manager = context[SDWebImageContextCustomManager];
    if (!manager) {
        manager = [SDWebImageManager sharedManager];
    } else {
        // 当时感觉莫名其妙,后来看了下,属于早期废弃的逻辑,这里现在只是在兼容
        // context 中不再保存 SDWebImageManager 了,manager 永远为 nil
        
        // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
        // 删除这个管理器以避免保留周期(manger -> loader -> operation -> context -> manager)
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextCustomManager] = nil;
        context = [mutableContext copy];
    }
    
    // 是否使用弱引用缓存
    BOOL shouldUseWeakCache = NO;
    if ([manager.imageCache isKindOfClass:SDImageCache.class]) {
        shouldUseWeakCache = ((SDImageCache *)manager.imageCache).config.shouldUseWeakMemoryCache;
    }
    
    // SDWebImageDelayPlaceholder: 默认情况下,占位符图像是在图像加载时加载的。这个标志将延迟占位符图片的加载,直到图片加载完成。
    if (!(options & SDWebImageDelayPlaceholder)) {
        if (shouldUseWeakCache) {
            NSString *key = [manager cacheKeyForURL:url context:context];
            // call memory cache to trigger weak cache sync logic, ignore the return value and go on normal query
            // this unfortunately will cause twice memory cache query, but it's fast enough
            // in the future the weak cache feature may be re-design or removed
            // 调用内存缓存来触发弱缓存同步逻辑,忽略返回值,继续正常的查询,这将不幸地导致两次内存缓存查询,但它足够快,在未来,弱缓存功能可能会被重新设计或删除
            
            // 其实这里的意思是在原有的 NSCache 里面多存一个 NSMapTable 对象, Value被设计成了弱引用模式。
            // SD解释说后续可能重做或者删除,因为会导致内存进行两次缓存和查询
            [((SDImageCache *)manager.imageCache) imageFromMemoryCacheForKey:key];
        }
        dispatch_main_async_safe(^{
            // 设置占位图
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
    
    if (url) {
        // reset the progress
        
        // 重置进度条
        NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
        if (imageProgress) {
            imageProgress.totalUnitCount = 0;
            imageProgress.completedUnitCount = 0;
        }
        
#if SD_UIKIT || SD_MAC
        // check and start image indicator
        [self sd_startImageIndicator];
        id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
        
        // 图片下载进度 callBack Block
        SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            // 如果 NSProgress *imageProgress 设置了
            if (imageProgress) {
                imageProgress.totalUnitCount = expectedSize;
                imageProgress.completedUnitCount = receivedSize;
            }
#if SD_UIKIT || SD_MAC
            if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
                double progress = 0;
                if (expectedSize != 0) {
                    progress = (double)receivedSize / expectedSize;
                }
                progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imageIndicator updateIndicatorProgress:progress];
                });
            }
#endif
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        @weakify(self);
        // 生成一个请求任务
        /**
         * 1、检查URL 的情况。如果URL无效或者 URL = nil 就返回 一个为 nil 的 SDWebImageOperation;
         * 2、如果是一个404导致失败的图片,(SDWebImage 默认不重新请求,除非设置  options 包含了 SDWebImageRetryFailed)就直接返回 SDWebImageOperation 对象,返回错误信息;
         * 3、将返回的 SDWebImageOperation 任务对象 保存到 runningOperations 集合里面;
         * 4、创建和开启同步或者异步查询缓存任务,将任务保存到 SDWebImageOperation 的 cacheOperation 的属性下
         * 5、创建和开启图片下载任务,将任务保存到 SDWebImageOperation 的 loaderOperation 的属性下
         * 6、将 SDWebImageOperation 任务对象添加到 当前 view 对象的关联属性 operationsMapTab(字典)里面。
         */
        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) {
            @strongify(self);
            if (!self) { return; }
            // if the progress not been updated, mark it to complete state
            if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
                imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            
#if SD_UIKIT || SD_MAC
            // check and stop image indicator
            if (finished) {
                [self sd_stopImageIndicator];
            }
#endif
            
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            SDWebImageNoParamsBlock callCompletedBlockClosure = ^{
                if (!self) { return; }
                if (!shouldNotSetImage) {
                    [self sd_setNeedsLayout];
                }
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, data, error, cacheType, finished, url);
                }
            };
            
            // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
            // OR
            // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
            if (shouldNotSetImage) {
                dispatch_main_async_safe(callCompletedBlockClosure);
                return;
            }
            
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            if (image) {
                // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                targetImage = placeholder;
                targetData = nil;
            }
            
#if SD_UIKIT || SD_MAC
            // check whether we should use the image transition
            // 图片展示时的动画 transition
            /**
             * SDWebImageForceTransition
             * 默认情况下,当你使用' SDWebImageTransition '在图像加载完成后做一些视图转换,
             * 这种转换只适用于从管理器异步回调(从网络,或磁盘缓存查询)的图像,
             * 这个掩码可以强制在任何情况下应用视图转换,比如内存缓存查询,或者同步磁盘缓存查询。
             */
            SDWebImageTransition *transition = nil;
            BOOL shouldUseTransition = NO;
            if (options & SDWebImageForceTransition) {
                // Always
                shouldUseTransition = YES;
            } else if (cacheType == SDImageCacheTypeNone) {
                // From network
                shouldUseTransition = YES;
            } else {
                // From disk (and, user don't use sync query)
                if (cacheType == SDImageCacheTypeMemory) {
                    shouldUseTransition = NO;
                } else if (cacheType == SDImageCacheTypeDisk) {
                    if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) {
                        shouldUseTransition = NO;
                    } else {
                        shouldUseTransition = YES;
                    }
                } else {
                    // Not valid cache type, fallback
                    shouldUseTransition = NO;
                }
            }
            if (finished && shouldUseTransition) {
                // 图片展示时的动画 transition
                transition = self.sd_imageTransition;
            }
#endif
            dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
                // 图片设置
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
                callCompletedBlockClosure();
            });
        }];
        
        // 初始化 operation 任务对象后,将任务对象添加到 当前 view 对象的关联属性 operationsMapTab(字典)里面。
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
#if SD_UIKIT || SD_MAC
        [self sd_stopImageIndicator];
#endif
        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);
            }
        });
    }
}

总结一下:

  • 1、取消加载任务, 防止重用cell出现问题,保证了划出屏幕外,不再进行下载;(本文开始问题一答案)

  • 2、创建 SDWebImageManager 对象;

  • 3、是否使用弱引用缓存(可能重做或者废弃);

  • 4、设置占位图(异步主线程);

  • 5、生成一个请求任务 SDWebImageOperation 对象,对象中包含了 cacheOperation 缓存查询任务对象和 loaderOperation 下载任务对象;

  • 6、初始化 SDWebImageOperation 任务对象后,将任务对象添加到当前 view 对象的关联属性 operationsMapTab(NSMapTable)中;(使用 NSMapTable 是因为可能有多个 SDWebImageOperation 对象,因为 Button 有多个状态)

  • 7、开始 SDWebImageOperation 任务(这里面包含了,图片下载和图片固化逻辑);

  • 8、得到下载结果后设置进度条、设置加载 Indicator 状态、判断是否需要设置图片动画、切换主线程设置图片。

3.4 取消加载任务 sd_cancelImageLoadOperationWithKey()

本文问题一答案就在这里。

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // 取消加载任务, 防止重用cell出现问题,保证了划出屏幕外,不再进行下载
    if (key) {
        // Cancel in progress downloader from queue
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;
        
        // 加锁,防止线程资源争夺。
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            // 是否符合协议
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                [operation cancel];
            }
            @synchronized (self) {
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}

- (SDOperationsDictionary *)sd_operationDictionary {
    @synchronized(self) {
        
        // 原理就是给当前 View 对象上挂一个属性,
        // 如果属性存在,就代表是之前存在的已经开始加载图片了,不存在就代表是新创建的。
        SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
        if (operations) {
            return operations;
        }
        // https://www.jianshu.com/p/3b79383f66eb
        // 正常的 Dictionary 的 key是要遵循 NSCopying 协议的,Value是强引用,除非从字典中移除key,Value才会销毁。
        // 可以理解为:生成一个 key 是强引用,Value是弱引用的 一个特殊的可变字典 (MapTable)
        // 这样的 MapTable 如果 Value 自己销毁了,那么 NSMapTable 中 key-value 这一对儿就会自动移除。
        operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
        objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        return operations;
    }
}
// SDWebImageCombinedOperation

@implementation SDWebImageCombinedOperation

- (void)cancel {
    @synchronized(self) {
        if (self.isCancelled) {
            return;
        }
        self.cancelled = YES;
        if (self.cacheOperation) {
            [self.cacheOperation cancel];
            self.cacheOperation = nil;
        }
        if (self.loaderOperation) {
            [self.loaderOperation cancel];
            self.loaderOperation = nil;
        }
        [self.manager safelyRemoveOperationFromRunning:self];
    }
}

@end

从上述代码看到:

从当前 view 对象上找关联对象 operationDictionary ,如果找到了,说明之前已经有请求了,根据当前请求的 key 获取 SDWebImageOperation 对象,调用 [cacheOperation cancel][loaderOperation cancel]

这里关于 NSMapTable 拓展一下:

正常的 Dictionarykey 是要遵循 NSCopying 协议的,Value 是强引用,除非从字典中移除对 Value 的持有,Value 才会被销毁。

但是 MapTable 可以生成一个 key 是强引用,Value 是弱引用的一个特殊的可变字典 (MapTable),这样的 MapTable 如果 Value 自己销毁了,那么 NSMapTablekey-value 这一对儿就会自动移除。

这里用处比较大,比如,如果你想使用一个字典,但是 key 是个对象,那么你就需要类遵守的 NSCopying 协议,实现 copyWithZone()hash()isEqual() 方法,但是拜托我使用 对象 key 只是想方便开发,反而更麻烦了,这时可以使用 NSMapTable ,设置 key 是强引用,Value 根据情况设置强引用或弱引用,就能方便开发了。

3.5 开始加载图片 loadImageWithURL

- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock {
    // Invoking this method without a completedBlock is pointless
    // 在没有completedBlock的情况下调用这个方法是没有意义的
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
    // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
    // 最常见的错误是使用NSString对象而不是NSURL发送URL。由于一些奇怪的原因,Xcode不会对这种类型不匹配抛出任何警告。这里我们通过允许url作为NSString传递来防止这个错误。
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    // 防止应用程序在参数类型错误时崩溃,比如发送NSNull而不是NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    // 创建 operation 对象
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;

    BOOL isFailedUrl = NO;
    if (url) {
        SD_LOCK(_failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        SD_UNLOCK(_failedURLsLock);
    }

    // 默认情况下,当URL下载失败时,该URL将被列入黑名单,因此库不会继续尝试
    // SDWebImageRetryFailed: 。*该标志禁用黑名单。
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
        // SDWebImageErrorInvalidURL = 1000, // URL无效,如nil URL或已损坏的URL
        // SDWebImageErrorBlackListed = 1003, // 该URL被列入黑名单,因为下载程序标记了不可恢复的失败(如404),你可以使用'。retryFailed '选项来避免这种情况
        NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
        
        // 主线程 返回 completionBlock(sd_image加载的结果)
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
        return operation;
    }

    SD_LOCK(_runningOperationsLock);
    // 将任务保存到 runningOperations 里面,方便后期查询后进行取消操作
    [self.runningOperations addObject:operation];
    SD_UNLOCK(_runningOperationsLock);
    
    // Preprocess the options and context arg to decide the final the result for manager
    //预处理选项和上下文参数,以决定最终的结果
    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
    
    // Start the entry to load image from cache
    // 启动条目从缓存加载图像
    [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];

    return operation;
}

这里简述一下 SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context]; 这个方法干的事情:

  • 1、配置使用者设置的 Transformer 图片加载动画器;

  • 2、配置使用者设置的自定义图片缓存的 key,用于后续从内存+磁盘查询图片缓存用该 key

  • 3、配置使用者设置的缓存序列化器,你可以解决一些 webP 图片解码慢的问题,比如可以直接将 webP 解码后保存为 PNG/ JPEG

  • 4、 提供给外部对 options 进行二次修改的方法。

如果没有配置的话就使用默认值。

3.6 从缓存中查询 callCacheProcessForOperation

// Query normal cache process
// 查询正常缓存进程
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock {
    // Grab the image cache to use
    // 获取要使用的图像缓存
    id<SDImageCache> imageCache;
    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
        // 传入了自定义就使用自定义的
        imageCache = context[SDWebImageContextImageCache];
    } else {
        // 使用SDWebImageCache
        imageCache = self.imageCache;
    }
    
    // Get the query cache type
    // 获取缓存类型,默认内存+磁盘
    SDImageCacheType queryCacheType = SDImageCacheTypeAll;
    if (context[SDWebImageContextQueryCacheType]) {
        queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
    }
    
    // Check whether we should query cache
    // 检查是否需要查询缓存
    BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
    if (shouldQueryCache) {
        NSString *key = [self cacheKeyForURL:url context:context];
        @weakify(operation);
        
        // 查询缓存的 cacheOperation
        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) {
                // Image combined operation cancelled by user
                [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;
            } else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
                // Have a chance to query original cache instead of downloading
                [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
                return;
            }
            
            // Continue download process
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {
        // Continue download process
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
    }
}

查询缓存这里没有什么说的了,就是比较常规的东西。先从内存查询 NSCache 对象,命中后返回;内存没命中,从磁盘查询走文件系统,都没有命中就开始下载(超过缓存过期时间也重新下载)。

查询缓存成功后有一个解码操作,本文后面单独拿出来说,和本文问题 2 相关。

3.7 下载图片 downloadImageWithURL()

中间过程略过了,只说明调用流程。 callDownloadProcessForOperation() -> [imageLoader requestImageWithURL:...] -> [SDWebImageDownloader requestImageWithURL...] -> [SDWebImageDownloader downloadImageWithURL...] 就来到了当前小节的代码。

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock) {
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
            completedBlock(nil, nil, error, YES);
        }
        return nil;
    }
    
    SD_LOCK(_operationsLock);
    id downloadOperationCancelToken;
    NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
    if (!operation || operation.isFinished || operation.isCancelled) {
        
        // 创建任务,这里承载了图片下载和解码的所有工作
        operation = [self createDownloaderOperationWithUrl:url options:options context:context];
        
        if (!operation) {
            SD_UNLOCK(_operationsLock);
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}];
                completedBlock(nil, nil, error, YES);
            }
            return nil;
        }
        @weakify(self);
        operation.completionBlock = ^{
            @strongify(self);
            if (!self) {
                return;
            }
            SD_LOCK(self->_operationsLock);
            [self.URLOperations removeObjectForKey:url];
            SD_UNLOCK(self->_operationsLock);
        };
        self.URLOperations[url] = operation;
        
        // Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers.
        // 在提交到操作队列之前添加处理程序,避免操作在设置处理程序之前完成的竞态条件。
        downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        
        // 任务起飞
        [self.downloadQueue addOperation:operation];
    } else {
        // When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue)
        // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.
        
        //当我们重用下载操作附加更多的回调函数时,可能会有线程安全问题,因为回调函数的getter可能在另一个队列中(解码队列或委托队列)
        //因此我们锁定这里的操作,在' SDWebImageDownloaderOperation '中,我们使用' @synchonzied (self) ',以确保这两个类之间的线程安全。
        @synchronized (operation) {
            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        }
        if (!operation.isExecuting) {
            if (options & SDWebImageDownloaderHighPriority) {
                operation.queuePriority = NSOperationQueuePriorityHigh;
            } else if (options & SDWebImageDownloaderLowPriority) {
                operation.queuePriority = NSOperationQueuePriorityLow;
            } else {
                operation.queuePriority = NSOperationQueuePriorityNormal;
            }
        }
    }
    SD_UNLOCK(_operationsLock);
    
    // SDWebImage 设计的过程希望外部只在意获取到的结果,而不必考虑是怎么获取图片的。
    SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
    token.url = url;
    token.request = operation.request;
    token.downloadOperationCancelToken = downloadOperationCancelToken;
    
    return token;
}

主要逻辑是在 operation = [self createDownloaderOperationWithUrl:url options:options context:context]; 这里承载了图片下载和解码的所有工作,创建好 NSOperation 对象后,[self.downloadQueue addOperation:operation]; 任务起飞。

SDWebImageDownloaderOperation 内部重写了 startcancel 等方法。

3.8 创建下载器任务对象 createDownloaderOperationWithUrl()

- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
                                                                                  options:(SDWebImageDownloaderOptions)options
                                                                                  context:(nullable SDWebImageContext *)context {
    NSTimeInterval timeoutInterval = self.config.downloadTimeout;
    if (timeoutInterval == 0.0) {
        timeoutInterval = 15.0;
    }
    
    // ...
    
    // 创建一个下载 operation,重点方法
    NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
    
    // ... 
    
    return operation;
}

3.9 下载器任务 SDWebImageDownloaderOperation 类

/**
 Describes a downloader operation. If one wants to use a custom downloader op, it needs to inherit from `NSOperation` and conform to this protocol
 For the description about these methods, see `SDWebImageDownloaderOperation`
 @note If your custom operation class does not use `NSURLSession` at all, do not implement the optional methods and session delegate methods.
 */
@protocol SDWebImageDownloaderOperation <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
@required
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options
                                context:(nullable SDWebImageContext *)context;

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

- (BOOL)cancel:(nullable id)token;

@property (strong, nonatomic, readonly, nullable) NSURLRequest *request;
@property (strong, nonatomic, readonly, nullable) NSURLResponse *response;

@optional
@property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask;
@property (strong, nonatomic, readonly, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

// ...

@end


/**
 The download operation class for SDWebImageDownloader.
 */
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperation>

// ...
@end

Class SDWebImageDownloaderOperation 类遵守了 protocol SDWebImageDownloaderOperation 协议, SDWebImageDownloaderOperation 协议有遵守了 <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>

这里这样设计的原因:SDWebImage 设计者希望外部只在意获取到的结果,而不必考虑是怎么获取图片的。

所以在 SDWebImageDownloader 获取接收的 NSURLSessionTaskDelegate, NSURLSessionDataDelegate 回调全部传给了 SDWebImageDownloaderOperation 的对象处理。

比如:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {

    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
    if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) {
        [dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
    }
}

这个类就不细看了,是一些网络请求的数据回调处理。这里主要看一个方法就可以了:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    // If we already cancel the operation or anything mark the operation finished, don't callback twice
    if (self.isFinished) return;
    
    // 发送 SDWebImageDownloadStopNotification 通知 ... 
    // 发送 SDWebImageDownloadFinishNotification 通知 ... 
    
   
    // 下载错误了,向上层返回错误信息...
    
    // 下载成功了...
    
    // 重点来了
    [self.coderQueue cancelAllOperations];
    @weakify(self);
    
    // 解码队列
    [self.coderQueue addOperationWithBlock:^{
        @strongify(self);
        if (!self) {
            return;
        }
        // check if we already use progressive decoding, use that to produce faster decoding
        id<SDProgressiveImageCoder> progressiveCoder = SDImageLoaderGetProgressiveCoder(self);
        UIImage *image;
        
         // 有外部设置的解码器,就使用,没有使用默认
        if (progressiveCoder) {
            image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, YES, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
        } else {
        
            // 解码代码
            image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
        }
        CGSize imageSize = image.size;
        if (imageSize.width == 0 || imageSize.height == 0) {
            NSString *description = image == nil ? @"Downloaded image decode failed" : @"Downloaded image has 0 pixels";
            [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : description}]];
        } else {
            [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
        }
        [self done];
    }];
    
}

3.10 图片解码 SDImageLoaderDecodeImageData()

UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
    NSCParameterAssert(imageData);
    NSCParameterAssert(imageURL);
    
    UIImage *image;
    
    // SD向外部询问,有没有key的转换
    id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
    NSString *cacheKey;
    if (cacheKeyFilter) {
        cacheKey = [cacheKeyFilter cacheKeyForURL:imageURL];
    } else {
        /// 默认的
        cacheKey = imageURL.absoluteString;
    }
    
    // 是否这强制只解码第一帧并生成静态图像。
    BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
    
    // 缩放比例
    NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
    
    // 未设置默认1
    CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
    
    // 宽高比
    NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio];
    NSValue *thumbnailSizeValue;
    BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages);
    if (shouldScaleDown) {
        CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4;
        CGFloat dimension = ceil(sqrt(thumbnailPixels));
        thumbnailSizeValue = @(CGSizeMake(dimension, dimension));
    }
    
    // 缩略图大小
    if (context[SDWebImageContextImageThumbnailPixelSize]) {
        thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
    }
    
    // 配置赋值
    SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
    mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
    mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
    mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
    mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
    mutableCoderOptions[SDImageCoderWebImageContext] = context;
    SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
    
    // Grab the image coder
    // 获取图像编码器
    id<SDImageCoder> imageCoder;
    if ([context[SDWebImageContextImageCoder] conformsToProtocol:@protocol(SDImageCoder)]) {
        // 外部实现
        imageCoder = context[SDWebImageContextImageCoder];
    } else {
        // SDWebImage 实现
        imageCoder = [SDImageCodersManager sharedManager];
    }
    
    //
    if (!decodeFirstFrame) {
        // check whether we should use `SDAnimatedImage`
        // 检查我们是否应该使用SDAnimatedImage
        Class animatedImageClass = context[SDWebImageContextAnimatedImageClass];
        if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) {
            
            // 这里是动态图片解析过程
            // 推荐文章: https://zhuanlan.zhihu.com/p/30591648?from_voters_page=true
            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;
                }
            }
        }
    }
    if (!image) {
        image = [imageCoder decodedImageWithData:imageData options:coderOptions];
    }
    if (image) {
        BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
        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];
        }
    }
    
    return image;
}

重点在于:image = [[animatedImageClass alloc] initWithData:imageData scale:scale options:coderOptions];

3.11 获取动图每一帧信息 createFrameAtIndex()

关于获取动图的每一帧可以看看这篇文章 iOS平台图片编解码入门教程(Image/IO篇)

中间的代码就省略了,调用流程:[SDAnimatedImage initWithData(data,scale:,options)] -> [SDAnimatedImage initWithAnimatedCoder(animatedCoder,scale)] -> [animatedCoder animatedImageFrameAtIndex:0]

+ (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);
    
    // 图片宽和高
    CGFloat pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue];
    CGFloat pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue];
    
    // 获取图片的方向
    CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue];
    if (!exifOrientation) {
        // 默认: 上
        exifOrientation = kCGImagePropertyOrientationUp;
    }
    
    // CGImageSourceGetType: 获取图像格式
    CFStringRef uttype = CGImageSourceGetType(source);
    // Check vector format
    BOOL isVector = NO;
    if ([NSData sd_imageFormatFromUTType:uttype] == SDImageFormatPDF) {
        isVector = YES;
    }

    NSMutableDictionary *decodingOptions;
    if (options) {
        decodingOptions = [NSMutableDictionary dictionaryWithDictionary:options];
    } else {
        decodingOptions = [NSMutableDictionary dictionary];
    }
    CGImageRef imageRef;
    BOOL createFullImage = thumbnailSize.width == 0 || thumbnailSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height);
    if (createFullImage) {
        if (isVector) {
            if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
                // Provide the default pixel count for vector images, simply just use the screen size
#if SD_WATCH
                thumbnailSize = WKInterfaceDevice.currentDevice.screenBounds.size;
#elif SD_UIKIT
                thumbnailSize = UIScreen.mainScreen.bounds.size;
#elif SD_MAC
                thumbnailSize = NSScreen.mainScreen.frame.size;
#endif
            }
            CGFloat maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height);
            NSUInteger DPIPerPixel = 2;
            NSUInteger rasterizationDPI = maxPixelSize * DPIPerPixel;
            decodingOptions[kSDCGImageSourceRasterizationDPI] = @(rasterizationDPI);
        }
        imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
    } else {
       
        /**
         * kCGImageSourceCreateThumbnailWithTransform
         * 指定缩略图是否应该根据整个图像的方向和像素长宽比进行旋转和缩放。
         * 该键的值必须是 CFBooleanRef;
         * 默认值为kCFBooleanFalse
         */
        decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio);
        
        CGFloat maxPixelSize;
        if (preserveAspectRatio) {
            CGFloat pixelRatio = pixelWidth / pixelHeight;
            CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height;
            if (pixelRatio > thumbnailRatio) {
                maxPixelSize = thumbnailSize.width;
            } else {
                maxPixelSize = thumbnailSize.height;
            }
        } else {
            maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height);
        }
        
        /**
         * kCGImageSourceThumbnailMaxPixelSize
         * 指定缩略图的最大宽度和高度像素。
         * 如果这个键没有指定,缩略图的宽度和高度是不受限制,缩略图可能和图像本身一样大。
         * 如果存在,这个键的值必须是一个CFNumberRef。
         */
        
        decodingOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = @(maxPixelSize);
        
        /**
         * kCGImageSourceCreateThumbnailFromImageAlways
         * 指定是否应该从完整图像创建缩略图,即使缩略图存在于图像源文件中。
         * 缩略图将从完整的图像创建,受kCGImageSourceThumbnailMaxPixelSize指定的限制
         * 如果没有指定最大的像素大小,那么缩略图将是完整图像的大小,这可能不是您想要的。
         * 该键的值必须是CFBooleanRef;默认值为kCFBooleanFalse。
         */
        decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageAlways] = @(YES);
        
        // 返回图像源' isrc'中' index'处的图像缩略图。' options' 字典可以用来请求额外的缩略图创建选项;
        imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
    }
    if (!imageRef) {
        return nil;
    }
    // Thumbnail image post-process
    if (!createFullImage) {
        if (preserveAspectRatio) {
            // kCGImageSourceCreateThumbnailWithTransform will apply EXIF transform as well, we should not apply twice
            // kCGImageSourceCreateThumbnailWithTransform 也将应用EXIF转换,我们不应该应用两次
            
            /**
             * 这里的解释:
             * 如果 preserveAspectRatio == true
             * decodingOptions 中 kCGImageSourceCreateThumbnailWithTransform 就会给 true, 指定缩略图是否应该根据整个图像的方向和像素长宽比进行旋转和缩放。
             * 在处理的过程中已经旋转过了
             */
            exifOrientation = kCGImagePropertyOrientationUp;
        } else {
            // `CGImageSourceCreateThumbnailAtIndex` take only pixel dimension, if not `preserveAspectRatio`, we should manual scale to the target size
            // 'CGImageSourceCreateThumbnailAtIndex' 只取像素尺寸,如果不是' preserveAspectRatio ',我们应该手动缩放到目标尺寸
            CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:imageRef size:thumbnailSize];
            CGImageRelease(imageRef);
            imageRef = scaledImageRef;
        }
    }
    
#if SD_UIKIT || SD_WATCH
    UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
    UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:imageOrientation];
#else
    UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation];
#endif
    CGImageRelease(imageRef);
    return image;
}

3.12 静态图片解压缩 [SDImageCoderHelper decodedImageWithImage]

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    if (![self shouldDecodeImage:image]) {
        return image;
    }
    
    // 核心代码 开始解压缩
    CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
    if (!imageRef) {
        return image;
    }
#if SD_MAC
    UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
    // 解压缩后的位图,转换成 image
    UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
#endif
    CGImageRelease(imageRef);
    SDImageCopyAssociatedObject(image, decodedImage);
    decodedImage.sd_isDecoded = YES;
    return decodedImage;
}

通过 [self CGImageCreateDecoded:image.CGImage] 开始解压缩,代码如下:

+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
    if (!cgImage) {
        return NULL;
    }
    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;
    }
    
    // 判断有没有透明通道
    BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
    
    // iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]`
    // iOS更喜欢BGRA8888(预乘)或BGRX8888 bitmapInfo屏幕渲染,这是相同的' UIGraphicsBeginImageContext() '或' - [CALayer drawInContext:] '
    
    
    // Though you can use any supported bitmapInfo 
    // (see: https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB ) 
    // and let Core Graphics reorder it when you call `CGContextDrawImage`
    
    // 尽管您可以使用任何支持bitmapInfo
    // (参见:https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html / / apple_ref / doc / uid / TP30001066-CH203-BCIBHHBB)
    // 让核心图形重新排序的时候你叫“CGContextDrawImage”
    
    // But since our build-in coders use this bitmapInfo, this can have a little performance benefit
    // 但由于我们的内置编码器使用这个bitmapInfo,这可以有一点性能上的好处
    
    // kCGBitmapByteOrder32Hostv = kCGBitmapByteOrder32Little
    // kCGBitmapByteOrder32Little 小端对齐,对于iOS表述的RGBA R位于最低位A位于最高位。既有0xABGR
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
    
    // kCGImageAlphaPremultipliedFirst:  For example, premultiplied ARGB
    bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
    CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
    if (!context) {
        return NULL;
    }
    
    // Apply transform
    CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
    
    // 使用指定的矩阵转换上下文中的用户坐标系统。
    CGContextConcatCTM(context, transform);
    
    // 函数将原始位图绘制到上下文中
    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;
}

这里就借用一下 CC老师_HelloCoder 大佬的 探讨iOS 中图片的解压缩到渲染过程 说明一下 iOS 图片解压缩的过程,方便理解上述代码。(我复制了一下,大家也可以看原文)

image.png

通常计算机在显示是CPU与GPU协同合作完成一次渲染。接下来我们了解一下 CPU/GPU等在这样一次渲染过程中,具体的分工是什么?

  • CPU:计算视图 frame,图片解码,需要绘制纹理图片通过数据总线交给GPU

  • GPU:纹理混合,顶点变换与计算,像素点的填充计算,渲染到帧缓冲区。

  • 时钟信号:垂直同步信号V-Sync / 水平同步信号H-Sync。

  • ios设备双缓冲机制:显示系统通常会引入两个帧缓冲区,双缓冲机制图片显示到屏幕上是CPU与GPU的协作完成

对应 iOS 应用来说,图片是最占用手机内存的资源,同时也是不可或缺的组成部分。将一张图片从磁盘中加载出来,并最终显示到屏幕上,中间有一步就是解压缩。

我们平常使用 UIImage 的方法从磁盘加载一张图片时发现,拿获取到的 UIImage 对象直接给了 UIImageView,并没有书写上述这种解压缩的代码。

其实,将 UIImageUIImageView 的时候,隐式的 CATransaction 捕获到了 UIImageView 图层树的变化,Core Animation 就会在主队列上提交一个隐式的 transaction

此时主队列会开始分配内存缓冲区用于图片解压缩操作,然后将图片数据读入内存之后,会将后缀为 PNGJPEG 这种压缩后的图片数据解码成未压缩的位图形式,然后将 CPU 计算好图片的 Frame 和解压后的位图数据一起交付给 GPU ,由 GPU 渲染到 UIImageView 图层。

但是数据解码发生在主队列上。我们知道,如果大量使用主队列去做耗时操作势必会造成界面卡顿。

此时就有了提前在子线程对图片解压缩的过程,然后将解压缩后的数据再交付,就能避免上述问题发生。

CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
    size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
    CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
    CG_AVAILABLE_STARTING(10.0, 2.0);
  • data:如果不为 NULL,那么它应该指向一块大小至少为 bytesPerRow *height 字节的内存;如果为 NULL,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可;

  • width 和 height:位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;

  • bitsPerComponent:像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;

  • bytesPerRow :位图的每一行使用的字节数,大小至少为 width * bytesper pixe1 字节。当指定 O/NULL 时,系统不仅会自动计算,而且还会进行 cache line alignment 的优化;

  • space:就是我们前面提到的颜色空问,一般使用 RGB 即可;

  • bitmapinfo:位图的布局信息。

现在再看上方代码就给清晰了:

  • 创建一个位图上下文 CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);

  • 将原始位图绘制到上下文中 CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);

  • 创建一张新的解压缩后的位图 CGImageRef newImageRef = CGBitmapContextCreateImage(context);

  • 将位图返回以供上层使用

到这里网络请求和解码都结束了,后续就是一些返回 image 然后切主线程设置图片了。

接下来解决上面问题二:GIF 图片是怎么动起来的

3.13 SDAnimatedImageView 类的学习

@interface SDAnimatedImageView : UIImageView
/**
 The internal animation player.
 This property is only used for advanced usage, like inspecting/debugging animation status, control progressive loading, complicated animation frame index control, etc.
 @warning Pay attention if you directly update the player's property like `totalFrameCount`, `totalLoopCount`, the same property on `SDAnimatedImageView` may not get synced.
 */
 
 // 动图就是由这个家伙完成的
@property (nonatomic, strong, readonly, nullable) SDAnimatedImagePlayer *player;

// ... 其他属性就先忽略了
@end

其实动图就是由 SDAnimatedImagePlayer 完成的。

SDAnimatedImagePlayer 中启动了一个 NSTimer *displayLink 定时器对象,时间为 1/60 ,一个 runloop 的循环时间(前提没有卡顿)。

[NSTimer timerWithTimeInterval:kSDDisplayLinkInterval target:weakProxy selector:@selector(displayLinkDidRefresh:) userInfo:nil repeats:YES];

核心代码如下:

// SDAnimatedImagePlayer.m

- (void)displayDidRefresh:(SDDisplayLink *)displayLink {
    // If for some reason a wild call makes it through when we shouldn't be animating, bail.
    // Early return!
    if (!self.isPlaying) {
        return;
    }
    
    // 获取GIF图总数
    NSUInteger totalFrameCount = self.totalFrameCount;
    if (totalFrameCount <= 1) {
        // 1张不是GIF
        // Total frame count less than 1, wrong configuration and stop animating
        [self stopPlaying];
        return;
    }
    
    // 播放速率
    NSTimeInterval playbackRate = self.playbackRate;
    if (playbackRate <= 0) {
        // Does not support <= 0 play rate
        [self stopPlaying];
        return;
    }
    
    // 计算刷新持续时间
    NSTimeInterval duration = self.displayLink.duration;
    
    // 上一次从第几个播放的
    NSUInteger currentFrameIndex = self.currentFrameIndex;
    
    // 下一个取模,是否从头播放
    NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount;
    
    if (self.playbackMode == SDAnimatedImagePlaybackModeReverse) {
        nextFrameIndex = currentFrameIndex == 0 ? (totalFrameCount - 1) : (currentFrameIndex - 1) % totalFrameCount;
        
    } else if (self.playbackMode == SDAnimatedImagePlaybackModeBounce ||
               self.playbackMode == SDAnimatedImagePlaybackModeReversedBounce) {
        if (currentFrameIndex == 0) {
            self.shouldReverse = false;
        } else if (currentFrameIndex == totalFrameCount - 1) {
            self.shouldReverse = true;
        }
        nextFrameIndex = self.shouldReverse ? (currentFrameIndex - 1) : (currentFrameIndex + 1);
        nextFrameIndex %= totalFrameCount;
    }
    
    
    // Check if we need to display new frame firstly
    BOOL bufferFull = NO;
    if (self.needsDisplayWhenImageBecomesAvailable) {
        UIImage *currentFrame;
        SD_LOCK(_lock);
        currentFrame = self.frameBuffer[@(currentFrameIndex)];
        SD_UNLOCK(_lock);
        
        // Update the current frame
        if (currentFrame) {
            SD_LOCK(_lock);
            // Remove the frame buffer if need
            if (self.frameBuffer.count > self.maxBufferCount) {
                self.frameBuffer[@(currentFrameIndex)] = nil;
            }
            // Check whether we can stop fetch
            if (self.frameBuffer.count == totalFrameCount) {
                bufferFull = YES;
            }
            SD_UNLOCK(_lock);
            
            // Update the current frame immediately
            self.currentFrame = currentFrame;
            
            // 给 SDAnimatedImageView 回调
            [self handleFrameChange];
            
            self.bufferMiss = NO;
            self.needsDisplayWhenImageBecomesAvailable = NO;
        }
        else {
            self.bufferMiss = YES;
        }
    }
    
    // Check if we have the frame buffer
    
    // 如果有缓存直接走缓存
    if (!self.bufferMiss) {
        // Then check if timestamp is reached
       
        // 累计当前播放过的时间
        self.currentTime += duration;
        
        // 获取这一帧率的时间
        NSTimeInterval currentDuration = [self.animatedProvider animatedImageDurationAtIndex:currentFrameIndex];
        
        // 倍率播放计算时间
        currentDuration = currentDuration / playbackRate;
        
        // 当前帧时间戳未到达,返回
        if (self.currentTime < currentDuration) {
            // Current frame timestamp not reached, return
            return;
        }
        
        // Otherwise, we should be ready to display next frame
        self.needsDisplayWhenImageBecomesAvailable = YES;
        self.currentFrameIndex = nextFrameIndex;
        self.currentTime -= currentDuration;
        
        // 下一帧时间
        NSTimeInterval nextDuration = [self.animatedProvider animatedImageDurationAtIndex:nextFrameIndex];
        nextDuration = nextDuration / playbackRate;
        if (self.currentTime > nextDuration) {
            // Do not skip frame
            self.currentTime = nextDuration;
        }
        
        // Update the loop count when last frame rendered
        
        // 从头播放更新数据
        if (nextFrameIndex == 0) {
            // Update the loop count
            self.currentLoopCount++;
            [self handleLoopChange];
            
            // if reached the max loop count, stop animating, 0 means loop indefinitely
            NSUInteger maxLoopCount = self.totalLoopCount;
            if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) {
                [self stopPlaying];
                return;
            }
        }
    }
    
    // Since we support handler, check animating state again
    if (!self.isPlaying) {
        return;
    }

    // Check if we should prefetch next frame or current frame
    // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame
    // Or, most cases, the decode speed is faster than render speed, we fetch next frame
    NSUInteger fetchFrameIndex = self.bufferMiss? currentFrameIndex : nextFrameIndex;
    UIImage *fetchFrame;
    SD_LOCK(_lock);
    fetchFrame = self.bufferMiss? nil : self.frameBuffer[@(nextFrameIndex)];
    SD_UNLOCK(_lock);
    
    // 没有缓存,任务队列没有任务,创建一个播放任务
    if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) {
        // Prefetch next frame in background queue
        id<SDAnimatedImageProvider> animatedProvider = self.animatedProvider;
        @weakify(self);
        
        // 子线程创建任务
        NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
            @strongify(self);
            if (!self) {
                return;
            }
            
            // 获取每一帧信息
            UIImage *frame = [animatedProvider animatedImageFrameAtIndex:fetchFrameIndex];

            BOOL isAnimating = self.displayLink.isRunning;
            if (isAnimating) {
                SD_LOCK(self->_lock);
                // 缓存每一帧信息
                self.frameBuffer[@(fetchFrameIndex)] = frame;
                SD_UNLOCK(self->_lock);
            }
        }];
        
        // 启动任务
        [self.fetchQueue addOperation:operation];
    }
}

然后 SDAnimatedImagePlayer 通过 void (^animationFrameHandler)(NSUInteger index, UIImage * frame); 返回给 SDAnimatedImageView 当前播放的 index 和当前帧的图片信息。

- (void)setImage:(UIImage *)image
{
    if (self.image == image) {
        return;
    }
    
    // ...
        
    // Create animated player
    self.player = [SDAnimatedImagePlayer playerWithProvider:provider];
        
   // ...

   // Setup handler
   @weakify(self);
        
   // 重点代码回调
   self.player.animationFrameHandler = ^(NSUInteger index, UIImage * frame) {
       @strongify(self);
            
       // 设置播放的 index
       self.currentFrameIndex = index;
            
       // 设置需要刷的的 image
       self.currentFrame = frame;
            
       // 调用后触发 CALayer 的 CALayerDelegate
       [self.imageViewLayer setNeedsDisplay];
   };
        
    // ...
         
    // Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
    super.highlighted = NO;
        
    [self stopAnimating];
    [self checkPlay];

    [self.imageViewLayer setNeedsDisplay];
}

animationFrameHandler回调后,调用 [self.imageViewLayer setNeedsDisplay]; 刷新图片,调用后触发了 CALayerDelegate 回调,就可以做到动图的播放。代码如下:

// CALayerDelegate
- (void)displayLayer:(CALayer *)layer
{
    UIImage *currentFrame = self.currentFrame;
    if (currentFrame) {
        // 设置倍率
        layer.contentsScale = currentFrame.scale;
        // 设置图片
        layer.contents = (__bridge id)currentFrame.CGImage;
    } else {
        // If we have no animation frames, call super implementation. iOS 14+ UIImageView use this delegate method for rendering.
        if ([UIImageView instancesRespondToSelector:@selector(displayLayer:)]) {
            [super displayLayer:layer];
        }
    }
}

说到这里,我当时认为:动图的播放是获取 GIF 每一帧的信息后,然后使用 UIImageView 传入所有图片给 imageView 的动画组播放。但是看完圆源码后发现,我忽略了,动图每一帧的播放时间可能是不同的,如果按照我的思路当然也能动起来,结果就是效果很奇怪。

4、结语

SDWebImage 的魅力不止于此,这篇文章只是简单的了解了一下源码的调用逻辑和一些设计方式,之后还是需要细细研究。

看完这篇文章,再回头看 SDWebImage 框架结构图 会发现不再那么眼花缭乱了。