移动开发技术

Jason's Blog

SDWebImage源码解读

| Comments

SDWebImage是最流行的iOS第三方图片加载库,也是github上star数目最多的objective-c第三方库。这篇文章对SDWebImage的源码进行简单的分析,主要是分析代码的执行流程。源码版本是目前最新的稳定版本4.4.4。

源码目录

SDWebImage下分为以下几个目录

  • Downloader: 负责图片的下载,基于NSURLSession实现,主要类是SDWebImageDownloader
  • Cache: 负责图片的缓存,主要是对内存和磁盘进行读写,实现二级缓存功能,主要类是SDImageCache
  • Decoder: 负责图片的解码功能,以支持不同格式的图片,例如SDWebImageGIFCoder是对GIF图片的支持
  • Utils: 封装的工具类,主要类是SDWebImaegeManger,控制整个图片下载和缓存流程
  • Categories: 一些扩展支持,比如如果要对GIF图片进行下载展示,需要引入UIImage+GIF.h(框架已默认引入)
  • WebCache Categories: 对UIView的扩展支持,最常用的是UIImageView+WebCache.h,实现了UIImageView的图片加载和缓存功能
  • FLAnimatedImage: 对FLAnimatedImage进行了扩展,可以对动态图片进行加载和缓存

调用时序图

以官方Demo详情页加载图片为例,加载图片的时序图如下:

  1. 调用sd_internalSetImageWithURL:placeholderImage:options:operationKey:internalSetImageBlock:progress:completed:context:
  2. 调用loadImageWithURL:options:progress:completed:
  3. 调用queryCacheOperationForKey:cacheOptions:done查询缓存
  4. 调用imageFromMemoryCacheForKey查内存, 调用diskImageDataBySearchingAllPathsForKey查磁盘
  5. 调用downloadImageWithURL:options:progress:completed:从网络下载
  6. 调用storeImage:imageData:forKey:toDisk:completion:将结果缓存

源码分析

在官方Demo中,有一个列表页和详情页,我们从更简单的详情页来分析,详情页只有一个FLAnimatedImage控件,功能就是加载了一张图片,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__weak typeof(self) weakSelf = self;
[self.imageView sd_setImageWithURL:self.imageURL
                  placeholderImage:nil
                           options:SDWebImageProgressiveDownload
                          progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL *targetURL) {
                              dispatch_async(dispatch_get_main_queue(), ^{
                                  float progress = 0;
                                  if (expectedSize != 0) {
                                      progress = (float)receivedSize / (float)expectedSize;
                                  }
                                  weakSelf.progressView.hidden = NO;
                                  [weakSelf.progressView setProgress:progress animated:YES];
                              });
                          }
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                             weakSelf.progressView.hidden = YES;
                             [weakSelf.activityIndicator stopAnimating];
                             weakSelf.activityIndicator.hidden = YES;
                         }];

对外暴露的接口比较简单,传递需要加载图片的url,placeholder图片,加载选项options,加载过程回调progressBlock,完成回调completedBlock就可以了。

接下来到时序图的第(1)步,调用UIView+WebCachesd_internalSetImageWithURL:方法,代码如下:

1
2
3
4
5
6
7
8
9
10
[self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
               internalSetImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
                           ……
                       }
                            progress:progressBlock
                           completed:completedBlock
                             context:@{SDWebImageInternalSetImageGroupKey: group}];

继续跟进这个方法,时序图到第(2)步,调用SDWebImageManagerloadImageWithURL:方法,代码如下:

1
2
3
4
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
  //加载完图片的回调
  ……
}

时序图来到第(3)步,SDWebImageManager会调用SDImageCachequeryCacheOperationForKey来进行缓存查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//SDImageCache.m
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {

    // 读内存缓存操作
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    void(^queryDiskBlock)(void) =  ^{
      //读磁盘缓存
      ……
    };

    if (options & SDImageCacheQueryDiskSync) {
        queryDiskBlock();
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }

    return operation;
}

可以看到,在上边的代码中,调用imageFromMemoryCacheForKey先从内存里查询是否有图片缓存,调用dispatch_async(self.ioQueue, queryDiskBlock)从磁盘里查询是否有图片缓存。这是时序图中的第(4)步。如果命中了缓存,则直接回调完成block,不再走下边的流程了。如果没有命中缓存,那么继续下边。

流程来到时序图中的第(5)步,查询完缓存返回后,SDWebImageManager会调用SDWebImageDownloaderdownloadImageWithURL:方法从网络下载图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//SDWebImageDownloader.m
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {

    LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
    if (!operation || operation.isFinished || operation.isCancelled) {
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        //执行下载操作
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);

    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

从网络下载图片的代码如上,第8行生成NSOperation,第26行加入叫做downloadQueueNSOperationQueue执行。

下载完成后,时序图流程来到第(6)步,SDWebImageManager会调用SDImageCachestoreImage:方法将结果进行缓存,以便下次使用,然后再进行成功的回调,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

if (transformedImage && finished) {
    BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
    NSData *cacheData;
    // pass nil if the image was transformed, so we can recalculate the data from the image
    if (self.cacheSerializer) {
        cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
    } else {
        cacheData = (imageWasTransformed ? nil : downloadedData);
    }
    [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}

[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];

到这里图片加载流程结束。

SDWebImage里还有很多实现细节,比如多线程的控制和加锁,各种控制下载行为的选项,取消正在下载的操作,URL参数的容错,PlaceHolder的设置,加载IndicatorView的显示控制,下载过程Progress的控制等。由于篇幅原因在本文中省略,等以后有时间再细致分析。

Comments