SDWebImage源码学习笔记

1. 前言

大名鼎鼎SDWebImage不用多说,相信每一个iOS程序员或多或少都有了解。比如我,之前就大概只知道是个什么东西,基本属于没用过的状态。最近抽空学习了一下源码,在此记录下。

在GitHub上,SDWebImage描述为Asynchronous image downloader with cache support as a UIImageView category,翻译成中文是“UIImageView的一个category,支持缓存的异步图片下载器”。

可以在该链接中查看到最新的文档https://sdwebimage.github.io

本文使用的源码为SDWebImage 5.0+版本:

2. 架构

在GitHub上,SDWebImage提供了非常详细的架构图、类图和顺序图,其中下图是整体的架构图

SDWebImageHighLevelDiagram.jpeg

这个图中可以看到总体包括以下几个部分

  • 基础组件:包括工具类、分类方法、Image Coder(图片编码/解码)、Image Transformer(图片转换)
  • 顶层组件
    • Image Manager:负责处理Image Cache(处理图片缓存和落地)和Image Loader(处理图片的网络加载)
    • View Category:提供对外的API接口,图片加载动画和转场动画等。
    • Image Prefetcher:图片的预加载器,是相对比较独立的部分。

可以看到,SDWebImage提供了图片缓存的能力、网络加载的能力,还包括一些图片的处理。

顺序图

SDWebImageSequenceDiagram.jpg

