源码分析--SDWebImage

这个开源框架的主要作用就是:一个异步下载图片并且支持缓存的 `UIImageView'分类

框架中最常用的方法:

[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

  

框架中还有'UIButton'的分类,可以给'UIButton'异步加载图片,不过没有'UIImageView'分类中的这个方法常用。

接下来我们就以 `UIImageView+WebCache` 中的

- (void)sd_setImageWithURL:(NSURL *)url 

          placeholderImage:(UIImage *)placeholder;

 

这个方法为入口研究一下'SDWebImage'是如何工作的。

这个方法的实现:

- (void)sd_setImageWithURL:(NSURL *)url 
          placeholderImage:(UIImage *)placeholder {
    [self sd_setImageWithURL:url 
            placeholderImage:placeholder 
                     options:0 
                    progress:nil 
                   completed:nil];
}

方法的唯一作用就是调用了另一个方法:

[self sd_setImageWithURL:placeholderImage:options:progress:completed:]  

这个方法也是`UIImageView+WebCache` 中的核心方法了。

操作的管理

这是这个方法的第一行代码:

[self sd_cancelCurrentImageLoad];

框架中的所有操作实际上都是通过一个 `operationDictionary` 来管理, 而这个字典实际上是动态的添加到 `UIView` 上的一个属性, 至于为什么添加到 `UIView` 上, 主要是因为这个  `operationDictionary` 需要在 `UIButton` 和 `UIImageView` 上重用, 所以需要添加到它们的根类上.

这行代码是要保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突, 它会调用:

[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]

而这个方法会使当前 `UIImageView` 中的所有操作都被 `cancel`. 不会影响之后进行的下载操作.

占位图的实现

if (!(options & SDWebImageDelayPlaceholder)) {
    self.image = placeholder;
}

如果传入的 `options` 中没有 `SDWebImageDelayPlaceholder`(默认情况下 `options == 0`), 那么就会为 `UIImageView` 添加一个临时的 `image`, 也就是占位图.

获取图片

检测传入的 `url` 是否非空, 如果非空那么一个全局的 `SDWebImageManager` 就会调用以下的方法获取图片:

[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]

下载完成后会调用 `(SDWebImageCompletionWithFinishedBlock)completedBlock` 为 `UIImageView.image` 赋值, 添加上最终所需要的图片.

dispatch_main_sync_safe(^{
    if (!wself) return;
    if (image) {
        wself.image = image;
        [wself setNeedsLayout];
    } else {
        if ((options & SDWebImageDelayPlaceholder)) {
            wself.image = placeholder;
            [wself setNeedsLayout];
        }
    }
    if (completedBlock && finished) {
        completedBlock(image, error, cacheType, url);
    }
});

而最后, 在 `[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]` 返回 `operation` 的**同时**, 也会向 `operationDictionary` 中添加一个键值对, 来表示操作的正在进行:

[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

它将 `opertion` 存储到 `operationDictionary` 中方便以后的 `cancel`.到此为止我们已经对 `SDWebImage` 框架中的这一方法分析完了, 接下来我们将要分析 `SDWebImageManager` 中的方法 

[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]

SDWebImageManager

这个类就是隐藏在 `UIImageView+WebCache` 背后, 用于处理异步下载和图片缓存的类, 当然你也可以直接使用 `SDWebImageManager` 的上述方法`downloadImageWithURL:options:progress:completed:` 来直接下载图片.可以看到, 这个类的主要作用就是为 `UIImageView+WebCache` 和 `SDWebImageDownloader, SDImageCache` 之间构建一个桥梁, 使它们能够更好的协同工作, 我们在这里分析这个核心方法的源代码, 它是如何协调异步下载和图片缓存的.

if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}

if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}

这块代码的功能是确定 `url` 是否被正确传入, 如果传入参数的是 `NSString` 类型就会被转换为 `NSURL`. 如果转换失败, 那么 `url` 会被赋值为空, 这个下载的操作就会出错.

 

既然我们获取了 `url`, 再通过 `url` 获取对应的 `key`

NSString *key = [self cacheKeyForURL:url];

 

下一步是使用 `key` 在缓存中查找以前是否下载过相同的图片. 

operation.cacheOperation = [self.imageCache 
  queryDiskCacheForKey:key 
               done:^(UIImage *image, SDImageCacheType cacheType) { ... }];

这里调用 `SDImageCache` 的实例方法 `queryDiskCacheForKey:done:` 来尝试在缓存中获取图片的数据. 而这个方法返回的就是货真价实的 `NSOperation`.

如果我们在缓存中查找到了对应的图片, 那么我们直接调用 `completedBlock` 回调块结束这一次的图片下载操作.

dispatch_main_sync_safe(^{

    completedBlock(image, nil, cacheType, YES, url);
});

如果我们没有找到图片, 那么就会调用 `SDWebImageDownloader` 的实例方法:

id <SDWebImageOperation> subOperation =

  [self.imageDownloader downloadImageWithURL:url 

                                     options:downloaderOptions 

                                    progress:progressBlock 

                                   completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { ... }];

如果这个方法返回了正确的 `downloadedImage`, 那么我们就会在全局的缓存中存储这个图片的数据:

 

[self.imageCache storeImage:downloadedImage 
   recalculateFromImage:NO 
                  imageData:data 
                     forKey:key 
                     toDisk:cacheOnDisk];

并调用 `completedBlock` 对 `UIImageView` 或者 `UIButton` 添加图片, 或者进行其它的操作.

最后, 我们将这个 `subOperation` 的 `cancel` 操作添加到 `operation.cancelBlock` 中. 方便操作的取消.

operation.cancelBlock = ^{
    [subOperation cancel];
    }

SDWebImageCache

它维护了一个内存缓存和一个可选的磁盘缓存, 我们先来看一下在上一阶段中没有解读的两个方法, 首先是:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key 

                                 done:(SDWebImageQueryCompletedBlock)doneBlock;

 

这个方法的主要功能是异步的查询图片缓存. 因为图片的缓存可能在两个地方, 而该方法首先会在内存中查找是否有图片的缓存.

 

UIImage *image = [self imageFromMemoryCacheForKey:key];

 

这个 `imageFromMemoryCacheForKey` 方法会在 `SDWebImageCache` 维护的缓存 `memCache` 中查找是否有对应的数据, 而 `memCache` 就是一个 `NSCache`.

如果在内存中并没有找到图片的缓存的话, 就需要在磁盘中寻找了, 这个就比较麻烦了..

在这里会调用一个方法 `diskImageForKey` 这个方法的具体实现我在这里就不介绍了, 涉及到很多底层 `Core Foundation` 框架的知识, 不过这里文件名字的存储使用 `MD5` 处理过后的文件名.

CC_MD5(str, (CC_LONG)strlen(str), r);

对于其它的实现细节也就不多说了...

如果在磁盘中查找到对应的图片, 我们会将它复制到内存中, 以便下次的使用.

UIImage *diskImage = [self diskImageForKey:key];
if (diskImage) {
    CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale;
    [self.memCache setObject:diskImage forKey:key cost:cost];
}

这些就是 `SDImageCache` 的核心内容了, 而接下来将介绍如果缓存没有命中, 图片是如何被下载的.

 

SDWebImageDownloader

这个类的核心功能就是下载图片, 而核心方法就是上面提到的:

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

 

这个方法直接调用了另一个关键的方法:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock 
          andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock 
                     forURL:(NSURL *)url 
             createCallback:(SDWebImageNoParamsBlock)createCallback

它为这个下载的操作添加回调的块, 在下载进行时, 或者在下载结束时执行一些操作, 先来阅读一下这个方法的源代码:

BOOL first = NO;

if (!self.URLCallbacks[url]) {
    self.URLCallbacks[url] = [NSMutableArray new];
    first = YES;
}

// Handle single download of simultaneous download request for the same URL

NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;

if (first) {
    createCallback();
}

  

 

方法会先查看这个 `url` 是否有对应的 `callback`, 使用的是 `downloader` 持有的一个字典 `URLCallbacks`. 

如果是第一次添加回调的话, 就会执行 `first = YES`, 这个赋值非常的关键, 因为 `first` 不为 `YES` 那么 HTTP 请求就不会被初始化, 图片也无法被获取.

然后, 在这个方法中会重新修正在 `URLCallbacks` 中存储的回调块.

 

NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;

 

如果是第一次添加回调块, 那么就会直接运行这个 `createCallback` 这个 block, 而这个 block, 就是我们在前一个方法 `downloadImageWithURL:options:progress:completed:` 中传入的回调块.

[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{ ... }];

我们下面来分析这个传入的无参数的代码. 首先这段代码初始化了一个 `NSMutableURLRequest`:

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] 
  initWithURL:url 
        cachePolicy:...
    timeoutInterval:timeoutInterval];

这个 `request` 就用于在之后发送 `HTTP` 请求.

在初始化了这个 `request` 之后, 又初始化了一个 `SDWebImageDownloaderOperation` 的实例, 这个实例, 就是用于请求网络资源的操作. 它是一个 `NSOperation` 的子类, 

operation = [[SDWebImageDownloaderOperation alloc] 
  initWithRequest:request
                options:options
               progress:...
              completed:...
              cancelled:...}];

但是在初始化之后, 这个操作并不会开始(`NSOperation` 实例只有在调用 `start` 方法或者加入 `NSOperationQueue` 才会执行), 我们需要将这个操作加入到一个 `NSOperationQueue` 中.

 

[wself.downloadQueue addOperation:operation];

只有将它加入到这个下载队列中, 这个操作才会执行.

 

SDWebImageDownloaderOperation

这个类就是处理 HTTP 请求, URL 连接的类, 当这个类的实例被加入队列之后, `start` 方法就会被调用, 而 `start` 方法首先就会产生一个 `NSURLConnection`.

@synchronized (self) {
    if (self.isCancelled) {
        self.finished = YES;
        [self reset];
        return;
    }
    self.executing = YES;
    self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
    self.thread = [NSThread currentThread];
}

而接下来这个 `connection` 就会开始运行:

[self.connection start];

 

它会发出一个 `SDWebImageDownloadStartNotification` 通知

 

[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];

代理

 

`start` 方法调用之后, 就是 `NSURLConnectionDataDelegate` 中代理方法的调用.

 

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;

在这三个代理方法中的前两个会不停回调 `progressBlock` 来提示下载的进度.

而最后一个代理方法会在图片下载完成之后调用 `completionBlock` 来完成最后 `UIImageView.image` 的更新.

而这里调用的 `progressBlock` `completionBlock` `cancelBlock` 都是在之前存储在 `URLCallbacks` 字典中的. 

 

这大概就是

[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]  placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

这个方法执行的全部过程了.

小结 

`SDWebImage` 的图片加载过程: 

 * 查看缓存

* 缓存命中

    * 返回图片

         * 更新 `UIImageView`

  * 缓存未命中

    * 异步下载图片

     * 加入缓存

         * 更新 `UIImageView`

 

 

 

  

 

  

 

 

 

  

posted @ 2016-04-27 16:37  _NeverMore  阅读(173)  评论(0编辑  收藏  举报