iOS-源码解析

SDWebImage 分析

2017-07-16  本文已影响217人  wyanassert

SDWebImage 分析

Version 4.0.0


导航

按照模块分析 SDWebImage

1. UI交互的基类 UIView+WebCache

2. SDWebImage 的主要管理者 SDWebImageManager

3. 缓存模块 SDImageCache

4. 下载模块 SDWebImageDownloader

5. 下载的执行者 SDWebImageDownloaderOperation

6. 预加载 SDWebImagePrefetcher

7. GIF子模块 FLAnimatedImage


UIKit 交互1 -- UIView+WebCache

1. 接口定义

a. 下载图片

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

b. 下载已经缓存过的图片

- (void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
                                 placeholderImage:(nullable UIImage *)placeholder
                                          options:(SDWebImageOptions)options
                                         progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                        completed:(nullable SDExternalCompletionBlock)completedBlock;

c. 下载动图

- (void)sd_setAnimationImagesWithURLs:(nonnull NSArray<NSURL *> *)arrayOfURLs;

d. 取消下载动图

- (void)sd_cancelCurrentAnimationImagesLoad;

2. 分析

a. 下载图片

下载图片有数个方法定义, 见UIView+WebCache.h, 最终都调用了UIView+WebCache中的 sd_internalSetImageWithURL 这个方法.

sd_internalSetImageWithURL 方法做的事情:

Tips
dispatch_main_async_safe 定义了在主线程进行UI操作的宏:

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

weakSelf 为 nil 时候直接结束避免崩溃或者其他错误.

b. 下载已经缓存过的图片

NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url];
UIImage *lastPreviousCachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:key];

[self sd_setImageWithURL:url placeholderImage:lastPreviousCachedImage ?: placeholder options:options progress:progressBlock completed:completedBlock];

Tips : 在执行下载的过程中, 如果找到了缓存, 就忽略placeholder, 避免一次无效操作.

c. 下载动图

PS. 这也解释了 - (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key 为什么会有如下看起来很奇怪的代码, operation的类型并不是固定的.

SDOperationsDictionary *operationDictionary = [self operationDictionary];
id operations = operationDictionary[key];
if (operations) {
    if ([operations isKindOfClass:[NSArray class]]) {
        for (id <SDWebImageOperation> operation in operations) {
            if (operation) {
                [operation cancel];
            }
        }
    } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
        [(id<SDWebImageOperation>) operations cancel];
    }
    [operationDictionary removeObjectForKey:key];
}

Tips : [self operationDictionary] 使用 Runtime 为实例增加了变量.

Question : 如何保证下载的顺序?

d. 取消下载动图

Tips : 关于id <SDWebImageOperation> operation解释:
每个 operaton 都有实现一个 - (void)cancel; 方法, 这个是在SDWebImageOperation协议中定义, 无论是什么类型实例, 只要实现了该协议, 都可以统一调用,详细解释可以搜索iOS+面向接口编程.

3. 小结

UIView+WebCache 模块中, 只做了一些简单的操作, 定义好了与 UIKit 交互接口, 下载与取消交给了 SDWebImageManager 处理, 缓存交给了 SDImageCache 处理.


SDWebImage幕后管理者 -- SDWebImageManager

1. 接口定义

a. 缓存模块

@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;

b. 下载模块

@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;

c. 下载图片

- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                            completed:(nullable SDInternalCompletionBlock)completedBlock;

d. 手动设置图片缓存