通过顺序图,可以清楚的看到整个接口的调用流程。

  1. 需要加载图片时,Other Object只需要调用``UIImageView+WebCahce中的sd_setImageWithURL()`方法即可
  2. sd_setImageWithURL()会调用UIVIew+WebCache中的内部加载方法sd_internalSetImageWithURL()
  3. 接下来会调用SDWebImageManagerloadImage()方法,可以看到,主要的逻辑都在这个SDWebImageManager
  4. SDWebImageManager会分别调用SDImageCache加载缓存数据,然后调用SDWebImageDownloader从网络中加载图片
  5. 加载完成后,会回调回UIImageView中,设置图片

对于使用者来说,复杂的逻辑都隐藏在SDWebImageManager之后,还有一些更详细的类图,有兴趣的可以直接到GitHub的ReadMe去查看。

3. View Category

3.1 WebCache

SDWebImage提供了以下几个Category可以方便的完成图片加载

  • UIImageView+HighlightedWebCache
  • UIImageView+WebCache
  • UIButton+WebCache
  • NSButton+WebCache
  • UIView+WebCache

主要的处理逻辑,最终都会调用UIView+WebCache的下述接口:

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock;

该方法非常长,主要的流程如下:

  1. 取消在进行的operation
    • operation存储在由UIView+WebCacheOperation中维护的字典SDOperationsDictionary中,默认使用当前类名作为operation的key,其中value是weak指针,因为该operationSDWebImageManager维护
  2. 若外部没有设置SDWebImageDelayPlaceholder,则异步在主线程将placeholder设置到UIImageView
  3. 重置记录进度的NSProgress对象,该对象由当前分类实例维护
  4. 启动ImageIndicator,默认是一个旋转菊花,其中iWatch是不支持的
  5. 接下来就是获取SDWebImageManager了,可以支持外部配置,否则会使用全局唯一的单例
  6. 设置进度的回调SDImageLoaderProgressBlock,该block中,会更新内部的进度条、菊花,然后再回调给外层调用者
  7. 调用SDWebImageManager的加载方法loadImageWithURL:options:context:progress:completed:,启动图片的加载流程
  8. 在7中方法的completed回调中,完成进度更新、关闭菊花、回调completedBlock以及设置图片等操作

4. SDWebImageManager

SDWebImageManager是一个单例类,维护了两个主要的对象imageCacheimageLoader:

@property (strong, nonatomic, readonly, nonnull) id imageCache;
@property (strong, nonatomic, readonly, nonnull) id imageLoader;

4.1 加载图片前的准备工作

主要接口loadImageWithURL:options:context:progress:completed:的实现逻辑如下:

  1. 兼容逻辑,若传进来的url是NSString而不是NSURL,则转换为NSURL
  2. 创建一个新的SDWebImageCombinedOperation
  3. 判断是否是已经失败且不需要重试的url或者url无效,直接回调completedBlock返回
  4. operation加入到Set runningOperations
  5. 在执行加载操作前,调用processedResultForURL方法,对urloptionscontext做一次加工操作
    • 在该方法中,SDWebImageManager设置会判断是否外部有设置transformercacheKeyFiltercacheSerializer
    • 最后,会调用外部配置的optionsProcessor对象的processedResultForURL方法,让使用者有机会修改上述参数
  6. 调用callCacheProcessForOperation方法,开始从缓存中加载图片

关键代码,代码中只保留关键逻辑:

- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock {
    // 1
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
    // 2
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;
    // 3
    BOOL isFailedUrl = NO;
    if (url) {
        SD_LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        SD_UNLOCK(self.failedURLsLock);
    }

    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;
    }
    // 4
    SD_LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    SD_UNLOCK(self.runningOperationsLock);
    // 5
    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
    // 6
    [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];

    return operation;
}

4.2 从缓存中加载图片

接口名称为callCacheProcessForOperation,该方法中

  1. 判断context中是否传入了自定义的SDImageCache,否则使用默认的imageCache
  2. 判断options是否配置了SDWebImageFromLoaderOnly,该参数表明,是否仅从网络加载
  3. 若仅从网络中加载,直接调用callDownloadProcessForOperation接口,开始下载的步骤
  4. 否则,获取url对应的key,并调用imageCache的接口queryImageForKey,从缓存中加载图片,在该接口回调中,调用3中的下载接口。

关键代码如下:

- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock {
    // 1
    id imageCache;
    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
        imageCache = context[SDWebImageContextImageCache];
    } else {
        imageCache = self.imageCache;
    }
    // 2
    BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
    if (shouldQueryCache) {
        // 4
        NSString *key = [self cacheKeyForURL:url context:context];
        @weakify(operation);
        operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context 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;
            }
            // Continue download process
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {
        // 3
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
    }
}

从网络中加载图片

接口名为callDownloadProcessForOperation,实现逻辑如下:

  1. 与SDImageCache类似,SDImageLoader也支持外部配置,否则使用默认的imageLoader
  2. 一系列参数判断,主要为了判断是否可以下载
  3. 当有图片时,在该方法中可能会先通过callCompletionBlockForOperation接口,异步回调completedBlock设置已经加载好的图片
  4. 当判断可以下载后,会调用imageLoaderrequestImageWithURL接口,启动下载
  5. requestImageWithURL的回调中,处理一些失败等异常逻辑。
  6. 若加载成功,则通过callStoreCacheProcessForOperation接口,将下载的图片缓存到本地
  7. 当不需要下载时,会直接返回,若有缓存则会带上缓存的图片。

关键代码:

- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                    url:(nonnull NSURL *)url
                                options:(SDWebImageOptions)options
                                context:(SDWebImageContext *)context
                            cachedImage:(nullable UIImage *)cachedImage
                             cachedData:(nullable NSData *)cachedData
                              cacheType:(SDImageCacheType)cacheType
                               progress:(nullable SDImageLoaderProgressBlock)progressBlock
                              completed:(nullable SDInternalCompletionBlock)completedBlock {
    // 1
    id imageLoader;
    if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
        imageLoader = context[SDWebImageContextImageLoader];
    } else {
        imageLoader = self.imageLoader;
    }
    // 2
    BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
    shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
    shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
    shouldDownload &= [imageLoader canRequestImageForURL:url];
    if (shouldDownload) {
        if (cachedImage && options & SDWebImageRefreshCached) {
            // 3
            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            // 将cachedImage传到image loader中用于检查是否是相同的图片
            mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
            context = [mutableContext copy];
        }
        // 4
        @weakify(operation);
        operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
            @strongify(operation);
            // 5
            if {
              // 一系列失败逻辑
            } else {
                // 6
                [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
            }
        }];
    } else if (cachedImage) { // 7
        [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
        [self safelyRemoveOperationFromRunning:operation];
    } else {
        [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
        [self safelyRemoveOperationFromRunning:operation];
    }
}

该方法中第6步从网络拉取成功后,会调用callStoreCacheProcessForOperation方法将图片缓存到本地,以及通过调用者提供的SDImageTransformer转换图片。

缓存图片

调用者提供两种自定义操作:

  • 自定义的SDImageTransformer将图片转换成另一个图片
  • 自定义的SDWebImageCacheSerializer将图片序列化为NSData

具体逻辑如下:

  1. 如果有提供SDWebImageCacheSerializer,则会先调用接口将图片序列化之后,再调用存储接口缓存图片。注意这一步是放在global_queue中执行的,不会阻塞主线程,同时使用autoreleasepool保证NSData能第一时间释放
  2. 第1步结束后,调用storeImage接口,通过imageCache对象将图片缓存到本地。默认该操作是放在imageCache维护的io队列中执行的。
  3. 最后一步操作,则是调用callTransformProcessForOperation接口,转换图片。

关键代码:

- (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 {
    // 默认拉回来的图片就是originImage,当提供了transformer转化图片时,可以选择将原图片和转换后的图片都缓存起来
    NSString *key = [self cacheKeyForURL:url context:context];
    id transformer = context[SDWebImageContextImageTransformer];
    id cacheSerializer = context[SDWebImageContextCacheSerializer];

    // 这里会缓存原图,如果转换只要完成下载,始终缓存原图
    if (shouldCacheOriginal) {
        SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
        if (cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) {
            // 1 放到全局队列中异步序列化
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                @autoreleasepool {
                    NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
                    [self storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
                        [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
                    }];
                }
            });
        } else {
            // 2
            [self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
                [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
            }];
        }
    } else {
        [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
    }
}

转换图片

如果外部有设置SDImageTransformer,则会判断是否需要将转换后的图片也缓存起来,关键代码:

- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                     url:(nonnull NSURL *)url
                                 options:(SDWebImageOptions)options
                                 context:(SDWebImageContext *)context
                           originalImage:(nullable UIImage *)originalImage
                            originalData:(nullable NSData *)originalData
                                finished:(BOOL)finished
                                progress:(nullable SDImageLoaderProgressBlock)progressBlock
                               completed:(nullable SDInternalCompletionBlock)completedBlock {
    // the target image store cache type

    NSString *key = [self cacheKeyForURL:url context:context];
    id transformer = context[SDWebImageContextImageTransformer];
    id cacheSerializer = context[SDWebImageContextCacheSerializer];
    
    if (shouldTransformImage) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            @autoreleasepool {
                UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
                if (transformedImage && finished) {
                    if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
                        cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : originalData) imageURL:url];
                    } else {
                        cacheData = (imageWasTransformed ? nil : originalData);
                    }
                    // keep the original image format and extended data
                    SDImageCopyAssociatedObject(originalImage, transformedImage);
                    [self storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType options:options context:context completion:^{
                        [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }];
                } else {
                    [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                }
            }
        });
    } else {
        [self callCompletionBlockForOperation:operation completion:completedBlock image:originalImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
    }
}

加载完成

一切完成后,会通过callCompletionBlockForOperation回调到最外层的调用者。

5. SDImageCache

关于SDIMageCache,可以直接看以下类图,用协议定义了所有关键类,包括SDImageCacheSDMemoryCacheSDDiskCache

SDWebImageCacheClassDiagram.png

5.1 SDImageCache

  • 持有SDMemoryCacheSDDiskCache,用于从内存和硬盘中加载图片。可以通过SDImageCacheConfig配置我们自定义实现的Cache
  • 维护了一个io队列,所有从硬盘中异步读取内容的操作均通过该io队列执行
  • 监听了App进程被系统杀掉和App切换到后台的通知,清除过期的数据

获取图片接口queryCacheOperationForKey

  1. 首先判断外部是否有传入transformer对象,若有,则会将key通过SDTransformedKeyForKey接口将keytranformerKey拼接在一起得到新的key

  2. 通过memoryCache从内存中获取图片,默认情况下,如果获取到图片,则直接返回

  3. 若设置了SDImageCacheQueryMemoryData参数,则仍然从硬盘中加载图片的data数据。默认异步从硬盘加载,可通过设置参数同步加载

  4. 加载完成后,通过block同步或异步返回

有两处细节需要注意:

  • 使用@autoreleasepool保证大的内存占用可以快速释放
  • 异步加载时,使用io队列。异步回调block时,使用主线程回调

关键代码:

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
    // 1
    id transformer = context[SDWebImageContextImageTransformer];
    if (transformer) 
        NSString *transformerKey = [transformer transformerKey];
        key = SDTransformedKeyForKey(key, transformerKey);
    }
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    
    if (image) {
          // 处理SDImageCacheDecodeFirstFrameOnly或SDImageCacheMatchAnimatedImageClass的逻辑
    }

    // 2
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    
    // 3
    NSOperation *operation = [NSOperation new];
    // 检查是否需要同步查询disk
    // 1. 内存缓存命中且设置了同步
    // 2. 内存缓存没有命中但设置了同步读取硬盘数据
    BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                                (!image && options & SDImageCacheQueryDiskDataSync));
    void(^queryDiskBlock)(void) =  ^{
        if (operation.isCancelled) {
            if (doneBlock) {
                doneBlock(nil, nil, SDImageCacheTypeNone);
            }
            return;
        }
        
        @autoreleasepool {
            // 从硬盘中加载图片的data
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeNone;
            if (image) { // 内存中已经有图片,但是需要图片data
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                cacheType = SDImageCacheTypeDisk;
                // 将imageData转换成image
                diskImage = [self diskImageForKey:key data:diskData options:options context:context];
                // 将图片缓存到内存中
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    NSUInteger cost = diskImage.sd_memoryCost;
                    [self.memoryCache setObject:diskImage forKey:key cost:cost];
                }
            }
            
            if (doneBlock) {
                if (shouldQueryDiskSync) {
                    doneBlock(diskImage, diskData, cacheType);
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, cacheType);
                    });
                }
            }
        }
    };
    
    // 4
    if (shouldQueryDiskSync) {
        dispatch_sync(self.ioQueue, queryDiskBlock);
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    
    return operation;
}

存储图片接口storeImage

外部可设置不同的SDImageCacheType,决定是否需要缓存到内存以及硬盘中

  1. 内存缓存:根据shouldCacheImagesInMemory接口判断是否要缓存到内存中

  2. 硬盘缓存:

    1. 首次将图片转换为NSData,使用SDAnimatedImage接口或者SDImageCodersManager将图片转化为NSData
    2. 通过diskCache存储NSData到硬盘中
    3. 检查图片是否有sd_extendedObject,如果有则也会存储到硬盘中,使用了NSKeyedArchiversd_extendedObject转换为NSData
      1. NSKeyedArchiver的在iOS 11上提供了新的接口archivedDataWithRootObject:requiringSecureCoding:error
      2. 这里为了兼容iOS 11以下的系统,使用了旧的接口archivedDataWithRootObject:,通过clang diagnostic ignored "-Wincompatible-pointer-types"屏蔽了方法过期警告;使用try catch捕获异常
      3. 通过diskCachesetExtendedData将扩展数据存储到硬盘中

关键代码:

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
          toMemory:(BOOL)toMemory
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {

    // 1 
    if (toMemory && self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = image.sd_memoryCost;
        [self.memoryCache setObject:image forKey:key cost:cost];
    }
    // 2
    if (toDisk) {
        // 使用iO队列异步存储
        dispatch_async(self.ioQueue, ^{
            @autoreleasepool {
                // 2.1
                NSData *data = imageData;
                if (!data && image) {
                    data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
                }
                // 2.2
                [self _storeImageDataToDisk:data forKey:key];
                if (image) {
                    // 2.3
                    id extendedObject = image.sd_extendedObject;
                    if ([extendedObject conformsToProtocol:@protocol(NSCoding)]) {
                        NSData *extendedData;
                        // 2.3.1
                        if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) {
                            NSError *error;
                            extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject requiringSecureCoding:NO error:&error];
                            if (error) {
                                NSLog(@"NSKeyedArchiver archive failed with error: %@", error);
                            }
                        } else {
                            // 2.3.2
                            @try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
                                extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject];
#pragma clang diagnostic pop
                            } @catch (NSException *exception) {
                                NSLog(@"NSKeyedArchiver archive failed with exception: %@", exception);
                            }
                        }
                        if (extendedData) { // 2.3.4
                            [self.diskCache setExtendedData:extendedData forKey:key];
                        }
                    }
                }
            }
            
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}

5.2 SDMemoryCache

SDMemoryCache继承自NSCache,其内部做了如下一些事情:

持有一个NSMapTableweakCache

weakCachekeystrong类型,valueweak类型,在缓存图片时,weakCache也会缓存一份图片的keyvalue

这么做的目的是,当NSCache因内存警告清除了缓存内容后,如果有图片在App某些地方仍然被引用,那么就可以通过weakCache来快速加入到NSCache中,从而阻止了重复从硬盘中读取。

weakCacheLock

使用了GCD的dispatch_semaphore_t信号量方式,保证多线程操作weakCache时的安全性。

5.3 SDDiskCache

SDDiskCache内部通过NSFileManager实现了文件的读写。值得注意的是

  • 存储文件到硬盘时,SDDiskCache会将存储的key转换成md5值后存入本地。
  • 清理过期数据逻辑,总共分两个步骤
    • 第一个步骤:根据SDImageCacheConfigExpireType设定的排序依据,删除超过设定的过期时间的文件。
    • 在遍历所有文件时,计算当前存储文件的总大小。
    • 第二个步骤:当存储的总大小超过设定的总大小时,按照SDImageCacheConfigExpireType设定的时间排序,删除文件,直到设定大小的1/2为止。
    • 清除文件的时机是在App退出或退到后台时,由SDImageCache调用。
  • 存储extendData:使用了系统的库,通过setxattrgetxattrremovexattr实现了extendData的设置、读取、移除操作。

清理过期数据的关键代码:

- (void)removeExpiredData {

    NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                               includingPropertiesForKeys:resourceKeys
                                                                  options:NSDirectoryEnumerationSkipsHiddenFiles
                                                             errorHandler:NULL];
    
    NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
    for (NSURL *fileURL in fileEnumerator) {
        
        NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
        // 删除过期的文件
        NSDate *modifiedDate = resourceValues[cacheContentDateKey];
        if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
            [urlsToDelete addObject:fileURL];
            continue;
        }
        
        // 存储文件属性为后边的文件大小检查做准备
        NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
        currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
        cacheFiles[fileURL] = resourceValues;
    }
    
    for (NSURL *fileURL in urlsToDelete) {
        [self.fileManager removeItemAtURL:fileURL error:nil];
    }
    
    // 若剩余的文件大小仍然超过了设定的最大值,那么执行第二步步骤。优先删除更早的文件
    NSUInteger maxDiskSize = self.config.maxDiskSize;
    if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
        // 目标是删除到最大值的一半
        const NSUInteger desiredCacheSize = maxDiskSize / 2;
        // 按时间排序
        NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent                                                 usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                     return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
                                                                 }];
        
        // 删除文件直到剩余大小是最大值的一半
        for (NSURL *fileURL in sortedFiles) {
            if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
                NSDictionary *resourceValues = cacheFiles[fileURL];
                NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
                
                if (currentCacheSize < desiredCacheSize) {
                    break;
                }
            }
        }
    }
}

6. SDImageLoader

SDImageLoader的类图如下,该模块主要处理网络请求逻辑。

SDWebImageLoaderClassDiagram.png

6.1 SDWebImageDownloader

SDWebImageDownloaderSDWebImage提供的图片下载器类,实现了SDImageLoader协议。提供了一些配置参数以及多个下载图片接口。

@interface SDWebImageDownloader : NSObject

@property (nonatomic, copy, readonly) SDWebImageDownloaderConfig *config;
@property (nonatomic, strong) id requestModifier;
@property (nonatomic, strong) id responseModifier;
@property (nonatomic, strong) id decryptor;
@property (nonatomic, readonly) NSURLSessionConfiguration *sessionConfiguration;

- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options context:(SDWebImageContext *)context progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

#pragma mark - Protocol

- (id)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock;

一些关键的参数如下:

  • downloadQueueNSOperationQueue类型,用于执行每一个下载任务创建的NSOperation

  • URLOperations:字典类型,key为URL,valueNSOperation,使用该对象来维护SDWebImageDownloader生命周期内所有网络请求的Operation对象。

  • session:使用外部或者默认的sessionConfiguration创建的NSURLSession对象。

图片下载核心流程

核心图片下载方法为downloadImageWithURL,主要流程如下:

  1. 判断URLOperations是否已经缓存该url对应的NSOperation对象

  2. 若已经存在operation,将该方法传入的progressBlockcompletedBlock加入到operation中,同时若该operation还未被执行时,会根据传入的options调整当前queue的优先级。

  3. operation不存在、已经完成或者被取消,通过createDownloaderOperationWithUrl方法创建一个新的operation

  4. operation创建成功,设置completionBlock,添加operationURLOperations中,调用addHandlersForProgress添加progressBlockcompletedBlock,最后,将operation添加到downloadQueue(根据苹果文档,在添加operationqueue之前,需要执行完所有配置)。

  5. 最后,创建并返回SDWebImageDownloadToken对象,该对象包含了urlrequest、以及downloadOperationCancelToken。`

