SDWebImage源码阅读心得

版权所有, 转载请著明出处,保留链接。

#SDWebImage源码阅读心得

##概括
SDWebImage是一个异步下载图片的开源模块。
实现的业务功能,是异步下载网络上的图片,为上层屏蔽实现细节,提供接口调用。

一张图片在Cocoa中用UIImage对象来表现,而要在视图上显示,则需要UIImageView,显然它是视图对象。其组合依赖关系如下:

UIImageView->UIImage->图像数据

那么最直接的想法,是在UIImageView视图上扩展一个接口(异步接口),当上层模块需要显示一张图片时,创建一个UIImageView的实例,再调用扩展的异步接口,传入图片的URL(获取图像数据),回调函数(异步调用完成函数)等参数,这样UIImageView的事情就做完了,上层模块想要把UIImageView显示到哪个视图,直接在该视图上调用addSubView即可, 当图像数据从网络上获取后, UIImageView自然会显示出该图像, 因为UIImageView已经为我们封装了图像显示功能,现在只欠缺的图像数据。这是一个最朴素的应用,当然上层模块如果在图像的异步请求中,突然不想要该图像了,那么,此时SDWebImage也提供了撤销的接口。

SDWebImage源码阅读后,最大的感受是其职责明确,即各个类功能单一,权责明确。类之间通过组合实现业务功能,通过Objective-C的Category(类别)来扩展基础框架Cocoa类的功能。

##代码结构
SDWebImage的主要代码结构如下:

SDWebImage                                ---> 主文件夹

    Categories                            ---> 对Cocoa框架中类的扩展
        UIImageView+WebCache            ---> UIImageView的扩展,对外接口,异步下载图像
        UIView+WebCacheOperation        ---> UIView的扩展,UIView实例关联任务Operation,对外接口,撤销任务用
        ......

    Utils
        SDWebImageManager                ---> 模块中中枢系统,各个类实例由它协调,提供单例接口
        ......

    Cache
        SDImageCache                    ---> 负责所有图片的存储,包括内存上的缓存、硬盘文件存储,提供单例接口

    Downloader
        SDWebImageDownloader            ---> 负责模块中所有图片的下载,提供单例接口
        SDWebImageDownloaderOperation    ---> 负责模块中一张图片的下载,即一个任务,继承自Operation

    .....

其主要脉络来自于UIImageView+WebCache, SDWebImageManager, SDImageCache, SDWebImageDownloader, SDWebImageDownloaderOperation, 至于涉及图片的部分,例如格式,编码,resolution都没有深究。

##业务情景及逻辑
想象这样一个情景,假如网络上有图片1,图片2,图片3…图片n, 表现为URL1, URL2,URL3…URLn, 需要下载。当然不是一张图片,一张图片,串行的去下载,也不是开始下载一张图片后,然后一直等待其下载完,再去处理别的任务,也就是说需要的是并行异步的下载,各个图片间没有依赖关系,都是独立的事件。那么,SDWebImage模块就需要解决这样的一个业务情景,它需要管理这些图片的下载任务,管理这些图片的存储。

显然,一张图片表现为一个URL(NSURL对象), 一个NSURL对象对应着一个网络请求(NSMutalbeURLRequest), 一个网络请求对应着一个HTTP连接(NSURLConnection), 而一个HTTP连接对应着一个任务操作(SDWebImageDownloaderOperation), 如下:

网络图片--->NSURL--->NSMutableURLRequest--->NSURLConnection--->SDWebImageDownloaderOperation

而多张图片,就会有多个任务操作,多个操作显然会放在一个下载队列中(NSOperationQueue),而这个下载队列downloadQueue由SDWebImageDownloader类的单例来管理。在SDWebImage模块内部,凡是有图片URL需要下载,就丢给SDWebImageDownloader类的单例,由它负责下载,实现并行的异步下载。