- (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url;

e. 取消所有的操作

- (void)cancelAll;

f. 当前是否有操作在运行

- (BOOL)isRunning;

g. 异步检查图片是否已经被缓存

- (void)cachedImageExistsForURL:(nullable NSURL *)url
                     completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

h. 异步检查图片是否已经被缓存在了磁盘上

- (void)diskImageExistsForURL:(nullable NSURL *)url
                   completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

i. 获取URL缓存索引的Key

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url;

2. 分析

a. 缓存模块

b. 下载模块

c. 下载图片

d. 手动设置图片缓存

e. 取消所有的操作

f. 当前是否有操作在运行

g. 异步检查图片是否已经被缓存

h. 异步检查图片是否已经被缓存在了磁盘上

i. 获取URL缓存索引的Key

3. 小结

SDWebImage可以看出作者考虑到了很多一般开发者不会去考虑的事情, 简单的如线程安全, 更细致的如imageManager:transformDownloadedImage:withURL:方法, 方便使用SDWebImage的人在使用之前先处理, 再缓存, 一个个简单的应用场景是用户想对一张网络图片进行模糊处理, 一般的步骤是先用SDWebImage下载,然后自行模糊处理,再展示. 但如果有大量图片要处理, 又涉及到tableView的复用问题, 为了提高性能, 使用者要自己对模糊之后的图片做缓存, 优化缓存策略和IO潜在地问题等等. 实际上SDWebImage 已经可以处理这个问题而不需要使用者再去考虑.


SDWebImage缓存模块 -- SDImageCache

1. 接口定义

a. 缓存图片

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

b. 缓存图片到硬盘上(只能从IO Queue 调用)

- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;

c. 检查图片是否在硬盘上缓存(只检查, 不会把图片加到内存)

- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

d. 检查是否有缓存

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;

e. 从内存中取缓存的图片(同步)

- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key;

f. 从硬盘取缓存的图片(同步)

- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key;

g. 从缓存中取图片(同步)

- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;

h. 从内存和硬盘删除图片缓存(异步)

- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;

i. 从内存移除缓存, 选择是否从硬盘移除

- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;

j. 清除内存缓存

- (void)clearMemory;

k. 异步清除磁盘缓存

- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;

l. 异步清除硬盘上已经过期的缓存

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

2. 分析

初始化方法

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory;

初始化各个属性:

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

_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

生成的一个串行队列,专用于IO操作. 不再使用的时候应该使用dispatch_release释放队列.

@property (strong, nonatomic, nonnull) NSCache *memCache;

NSCache是iOS系统提供的缓存类,通过键值对对需要缓存的对象作强引用来达到缓存的目的.

NSFileManager *_fileManager;
dispatch_sync(_ioQueue, ^{
            _fileManager = [NSFileManager new];
        });

注意,生成_fileManager在ioQueue中,并且是是一个同步操作, 之后_fileManager都要在ioQueue中进行.

a. 缓存图片

Tips
NSCache 有最大缓存容积的设置totalCostLimit, 但是这个设置只有在设置缓存的时候指定要缓存对象占用的字节数(cost)才能生效. 但是对象的内存占用计算十分复杂, SDWebImage只是给出了一个大致值image.size.height * image.size.width * image.scale * image.scale;.

b. 缓存图片到硬盘上(只能从IO Queue 调用)

此方法只能在ioQueue中调用,奇怪的是SDImageCache并没有暴露ioQueue访问, 因此, 将此方法暴露在.h文件是没有意义的.

c. 检查图片是否在硬盘上缓存(只检查, 不会把图片加到内存)

在某个版本之前, 硬盘缓存没有文件后缀名, 为了兼容, 要做两次查找

d. 检查是否有缓存

e. 从内存中取缓存的图片(同步)

f. 从硬盘取缓存的图片(同步)

理论上来说, 这句话放在ioQueue中执行会好一些, 猜测可能是需要同步执行

g. 从缓存中取图片(同步)

h. 从内存和硬盘删除图片缓存(异步)

i. 从内存移除缓存, 选择是否从硬盘移除

j. 清除内存缓存

k. 异步清除磁盘缓存

l. 异步清除硬盘上已经过期的缓存

Tips : [[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]这个比较挺有意思.

3. 小结

这一个模块中,有内存与硬盘两级缓存, NSCache 在系统级别保证了线程安全,相对来说处理容易. 但是IO操作本身较为耗时, 单独创建一个队列作为ioQueue来进行IO操作, 达到在硬盘上缓存的目的.

SDWebImage下载模块 -- SDWebImageDownloader

1. 接口定义

a. 初始化

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;

b. 设置请求的Header

- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field;

c. 下载图片

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

d. 取消下载

- (void)cancel:(nullable SDWebImageDownloadToken *)token;

e. 暂停下载

- (void)setSuspended:(BOOL)suspended;

f. 取消所有的下载

- (void)cancelAllDownloads;

2. 分析

a. 初始化

b. 设置请求的Header

c. 下载图片

Tips : 怎么开始下载的? SDWebImageDownloaderOperation继承了NSOperation, 并重写了start()方法, 并在start()方法中调用了[self.dataTask resume];开始下载.

d. 取消下载

Tips : [SDWebImageDownloaderOperation cancel:]首先将token对应的callback移除掉. 当所有的callbacl都移除掉之后, 会调用父类NSOperationcancel方法, 这会将isCancelled属性置为YES, 在start方法调用的时候就不会真正执行. 最后调用[self.dataTask cancel];关闭数据传输.

Question: 手动调cancel方法后, 就不会执行失败的block了吗?

e. 暂停下载

f. 取消所有的下载

3. 小结

这一个模块开始进行图片下载相关代码的执行, 然而真正的下载代码还是被放在了SDWebImageDownloaderOperation中, 'SDWebImageDownloader'模块的分析只是对SDWebImageDownloaderOperation做了简单的描述, 主要还是重点分析本模块所做的事情--管理所有的下载行为. 此外, self.downloadQueue保证了对self.URLOperations操作能并发, 但又不相互干扰(同时保证异步和并发, 但实际上并没有并发).

SDWebImage下载的执行者 -- SDWebImageDownloaderOperation


1. 接口定义

a. 初始化

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

b. 存储回调Block

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

c. 开始(继承父类)

- (void)start;

d. 取消(继承父类)

- (void)cancel;

e. 是否在执行(继承父类)

- (void)setFinished:(BOOL)finished;

f. 是否已结束(继承父类)

- (void)setExecuting:(BOOL)executing;

g. 取消单个操作

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

2. 分析

a. 初始化

b. 存储回调Block

c. 开始(继承父类)

d. 取消(继承父类)

e. 是否在执行(继承父类)

f. 是否已结束(继承父类)

g. 取消单个操作

Tips: startcancel@synchronized保证的线程安全, 对_callbackBlocks的操作使用一个队列保障线程安全. 此外, operation持有两个session, 一个是unownedSession, 这个由SDWebImageDownloader持有, operation对它保持弱引用, 还有一个是ownedSession, 当初始化的session被释放时候, 使用自己生成的session, 并用ownedSession保持引用, 并在[self reset]中释放这个session.

3. 小结

这个Operation完成了SDWebImage最重要的下载功能. 将一个URL的下载下载封装成一个NSOperation, 特别是在线程安全上做了一些优化, 和使用异步或是同步, 哪些操作需要保证线程安全, 哪些元素需要复制, 值得思考. 在SDWebImage的issue中有很多于此模块有关的, 值得细看.

SDWebImage 预加载 -- SDWebImagePrefetcher


1. 接口定义

1. 初始化

- (nonnull instancetype)initWithImageManager:(nonnull SDWebImageManager *)manager;

2. 执行预加载

- (void)prefetchURLs:(nullable NSArray<NSURL *> *)urls
            progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
           completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;

3. 取消

- (void)cancelPrefetching;

2. 分析

1. 初始化

(lldb) pinternals 0x61000107f280
(SDWebImageManager) $12 = {
  NSObject = {
    isa = SDWebImageManager
  }
  _delegate = nil
  _imageCache = 0x00006080010661c0
  _imageDownloader = 0x00006080000ff800
  _cacheKeyFilter = (null)
  _failedURLs = 0x00006080006406f0 0 elements
  _runningOperations = 0x00006080010671c0 @"1 element"
}

(lldb) pinternals 0x000060000106ef00
(SDWebImageManager) $8 = {
  NSObject = {
    isa = SDWebImageManager
  }
  _delegate = nil
  _imageCache = 0x00006080010661c0
  _imageDownloader = 0x00006080000ff800
  _cacheKeyFilter = (null)
  _failedURLs = 0x00006000006524b0 0 elements
  _runningOperations = 0x0000600001277f80 @"1 element"
}

2. 执行预加载

if (self.prefetchURLs.count > self.requestedCount) {
    dispatch_async(self.prefetcherQueue, ^{
        [self startPrefetchingAtIndex:self.requestedCount];
    });
}

3. 取消

3. 小结

这一个模块大部分是依靠SDWebImageManager来完成主体功能, 我曾经在某篇博客上看到有人说SDWebImagePrefetcher是不支持并发的, 至少在目前这个版本看来, 是完全支持一组URL并发的, 但是不支持同时预加载多组URL.

SDWebImage 子模块 GIF -- FLAnimatedImage

SDWebImage 支持动态图的, 建立在Flipboard的开源项目FLAnimatedImage的基础之上, 增加的一个扩展, 使用方法是pod 'SDWebImage/GIF', 或者手动把SDWebImage文件夹中的FLAnimatedImage文件夹拖入工程.

1. 接口定义

1. 加载Gif

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

2. 分析

1. 加载Gif

#import "NSData+ImageContentType.h"
#import "UIView+WebCache.h"

[self.gifImageView sd_internalSetImageWithURL:[NSURL URLWithString:self.resource.previewImageUrl]
                             placeholderImage:nil
                                      options:0
                                 operationKey:nil
                                setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
                                    SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                                    if (imageFormat == SDImageFormatGIF) {
                                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                            FLAnimatedImage *animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData];;
                                            dispatch_async(dispatch_get_main_queue(), ^{
                                                weakSelf.gifImageView.animatedImage = animatedImage;
                                            });
                                        });
                                        weakSelf.gifImageView.image = nil;
                                    } else {
                                        weakSelf.gifImageView.image = image;
                                        weakSelf.gifImageView.animatedImage = nil;
                                    }
                                }
                                     progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
                                     }];

3. 小结

这个模块被应该做三件事情, 一件是下载, 这个在下载模块完成了; 第二个是从下载下来的二进制文件中生成一张图片, 这个在UIImage+MultiFormat模块中完成的, 有兴趣的同学可以看看这个文件; 第三个是展示二进制文件, 这个是FLAnimatedImage做的.


写在最后

最近想看一下一些优秀的开源库是如何编写的, SDWebImage是我看的第一份源码(以前草草看的不算), 受益匪浅. 这次我边看边写笔记, 最终整理这篇博客, 不仅仅是对源码的流程讲解, 有一些小的细节小技巧我也有单独标出来. 平时码代码的过程还是太随意了, 因为工程的量级决定不需要太注重一些细节, 但是对于这些细节, 能注意的还是应该注意.

作者:wyanassert

原地址:https://github.com/wyanassert/WYBlob/blob/master/doc/SDWebImage/Analyze.md

上一篇下一篇

猜你喜欢

热点阅读