关键代码:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {

    id downloadOperationCancelToken;
    // 1
    NSOperation *operation = [self.URLOperations objectForKey:url];
    // 3
    if (!operation || operation.isFinished || operation.isCancelled) {
        operation = [self createDownloaderOperationWithUrl:url options:options context:context];
        @weakify(self);
        operation.completionBlock = ^{
            @strongify(self);
            if (!self) {
                return;
            }
            [self.URLOperations removeObjectForKey:url];
        };
        self.URLOperations[url] = operation;
        // 4
        downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        [self.downloadQueue addOperation:operation];
    } else { // 2
        @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;
            }
        }
    }
    // 5
    SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
    token.url = url;
    token.request = operation.request;
    token.downloadOperationCancelToken = downloadOperationCancelToken;
    
    return token;
}

SDWebImageDownloadToken实现了SDWebImageOperation协议,对于外部调用者来说,可以通过id取消当前操作,定义如下:

@interface SDWebImageDownloadToken : NSObject 

- (void)cancel;
@property (nonatomic, strong, nullable, readonly) NSURL *url;
@property (nonatomic, strong, nullable, readonly) NSURLRequest *request;
@property (nonatomic, strong, nullable, readonly) NSURLResponse *response;
@property (nonatomic, strong, nullable, readonly) NSURLSessionTaskMetrics *metrics;