假如现在,扩展后的UIImageView需要显示一张来自网络上的图片(URL), 那么它会把这个任务交给大内总管SDWebImageManager类的单例,SDWebImageManager首先会询问图片存储管理者SDImageCache类的实例,该URL对应的图片有没有在本地(内存或硬盘上),如果有,则读取图片数据返回,如果没有,则把这个任务交给SDWebImageDownloader类的单例,去下载网络上的图片,下载完成,则先由SDImageCache类的实例存储到本地,再告诉UIImageView这边已经获取到图片了,你可以显示了,注意,这其中的过程都是异步的。显然SDWebImageManager管理着这样的一个个任务(Operation),它把这些任务存储在了一个数组中:runningOperation。SDWebImageManager关心的是URL对应的图片UIImage, 至于UIImage是在本地SDWebCache获取的,还是通过SDWebImageDownloader从网络上获取的,它不关心,怎么获取的也不关心,这由你们兄弟俩SDWebCache和SDWebImageDownloader负责,我(SDWebImageManager)只在的记账本(runningOpertions数组)上记录有这本帐(SDWebImageCombinedOperation),到时候,处理好后,把结果告诉我就行。组合依赖调用如下示意:

图片-->URL--->UIImageView---->SDWebImageManager  --->SDImageCahce
                                                 --->SDImageDowloader ---->SDWebImageDownloaderOperation

现在再来谈谈SDWebImage中存储SDWebCache的实现,SDWebCache通过NSCache实现内存中的存取, NSCache像一个字典般通过KEY(URL计算后)-Value(UIImage)的形式存取,显然我们要指定一个文件夹路径diskCachePath,是图片硬盘上的存储地,由于存取异步并行的,SDWebCache管理着多张图片的同时存取,因而有一个队里来管理存取操作,特别是写操作,需要串行化,即ioQueue。此外,存储是占用APP资源的,当APP内存不足,或者APP退出、进入后台运行时,需要对APP内存进行必要的清理,此时SDWebCache以一定的策略清除内存和硬盘上的UIImage。

##主要接口函数

###UIImageView+WebCache
UIImageView+WebCache的的核心函数是如下:

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

其他存储的函数最终都是调用它的,例如:

- (void)sd_setImageWithURL:(NSURL *)url;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
......

其中progressBlock, completedBlock是回调函数,其形式如下,也是对外接口的一部分:

typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, 
                                                  NSInteger expectedSize); 
                                                  -------声明在SDWebImageDownloader头文件中

typedef void(^SDWebImageCompletionBlock)(UIImage *image, 
                                          NSError *error, 
                                          SDImageCacheType cacheType, 
                                          NSURL *imageURL); 
                                          -------声明在SDWebImageManager头文件中

核心函数的主要过程如下如下:

  1. 撤销当前UIImageView实例关联的获取图像的任务operation;
  2. 把传入参数URL与当前UIImageView实例关联起来;
  3. 根据需要设置替代图片(placeholder);
  4. 把URL丢给SDWebImageManager的单例(其实是调用其一个函数,记为函数A),叫它去获取图片;
  5. 调用SDWebImageManager单例函数后异步返回一个operation给UIImageView,UIImageView存储这个任务,其实是存储在UIView的关联字典中,待需要时撤销该operation;
  6. 待SDWebImageManager找到图片时,回调完成函数,在完成函数中告知UIImageView,你可以重画了,即setNeedsLayout;
  7. 结束。

###SDWebImageManager
A函数的神秘面纱如下,看清楚了:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, 
                                                      NSError *error, 
                                             SDImageCacheType cacheType, 
                                                         BOOL finished, 
                                                        NSURL *imageURL);

其中progressBlock与UIImageView+WebCache的一样,无需赘述,从中可以看出SDWebImageManager返回给UIImageView+WebCache的不仅是马上返回的遵守SDWebImageOperation协议的operation, 还有获取图片的情况,通过块SDWebImageCompletionWithFinishedBlock来表现,回调报告,具体有image, error, cacheType, finished, imageURL参数。
其具体处理逻辑如下:

  1. 创建SDWebImageCombinedOperation实例,并把这个实例加入runningOperations数组中,显然一个图片URL会产生一个operation,并记录在runningOperations数组中;
  2. 计算URL对应的KEY,依据key向SDWebCache查询是否有这样key的UIImage图片存在,异步返回,通过回调函数告之SDWebImageManager结果, 记为函数B;
  3. 在SDWebCache的查询done回调函数中,如果图片不存在,并判定需要下载该URL图片,则计算下载参数SDWebImageDownloaderOptions, 则向SDWebImageDownloader请求下载该URL对应的图片,异步返回,记为函数C,而后设置SDWebImageCombinedOperation实例操作的cancelBlock;
  4. 在SDWebImageDownloader的下载完成回调函数中,请求SDWebCache保存下载图片(异步),记为函数D,而后调用completedBlock回调,告之UIImageView+WebCache;

