书接上文,在经过 WebCache Categories 调用了 sd_setImageWithURL:placeholderImage: 等方法后,都会汇总到 SDWebImageManager 的 loadImageWithURL:options:context:progress:completed: 方法中。SDWebImageManager 类是整个 SDWebImage 的核心管理类,负责协调下载和缓存的关系,接下来咱们就一步步的分析其源代码,探究其实现。git
SDWebImageManager 的指定初始化方法声明和实现以下:github
/** * 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<SDImageCache>)cache loader:(nonnull id<SDImageLoader>)loader NS_DESIGNATED_INITIALIZER;
- (nonnull instancetype)initWithCache:(nonnull id<SDImageCache>)cache loader:(nonnull id<SDImageLoader>)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<SDImageCache> cache = [[self class] defaultImageCache];
if (!cache) {
cache = [SDImageCache sharedImageCache];
}
id<SDImageLoader> 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<SDImageCache> 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<SDImageLoader> defaultImageLoader;
static id<SDImageCache> _defaultImageCache;
static id<SDImageLoader> _defaultImageLoader;
+ (id<SDImageCache>)defaultImageCache {
return _defaultImageCache;
}
+ (void)setDefaultImageCache:(id<SDImageCache>)defaultImageCache {
if (defaultImageCache && ![defaultImageCache conformsToProtocol:@protocol(SDImageCache)]) {
return;
}
_defaultImageCache = defaultImageCache;
}
+ (id<SDImageLoader>)defaultImageLoader {
return _defaultImageLoader;
}
+ (void)setDefaultImageLoader:(id<SDImageLoader>)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;
复制代码
方法实现以下:app
- (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 是每次进行图片加载都会使用到的实例,其声明以下:框架
/** A combined operation representing the cache and loader operation. You can use it to cancel the load process. */
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
/** 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<SDWebImageOperation> cacheOperation;
/** The loader operation from the image loader (such as download operation) */
@property (strong, nonatomic, nullable, readonly) id<SDWebImageOperation> loaderOperation;
@end
复制代码
其 cancel 方法实现以下:less
- (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,本质上就是模型类。dom
Objective-C 提供的集合类都不是线程安全的,因此须要加锁,SDWebImage 是经过 dispatch_semaphore_t 来当作锁使用的,好比:async
@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 是一个模型类,在这里是为了统一 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<SDImageTransformer> transformer = self.transformer;
[mutableContext setValue:transformer forKey:SDWebImageContextImageTransformer];
}
// Cache key filter from manager
if (!context[SDWebImageContextCacheKeyFilter]) {
id<SDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
[mutableContext setValue:cacheKeyFilter forKey:SDWebImageContextCacheKeyFilter];
}
// Cache serializer from manager
if (!context[SDWebImageContextCacheSerializer]) {
id<SDWebImageCacheSerializer> 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<SDWebImageCacheKeyFilter> 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];
}
}
复制代码
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url cacheKeyFilter:(id<SDWebImageCacheKeyFilter>)cacheKeyFilter {
if (!url) {
return @"";
}
if (cacheKeyFilter) {
return [cacheKeyFilter cacheKeyForURL:url];
} else {
return url.absoluteString;
}
}
复制代码
- (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<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
id<SDWebImageCacheSerializer> 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 是一个协议,声明以下:
/** 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 <NSObject>
@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 也是一个协议,能够对即将缓存的图片的数据进行一些处理。
/** 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 <NSObject>
- (nullable NSData *)cacheDataWithImage:(nonnull UIImage *)image originalData:(nullable NSData *)data imageURL:(nullable NSURL *)imageURL;
@end
复制代码
以上就是 Manager 主流程中的全部代码了。