@end

创建NSOperation对象

NSOperation通过createDownloaderOperationWithUrl方法创建,主要流程如下:

  1. 创建NSMutableURLRequest对象,设置缓存策略、是否使用默认Cookies 、Http头信息等。
  2. 获取外部配置的SDWebImageDownloaderRequestModifier对象,若没有则使用self的,通过modifiedRequestWithRequest接口在请求之前有机会检查并修改一次Request,若返回了nil,本次请求会终止。
  3. 外部同样可以配置SDWebImageDownloaderResponseModifier对象,用来修改Response,这个会先存储在context中,等待请求回来后再去调用。
  4. 获取SDWebImageDownloaderDecryptor对象,同样是请求回来后,用于解密相关操作。
  5. context参数检查完毕后,需要创建NSOperation对象,此处可以通过设置configoperationClass来传入自定义的类名,若外部没有传入,则会使用SDWebImage提供改的SDWebImageDownloaderOperation类。
  6. 设置http请求的证书,首先获取config中的urlCredential,其次通过config中的usrnamepassword创建NSURLCredential对象。
  7. 设置其他参数,如http请求的证书、最小进度间隔、当前请求的优先级等。
  8. 如果设置了SDWebImageDownloaderLIFOExecutionOrder,表明所有的请求都是LIFO(后进先出)的执行方式,此处的处理方式是遍历当前downloadQueueoperations,将新的operation设置为所有operations的依赖,代码如下:

关键代码:

- (nullable NSOperation *)createDownloaderOperationWithUrl:(nonnull NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context {
    NSTimeInterval timeoutInterval = self.config.downloadTimeout;
    
    // 1
    NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData; // 默认情况下不使用NSURLCache
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
    mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
    mutableRequest.HTTPShouldUsePipelining = YES;
    mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
   
    // 2
    id requestModifier;
    if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
        requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
    } else {
        requestModifier = self.requestModifier;
    }
    NSURLRequest *request;
    if (requestModifier) {
        NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]];
    } else {
        request = [mutableRequest copy];
    }
    // 3
    id responseModifier;
    if ([context valueForKey:SDWebImageContextDownloadResponseModifier]) {
        responseModifier = [context valueForKey:SDWebImageContextDownloadResponseModifier];
    } else {
        responseModifier = self.responseModifier;
    }
    if (responseModifier) {
        mutableContext[SDWebImageContextDownloadResponseModifier] = responseModifier;
    }
    // 4
    id decryptor;
    if ([context valueForKey:SDWebImageContextDownloadDecryptor]) {
        decryptor = [context valueForKey:SDWebImageContextDownloadDecryptor];
    } else {
        decryptor = self.decryptor;
    }
    if (decryptor) {
        mutableContext[SDWebImageContextDownloadDecryptor] = decryptor;
    }
    
    context = [mutableContext copy];
    
    // 5
    Class operationClass = self.config.operationClass;
    if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
    } else {
        operationClass = [SDWebImageDownloaderOperation class];
    }
    NSOperation *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
    // 6
    if ([operation respondsToSelector:@selector(setCredential:)]) {
        if (self.config.urlCredential) {
            operation.credential = self.config.urlCredential;
        } else if (self.config.username && self.config.password) {
            operation.credential = [NSURLCredential credentialWithUser:self.config.username password:self.config.password persistence:NSURLCredentialPersistenceForSession];
        }
    }
    // 7
    if ([operation respondsToSelector:@selector(setMinimumProgressInterval:)]) {
        operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1);
    }
    
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    // 8
    if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
        for (NSOperation *pendingOperation in self.downloadQueue.operations) {
            [pendingOperation addDependency:operation];
        }
    }
    
    return operation;
}

