SDWebImage 源码解读(二):Manager
初始化方法
SDWebImageManager 的指定初始化方法声明和实现如下:
/** * Allows to specify instance of cache and image loader used with image manager. * @return new instance of `SDWebImageManager` with specified cache and loader. */ - (nonnull instancetype)initWithCache:(nonnull id)cache loader:(nonnull id)loader NS_DESIGNATED_INITIALIZER; - (nonnull instancetype)initWithCache:(nonnull id)cache loader:(nonnull id)loader { if ((self = [super init])) { _imageCache = cache; _imageLoader = loader; _failedURLs = [NSMutableSet new]; _failedURLsLock = dispatch_semaphore_create(1); _runningOperations = [NSMutableSet new]; _runningOperationsLock = dispatch_semaphore_create(1); } return self; } 复制代码
不过在最简单的使用方法中,SDWebImage 是当作单例来使用的,相关代码如下:
/** * Returns global shared manager instance. */ @property (nonatomic, class, readonly, nonnull) SDWebImageManager *sharedManager; + (nonnull instancetype)sharedManager { static dispatch_once_t once; static id instance; dispatch_once(&once, ^{ instance = [self new]; }); return instance; } - (nonnull instancetype)init { id cache = [[self class] defaultImageCache]; if (!cache) { cache = [SDImageCache sharedImageCache]; } id loader = [[self class] defaultImageLoader]; if (!loader) { loader = [SDWebImageDownloader sharedDownloader]; } return [self initWithCache:cache loader:loader]; } 复制代码
在 init 方法中,cache 和 loader 会先通过类方法进行查找,如果找不到再使用 SDImageChche 和 SDWebImageDownloader 的单例,相关代码如下所示:
/** The default image cache when the manager which is created with no arguments. Such as shared manager or init. Defaults to nil. Means using `SDImageCache.sharedImageCache` */ @property (nonatomic, class, nullable) id defaultImageCache; /** The default image loader for manager which is created with no arguments. Such as shared manager or init. Defaults to nil. Means using `SDWebImageDownloader.sharedDownloader` */ @property (nonatomic, class, nullable) id defaultImageLoader; static id _defaultImageCache; static id _defaultImageLoader; + (id)defaultImageCache { return _defaultImageCache; } + (void)setDefaultImageCache:(id)defaultImageCache { if (defaultImageCache && ![defaultImageCache conformsToProtocol:@protocol(SDImageCache)]) { return; } _defaultImageCache = defaultImageCache; } + (id)defaultImageLoader { return _defaultImageLoader; } + (void)setDefaultImageLoader:(id)defaultImageLoader { if (defaultImageLoader && ![defaultImageLoader conformsToProtocol:@protocol(SDImageLoader)]) { return; } _defaultImageLoader = defaultImageLoader; } 复制代码
加载图片
加载图片分为两个步骤,首先回去 cache 里面查找图片,如果没有再进行下载,方法的声明如下:
/** * Downloads the image at the given URL if not present in cache or return the cached version otherwise. * * @param url The URL to the image * @param options A mask to specify options to use for this request * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. * * @return Returns an instance of SDWebImageCombinedOperation, which you can cancel the loading process. */ - (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nonnull SDInternalCompletionBlock)completedBlock; 复制代码
方法实现如下:
- (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 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. if ([url isKindOfClass:NSString.class]) { url = [NSURL URLWithString:(NSString *)url]; } // Prevents app crashing on argument type error like sending NSNull instead of NSURL if (![url isKindOfClass:NSURL.class]) { url = nil; } // 创建一个 SDWebImageCombinedOperation 实例 SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; operation.manager = self; // 查询该 url 是否是下载失败过的 url BOOL isFailedUrl = NO; if (url) { SD_LOCK(self.failedURLsLock); isFailedUrl = [self.failedURLs containsObject:url]; SD_UNLOCK(self.failedURLsLock); } // 如果 url 内容为空,或者没有设置失败重试且当前 url 下载失败过,走失败回调 if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) { [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url]; return operation; } // 把当前 operation 放入 runningOperations 集合中 SD_LOCK(self.runningOperationsLock); [self.runningOperations addObject:operation]; SD_UNLOCK(self.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; } 复制代码
SDWebImageCombinedOperation
SDWebImageCombinedOperation 是每次进行图片加载都会使用到的实例,其声明如下:
/** A combined operation representing the cache and loader operation. You can use it to cancel the load process. */ @interface SDWebImageCombinedOperation : NSObject /** Cancel the current operation, including cache and loader process */ - (void)cancel; /** The cache operation from the image cache query */ @property (strong, nonatomic, nullable, readonly) id cacheOperation; /** The loader operation from the image loader (such as download operation) */ @property (strong, nonatomic, nullable, readonly) id loaderOperation; @end 复制代码
其 cancel 方法实现如下:
- (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]; } } 复制代码
SDWebImageCombinedOperation 遵守了 SDWebImageOperation 协议,且包含了标示查找缓存的 cacheOperation 和下载的 loaderOperation,本质上就是模型类。
SD_LOCK & SD_UNLOCK
Objective-C 提供的集合类都不是线程安全的,所以需要加锁,SDWebImage 是通过 dispatch_semaphore_t 来当做锁使用的,比如:
@property (strong, nonatomic, nonnull) dispatch_semaphore_t failedURLsLock; // a lock to keep the access to `failedURLs` thread-safe @property (strong, nonatomic, nonnull) dispatch_semaphore_t runningOperationsLock; // a lock to keep the access to `runningOperations` thread-safe 复制代码
SD_LOCK 和 SD_UNLOCK 则是两个宏,用于配合 dispatch_semaphore_t 达到锁的调用方式和效果:
#ifndef SD_LOCK #define SD_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); #endif #ifndef SD_UNLOCK #define SD_UNLOCK(lock) dispatch_semaphore_signal(lock); #endif 复制代码
失败回调
当出现错误时,会调用 callCompletionBlockForOperation:completion:error:url:,如下:
- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation completion:(nullable SDInternalCompletionBlock)completionBlock error:(nullable NSError *)error url:(nullable NSURL *)url { [self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES url:url]; } - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation completion:(nullable SDInternalCompletionBlock)completionBlock image:(nullable UIImage *)image data:(nullable NSData *)data error:(nullable NSError *)error cacheType:(SDImageCacheType)cacheType finished:(BOOL)finished url:(nullable NSURL *)url { dispatch_main_async_safe(^{ if (operation && !operation.isCancelled && completionBlock) { completionBlock(image, data, error, cacheType, finished, url); } }); } 复制代码
SDWebImageOptionsResult
SDWebImageOptionsResult 是一个模型类,在这里是为了统一 options 和 context 并对其进行统一配置,如果 context 里有,就用 context 里的,如果没有就用 SDWebImageManager 里的。同时如果 optionsProcessor 存在,则使用 optionsProcessor 生成 SDWebImageOptionsResult 实例。
- (SDWebImageOptionsResult *)processedResultForURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context { SDWebImageOptionsResult *result; SDWebImageMutableContext *mutableContext = [SDWebImageMutableContext dictionary]; // Image Transformer from manager if (!context[SDWebImageContextImageTransformer]) { id transformer = self.transformer; [mutableContext setValue:transformer forKey:SDWebImageContextImageTransformer]; } // Cache key filter from manager if (!context[SDWebImageContextCacheKeyFilter]) { id cacheKeyFilter = self.cacheKeyFilter; [mutableContext setValue:cacheKeyFilter forKey:SDWebImageContextCacheKeyFilter]; } // Cache serializer from manager if (!context[SDWebImageContextCacheSerializer]) { id cacheSerializer = self.cacheSerializer; [mutableContext setValue:cacheSerializer forKey:SDWebImageContextCacheSerializer]; } if (mutableContext.count > 0) { if (context) { [mutableContext addEntriesFromDictionary:context]; } context = [mutableContext copy]; } // Apply options processor if (self.optionsProcessor) { result = [self.optionsProcessor processedResultForURL:url options:options context:context]; } if (!result) { // Use default options result result = [[SDWebImageOptionsResult alloc] initWithOptions:options context:context]; } return result; } 复制代码
optionsProcessor 是一个遵守了 SDWebImageOptionsProcessor 协议的处理模块,用于我们自定义配置。
开启加载图片
在方法的最后调用 callCacheProcessForOperation:url:options:context:progress:completed:,正式进入加载图片操作:
// Query 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 { // Check whether we should query cache BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0; if (shouldQueryCache) { id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; // 从 context 中取得 cacheKeyFilter NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; // 获取图片缓存的 key @weakify(operation); // 从 imageCache 中查找图片 operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) { @strongify(operation); if (!operation || operation.isCancelled) { // 如果 operation 为空或已经取消,直接从队列中移除 operation [self safelyRemoveOperationFromRunning:operation]; 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]; } } 复制代码
获取图片缓存的 key
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url cacheKeyFilter:(id)cacheKeyFilter { if (!url) { return @""; } if (cacheKeyFilter) { return [cacheKeyFilter cacheKeyForURL:url]; } else { return url.absoluteString; } } 复制代码
从操作队列中移除 operation
- (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation { if (!operation) { return; } SD_LOCK(self.runningOperationsLock); [self.runningOperations removeObject:operation]; SD_UNLOCK(self.runningOperationsLock); } 复制代码
下载图片
在从缓存中没能查找出图片的时候,会进入下载操作阶段,方法实现如下:
// Download process - (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 { // Check whether we should download image from network BOOL shouldDownload = (options & SDWebImageFromCacheOnly) == 0; // 查看是否设置了只从缓存中找 shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached); // 查看缓存图片是否为 nil,且设置了刷新缓存标志位 shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]); // 查看代理方法是否被实现并取得结果 shouldDownload &= [self.imageLoader canRequestImageForURL:url]; // 查看 URL 是否可以被请求 if (shouldDownload) { if (cachedImage && options & SDWebImageRefreshCached) { // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server. [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image. SDWebImageMutableContext *mutableContext; if (context) { mutableContext = [context mutableCopy]; } else { mutableContext = [NSMutableDictionary dictionary]; } mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage; context = [mutableContext copy]; } // `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle @weakify(operation); operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) { @strongify(operation); if (!operation || operation.isCancelled) { // Do nothing if the operation was cancelled // See #699 for more details // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) { // Image refresh hit the NSURLCache cache, do not call the completion block } else if (error) { [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url]; BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error]; if (shouldBlockFailedURL) { SD_LOCK(self.failedURLsLock); [self.failedURLs addObject:url]; SD_UNLOCK(self.failedURLsLock); } } else { if ((options & SDWebImageRetryFailed)) { SD_LOCK(self.failedURLsLock); [self.failedURLs removeObject:url]; SD_UNLOCK(self.failedURLsLock); } [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock]; } if (finished) { [self safelyRemoveOperationFromRunning:operation]; } }]; } else if (cachedImage) { [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; [self safelyRemoveOperationFromRunning:operation]; } else { // Image not in cache and download disallowed by delegate [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url]; [self safelyRemoveOperationFromRunning:operation]; } } 复制代码
主要内容就是启动下载,并根据结果走不同的路径来处理图片。如果走最主要的路径,则还要在下载完成后去进行图片存储。
图片存储
// Store cache process - (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 { // the target image store cache type SDImageCacheType storeCacheType = SDImageCacheTypeAll; if (context[SDWebImageContextStoreCacheType]) { storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue]; } // the original store image cache type SDImageCacheType originalStoreCacheType = SDImageCacheTypeNone; if (context[SDWebImageContextOriginalStoreCacheType]) { originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue]; } id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; id transformer = context[SDWebImageContextImageTransformer]; id cacheSerializer = context[SDWebImageContextCacheSerializer]; BOOL shouldTransformImage = downloadedImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer; BOOL shouldCacheOriginal = downloadedImage && finished; // if available, store original image to cache if (shouldCacheOriginal) { // normally use the store cache type, but if target image is transformed, use original store cache type instead SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType; if (cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) { // 如果 cacheSerializer 不为空,则在全局并发队列中处理存储操作 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ @autoreleasepool { NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url]; [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType completion:nil]; } }); } else { [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType completion:nil]; } } // if available, store transformed image to cache if (shouldTransformImage) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ @autoreleasepool { UIImage *transformedImage = [transformer transformedImageWithImage:downloadedImage forKey:key]; // 使用 transformer 处理图片之后再进行存储 if (transformedImage && finished) { NSString *transformerKey = [transformer transformerKey]; NSString *cacheKey = SDTransformedKeyForKey(key, transformerKey); BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage]; NSData *cacheData; // pass nil if the image was transformed, so we can recalculate the data from the image if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) { cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : downloadedData) imageURL:url]; } else { cacheData = (imageWasTransformed ? nil : downloadedData); } [self.imageCache storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType completion:nil]; } [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; } }); } else { [self callCompletionBlockForOperation:operation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; } } 复制代码
SDImageTransformer
SDImageTransformer 是一个协议,声明如下:
/** A transformer protocol to transform the image load from cache or from download. You can provide transformer to cache and manager (Through the `transformer` property or context option `SDWebImageContextImageTransformer`). @note The transform process is called from a global queue in order to not to block the main queue. */ @protocol SDImageTransformer @required /** For each transformer, it must contains its cache key to used to store the image cache or query from the cache. This key will be appened after the original cache key generated by URL or from user. @return The cache key to appended after the original cache key. Should not be nil. */ @property (nonatomic, copy, readonly, nonnull) NSString *transformerKey; /** Transform the image to another image. @param image The image to be transformed @param key The cache key associated to the image @return The transformed image, or nil if transform failed */ - (nullable UIImage *)transformedImageWithImage:(nonnull UIImage *)image forKey:(nonnull NSString *)key; @end 复制代码
在图片下载完成后,我们可能希望对将要缓存的图片做一些处理,比如下载的是头像数据,可能会直接切成圆角存储起来,这样避免了以后每次使用图片都要设置带来的困扰。SDWebImage 框架为我们默认实现了一些,例如 SDImagePipelineTransformer,用于将多个 Transformer 当做渲染管线使用。还有 SDImageRoundCornerTransformer,用于对下载好的图片进行圆角处理等等。
SDWebImageCacheSerializer
SDWebImageCacheSerializer 也是一个协议,可以对即将缓存的图片的数据进行一些处理。
/** This is the protocol for cache serializer. We can use a block to specify the cache serializer. But Using protocol can make this extensible, and allow Swift user to use it easily instead of using `@convention(block)` to store a block into context options. */ @protocol SDWebImageCacheSerializer - (nullable NSData *)cacheDataWithImage:(nonnull UIImage *)image originalData:(nullable NSData *)data imageURL:(nullable NSURL *)imageURL; @end 复制代码
以上就是 Manager 主流程中的所有代码了。