###SDImageCache
SDImageCache依据KEY查询图片是否存在的B函数接口如下:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;

typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);

逻辑过程是:

  1. 在内存依据关键字查询图片是否存在,如果存在,则直接调用doneBlock回调返回;
  2. 如果不存在,则创建一个NSOperation的实例,放在ioQueue串行队列中查询在硬盘上是否存在,如果存在则把它设置在内存中来,最后通过doneBlock回调函数把结果返回给调用者。

SDWebCache保存图片的C函数接口如下:

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
  1. 保存在内存中;
  2. 如果要存到硬盘上,则在串行队列ioQueue,执行块函数把图片数据保存到文件;调用者异步返回;

###SDWebImageDownloader
C函数如下:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                     options:(SDWebImageDownloaderOptions)options
                                    progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);

逻辑过程如下:

  1. 为该URL记录progressBlock, completeBlock回调函数,记录在URLCallbacks字典中;
  2. 以URL,缓存策略等参数初始化一个NSMutableURLRequest实例 request,并设置request的http请求参数;
  3. 以request,回调函数块初始化一个SDWebImageDownloaderOperation实例,在各回调函数块中会调用progressBlock, completeBock回调函数,记为函数D;
  4. 设置operation队列优先级,任务执行次序,FIFO/LIFO, 把operation加入下载队列downloadQueue, 并把它返回给调用者;

###SDWebImageDownloaderOperation
SDWebImageDownloaderOperation继承自NSOperation,并遵守SDWebImageOperation协议,由于其是并行concurrent operation,必须要重载如下函数:

start
isConcurrent
isExecuting
isFinished

其中start函数中是任务operation的执行地,其他的函数是状态判断。SDWebImageDownloaderOperation 也实现了NSURLConnectionDataDelegate协议。D函数如下:

- (id)initWithRequest:(NSURLRequest *)request
          options:(SDWebImageDownloaderOptions)options
         progress:(SDWebImageDownloaderProgressBlock)progressBlock
        completed:(SDWebImageDownloaderCompletedBlock)completedBlock
        cancelled:(SDWebImageNoParamsBlock)cancelBlock;

初始化函数中,会copy, progressBlock, completeBlock, cancelBlock, 并这是operation状态量, 下面来看看其关键的start函数实现过程:

  1. 以request初始化一个NSURLConnection实例,并把自己作为NSURLConnection的delegate,并开始连接,请求网络数据。
  2. 在NSURLConnection的delegate协议中,如下,调用progressBlock, completedBlock,一层层往上回调,告之相应的调用者。

    • -(void)connection:(NSURLConnection )connection didReceiveResponse:(NSURLResponse )response
    • -(void)connection:(NSURLConnection )connection didReceiveData:(NSData )data

##总结

  1. 可以看出SDWebImage对于耗时较长的请求,都是异步,调用者从不等待,在回调函数块中处理请求结果;
  2. 接口不只在于能直接调用的类实例上的函数,还存在回调函数块,也是接口的一部分;
  3. SDWebImage模块中单例充分应用,单例是模块内分层的依据,一个单例全权负责某方面的工作;
  4. OBJECTI-C是的C超集合,明显有C的烙印,对于每一个.h/.m文件,组成一个小模块,.h中先如C语言般声明,接着是OBJECITV-C的部分Interface,类的声明,而.m中也是如C语言般的实现,再是OBJECTIVE-C中的类扩展和实现,.h中对外可见,.m中对内可见,对外不可见,显然遵守C的面向模块的设计思路;
  5. 换元思想…,那么SDWebImage这个框架可以应用在很多方面…

版权所有, 转载请著明出处,保留链接。