6.2 SDWebImageDownloaderOperation

在前边的SDWebImageDownloader初始化时,可以看到创建了NSURLSession对象,且delegate设置的为self,但实际上,当SDWebImageDownloader接收到NSURLSessionTaskDelegate或者NSURLSessionDataDelegate回调时,都会转发到对应的NSOperation对象去处理,默认情况,就是SDWebImageDownloaderOperation。来看看这里的主要流程吧。

启动方法start

  1. 通过beginBackgroundTaskWithExpirationHandler方法申请在进入后台后,更多的时间执行下载任务。

  2. 判断session,该类中有两个sessionunownedSession(外部传入),ownedSession(内部创建),当外部没有传入session时,内部则会再创建一个,保证任务可以继续执行。

  3. 保存缓存数据,如果设置了SDWebImageDownloaderIgnoreCachedResponse时,当拉取回来的数据和已缓存的数据一致,就回调上层nil,这里保存的缓存数据用于拉取结束后的判断。

  4. 通过dataTaskWithRequest创建NSURLSessionTask对象dataTask

  5. 设置dataTaskcoderQueue的优先级。

  6. 启动本次任务,通过progressBlock回调当前进度,这里block可以存储多个,外部通过addHandlersForProgress方法添加。

  7. 这里还会再在主线程抛一个启动的通知SDWebImageDownloadStartNotification

关键代码

- (void)start {
        // 1
#if SD_UIKIT
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak typeof(self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                [wself cancel];
            }];
        }
#endif
        // 2
        NSURLSession *session = self.unownedSession;
        if (!session) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:nil];
            self.ownedSession = session;
        }
        // 3
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            NSURLCache *URLCache = session.configuration.URLCache;
            if (!URLCache) {
                URLCache = [NSURLCache sharedURLCache];
            }
            NSCachedURLResponse *cachedResponse;
            @synchronized (URLCache) {
                cachedResponse = [URLCache cachedResponseForRequest:self.request];
            }
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        // 4
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    // 5
    if (self.dataTask) {
        if (self.options & SDWebImageDownloaderHighPriority) {
            self.dataTask.priority = NSURLSessionTaskPriorityHigh;
            self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
        } else if (self.options & SDWebImageDownloaderLowPriority) {
            self.dataTask.priority = NSURLSessionTaskPriorityLow;
            self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
        } else {
            self.dataTask.priority = NSURLSessionTaskPriorityDefault;
            self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
        }
        // 6
        [self.dataTask resume];
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        __block typeof(self) strongSelf = self;
        // 7
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
        });
    } else {
        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
        [self done];
    }
}

NSURLSessionTaskDelegateNSURLSessionDataDelegate

SDWebImageDownloaderOperation中,实现了NSURLSessiondelegate回调处理,具体逻辑比较多且不复杂,就不在这里赘述,可自行查阅代码。

7. 一些细节

7.1 宏定义

多平台适配

SDWebImage中多处地方使用了平台宏去区分不同平台的特性,对于想要了解跨平台的一些特性,非常有借鉴意义。

// iOS and tvOS are very similar, UIKit exists on both platforms
// Note: watchOS also has UIKit, but it's very limited
#if TARGET_OS_IOS || TARGET_OS_TV
    #define SD_UIKIT 1
#else
    #define SD_UIKIT 0
#endif

#if TARGET_OS_IOS
    #define SD_IOS 1
#else
    #define SD_IOS 0
#endif

#if TARGET_OS_TV
    #define SD_TV 1
#else
    #define SD_TV 0
#endif

#if TARGET_OS_WATCH
    #define SD_WATCH 1
#else
    #define SD_WATCH 0
#endif

以及通过宏将Mac平台的NSImage声明为UIImageNSImageView声明为UIImageView等,让一套代码得以方便你的适配多个平台不同的控件名称。

#if SD_MAC
    #import 
    #ifndef UIImage
        #define UIImage NSImage
    #endif
    #ifndef UIImageView
        #define UIImageView NSImageView
    #endif
    #ifndef UIView
        #define UIView NSView
    #endif
    #ifndef UIColor
        #define UIColor NSColor
    #endif
#else
    #if SD_UIKIT
        #import 
    #endif
    #if SD_WATCH
        #import 
        #ifndef UIView
            #define UIView WKInterfaceObject
        #endif
        #ifndef UIImageView
            #define UIImageView WKInterfaceImage
        #endif
    #endif
#endif

判断是否主线程dispatch_main_async_safe

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

一般情况下,需要判断是否在主线程,可能会使用NSThread.isMainThread来判断,这个就可以满足大部分的场景了。而SDWebImage的实现有些不一样,判断的方式是当前的queue是否是主队列,并没有判断当前的线程。

实际上,主线程和主队列不完全是一个东西,有微小的区别。主线程上也可以运行其他队列。

在这篇OpenRadar中有提到,在主线程但非主队列中调用MKMapViewaddOverlay方法是不安全的。具体可参考下列文章:

  • GCD's Main Queue vs. Main Thread
  • iOS知识小集之main-queue!=main-thread

7.2 多线程安全

在代码中有大量的地方使用了锁去保证多线程安全,包括常见的@synchonzied以及GCD的信号量

#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

8. 结语

SDWebImage代码暂时就讲解这么多,不过该库的功能远不止于此,非常强大,对于有需要使用的,可以再详细的去了解具体使用的地方。

你可能感兴趣的:(SDWebImage源码学习笔记)