SDWebImage原理

一、简介

1. 设计目的

SDWebImage 提供了 UIImageView、UIButton 、MKAnnotationView 的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。这样开发者就无须花太多精力在图片下载细节上,专心处理业务逻辑。

2. 特性

  • 提供 UIImageView, UIButton, MKAnnotationView 的分类,用来显示网络图片,以及缓存管理
  • 异步下载图片
  • 异步缓存(内存+磁盘),并且自动管理缓存有效性
  • 后台图片解压缩
  • 同一个 URL 不会重复下载
  • 自动识别无效 URL,不会反复重试
  • 不阻塞主线程
  • 高性能
  • 使用 GCD 和 ARC
  • 支持多种图片格式(包括 WebP 格式)
  • 支持动图(GIF)(4.0 之前的动图效果并不是太好,4.0 以后基于 FLAnimatedImage加载动图)

3. 用法

3.1 UITableView 中使用 UIImageView+WebCache

UITabelViewCell 中的 UIImageView 控件直接调用 sd_setImageWithURL: placeholderImage:方法即可

3.2 使用回调 blocks

在 block 中得到图片下载进度和图片加载完成(下载完成或者读取缓存)的回调,如果你在图片加载完成前取消了请求操作,就不会收到成功或失败的回调

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
//... completion code here ...
}];

5.3 SDWebImageManager 的使用

UIImageView(WebCache) 分类的核心在于 SDWebImageManager 的下载和缓存处理,SDWebImageManager将图片下载和图片缓存组合起来了。SDWebImageManager也可以单独使用。

    SDWebImageManager *manager = [SDWebImageManager sharedManager];
    [manager loadImageWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
        
    } completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
        if (image) {
            // do something with image
        }
    }];

5.4 单独使用 SDWebImageDownloader 异步下载图片

我们还可以单独使用 SDWebImageDownloader 来下载图片,但是图片内容不会缓存。

    SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
    [downloader downloadImageWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
        // progression tracking code
        
    } completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
        if (image && finished) {
            // do something with image
        }
    }];

5.5 单独使用 SDImageCache 异步缓存图片

SDImageCache 支持内存缓存和异步的磁盘缓存(可选),如果你想单独使用 SDImageCache 来缓存数据的话,可以使用单例,也可以创建一个有独立命名空间的 SDImageCache 实例。

添加缓存的方法:
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];

默认情况下,图片数据会同时缓存到内存和磁盘中,如果你想只要内存缓存的话,可以使用下面的方法:
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey toDisk:NO];
读取缓存时可以使用 queryDiskCacheForKey:done: 方法,图片缓存的 key 是唯一的,通常就是图片的 absolute URL。

SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
[imageCache queryCacheOperationForKey:myCacheKey done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {
        // image is not nil if image was found
}];

5.6 自定义缓存 key
有时候,一张图片的 URL 中的一部分可能是动态变化的(比如获取权限上的限制),所以我们只需要把 URL 中不变的部分作为缓存用的 key。

SDWebImageManager.sharedManager.cacheKeyFilter = ^(NSURL *url) {
 url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
 return [url absoluteString];
};

二、原理

SDWebImage原理_第1张图片
SDWebImage.png
SDWebImage原理_第2张图片
SDWebImage整体组件结构.png
SDWebImage原理_第3张图片
SDWebImage全图.jpeg

官方给的设置图片后执行时序图:
SDWebImage原理_第4张图片
执行时序图.png

在了解细节之前我们先大概浏览一遍主流程,也就是最核心的逻辑。

我们从 MasterViewController 中的[cell.imageView sd_setImageWithURL:url placeholderImage:placeholderImage];开始看起。
经过层层调用,直到 UIImageView+WebCache 中最核心的方法 sd_setImageWithURL: placeholderImage: options: progress: completed:。该方法中,主要做了以下几件事:

  • 取消当前正在进行的加载任务 operation
  • 设置 placeholder
  • 如果 URL 不为 nil,就通过 SDWebImageManager 单例开启图片加载任务 operation,SDWebImageManager 的图片加载方法中会返回一个 SDWebImageCombinedOperation 对象,这个对象包含一个 cacheOperation 和一个 cancelBlock。
流程简述
  • 1、当我门需要获取网络图片的时候,我们首先需要的便是URL,没有URL什么都没有,获得URL后我们SDWebImage实现的并不是直接去请求网路,而是检查图片缓存中有没有和URL相关的图片,如果有则直接返回image,如果没有则进行下一步。
  • 2、当图片缓存中没有图片时,SDWebImage依旧不会直从网络上获取,而是检查沙盒中是否存在图片,如果存在,则把沙盒中对应的图片存进image缓存中,然后按着第一步的判断进行。
  • 3、如果沙盒中也不存在,则显示占位图,然后根据图片的下载队列缓存判断是否正在下载,如果正在下载则等待,避免二次下载。如果不存则创建下载队列,下载完毕后将下载操作从队列中清除,并且将image存入图片缓存中。
  • 4、刷新UI(当然根据实际情况操作)将image存入沙盒缓存。
    SDWebImage原理_第5张图片
    SD流程.png
SDWebImageManager 的图片加载方法

downloadImageWithURL:options:progress:completed: 中会先拿图片缓存的 key (这个 key 默认是图片 URL)去 SDImageCache 单例中读取内存缓存,如果有,就返回给 SDWebImageManager;如果内存缓存没有,就开启异步线程,拿经过 MD5 处理的key 去读取磁盘缓存,如果找到磁盘缓存了,就同步到内存缓存中去,然后再返回给 SDWebImageManager

如果内存缓存和磁盘缓存中都没有,SDWebImageManager 就会调用 SDWebImageDownloader 单例的 -downloadImageWithURL: options: progress: completed:方法去下载,该会先将传入的 progressBlockcompletedBlock 保存起来,并在第一次下载该 URL 的图片时,创建一个 NSMutableURLRequest 对象和一个 SDWebImageDownloaderOperation 对象,并将该 SDWebImageDownloaderOperation 对象添加到 SDWebImageDownloader 的downloadQueue 来启动异步下载任务。

SDWebImageDownloaderOperation 中包装了一个 NSURLConnection 的网络请求,并通过 runloop 来保持 NSURLConnection 在 start 后、收到响应前不被干掉,下载图片时,监听 NSURLConnection 回调的-connection:didReceiveData:方法中会负责 progress 相关的处理和回调,- connectionDidFinishLoading:方法中会负责将 data 转为 image,以及图片解码操作,并最终回调 completedBlock

SDWebImageDownloaderOperation 中的图片下载请求完成后,会回调给 SDWebImageDownloader,然后 SDWebImageDownloader 再回调给 SDWebImageManagerSDWebImageManager 中再将图片分别缓存到内存和磁盘上(可选),并回调给 UIImageViewUIImageView 中再回到主线程设置 image 属性。至此,图片的下载和缓存操作就圆满结束了。

当然,SDWebImage 中还有很多细节可以深挖,包括一些巧妙设计和知识点,接下来再看看SDWebImage 中的实现细节。

查看SDWebImage的源码,与缓存有关的一共有四个文件SDImageCacheConfig和SDImageCache,首先看一下SDImageCacheConfig的头文件:

//是否压缩图片,默认为YES,压缩图片可以提高性能,但是会消耗内存
@property (assign, nonatomic) BOOL shouldDecompressImages;
 
//是否关闭iCloud备份,默认为YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;
 
//是否使用内存做缓存,默认为YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
 
/** 缓存图片的最长时间,单位是秒,默认是缓存一周
 * 这个缓存图片最长时间是使用磁盘缓存才有意义
 * 使用内存缓存在前文中讲解的几种情况下会自动删除缓存对象
 * 超过最长时间后,会将磁盘中存储的图片自动删除
 */
@property (assign, nonatomic) NSInteger maxCacheAge;
 
//缓存占用最大的空间,单位是字节
@property (assign, nonatomic) NSUInteger maxCacheSize;

NSCacheConfig类可以看得出来就是一个配置类,保存一些缓存策略的信息,没有太多可以讲解的地方,看懂就好,看一下NSCacheConfig.m文件的源码:

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

@implementation SDImageCacheConfig
- (instancetype)init {
    if (self = [super init]) {
        _shouldDecompressImages = YES;
        _shouldDisableiCloud = YES;
        _shouldCacheImagesInMemory = YES;
        _maxCacheAge = kDefaultCacheMaxCacheAge;
        _maxCacheSize = 0;
    }
    return self;
}
@end

从上面源码可以看出相关属性的默认值,以及maxCacheAge的默认值为一周时间。

/*
SDWebImage真正执行缓存的类
SDImageCache支持内存缓存,默认也可以进行磁盘存储,也可以选择不进行磁盘存储
*/
@interface SDImageCache : NSObject

#pragma mark - Properties

//SDImageCacheConfig对象,缓存策略的配置
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;

//内存缓存的最大cost,以像素为单位,后面有具体计算方法
@property (assign, nonatomic) NSUInteger maxMemoryCost;

//内存缓存,缓存对象的最大个数
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;

上面这一部分是属性的声明,属性很少,但我们在NSCache中都见过了,首先是SDImageCacheConfig,即前面讲解的缓存策略配置,maxMemoryCost其实就是NSCache的totalCostLimit,这里它使用像素为单位进行计算,maxMemoryCountLimit其实就是NSCache的countLimit,需要注意的是SDImageCache继承自NSObject没有继承NSCache,所以它需要保存这些属性。

/*
真正执行存储操作的方法
*/
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    //如果image为nil或image的URL为空直接返回即不执行保存操作
    if (!image || !key) {
        //如果回调块存在就执行完成回调块
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    // if memory cache is enabled
    //如果缓存策略指明要进行内存缓存
    if (self.config.shouldCacheImagesInMemory) {
        //根据前面的内联函数计算图片的大小作为cost
        NSUInteger cost = SDCacheCostForImage(image);
        //向memCache中添加图片对象,key即图片的URL,cost为上面计算的
        [self.memCache setObject:image forKey:key cost:cost];
    }

    //如果要保存到磁盘中
    if (toDisk) {
        //异步提交任务到串行的ioQueue中执行
        dispatch_async(self.ioQueue, ^{
            //进行磁盘存储的具体的操作,使用@autoreleasepool包围,执行完成后自动释放相关对象
            //我猜测这么做是为了尽快释放产生的局部变量,释放内存
            @autoreleasepool {
                NSData *data = imageData;
                //如果传入的imageData为空,图片不为空
                if (!data && image) {
                    // If we do not have any data to detect image format, use PNG format
                    //调用编码方法,获取NSData对象
                    //图片编码为NSData不在本文的讲述范围,可自行查阅
                    data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:SDImageFormatPNG];
                }
                //调用下面的方法用于磁盘存储操作
                [self storeImageDataToDisk:data forKey:key];
            }
            //存储完成后检查是否存在回调块
            if (completionBlock) {
                //异步提交在主线程中执行回调块
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    //如果不需要保存到磁盘中判断后执行回调块
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}

//具体执行磁盘存储的方法
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
    //判断图片NSData数据以及图片key是否为空,如果为空直接返回
    if (!imageData || !key) {
        return;
    }
    //检查当前执行队列是否为ioQueue,如果不是会提示开发者
    [self checkIfQueueIsIOQueue];

    //如果构造函数中构造的磁盘缓存存储图片路径的文件夹不存在
    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        //那就根据这个路径创建需要的文件夹
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }

    // get cache Path for image key
    // 根据key获取默认磁盘缓存存储路径下的MD5文件名的文件的绝对路径
    // 感觉有点绕口。。就是获取图片二进制文件在磁盘中的绝对路径,名称就是前面使用MD5散列的,路径就是构造函数默认构造的那个路径
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    // transform to NSUrl
    // 根据这个绝对路径创建一个NSURL对象
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    //使用NSFileManager创建一个文件,文件存储的数据就是imageData
    //到此,图片二进制数据就存储在了磁盘中了
    [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];

    // disable iCloud backup
    if (self.config.shouldDisableiCloud) {
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

上面就是图片缓存存储的核心方法了,如果要进行内存缓存就直接添加到memCache对象中,如果要进行磁盘缓存,就构造一个路径,构造一个文件名,然后存储起来就好了。这里面有几个重要的点,首先就是@autoreleasepool的使用,其实这里不添加这个autoreleasepool同样会自动释放内存,但添加后在这个代码块结束后就会立即释放,不会占用太多内存。其次,对于磁盘写入的操作是通过一个指定的串行队列实现的,这样不管执行多少个磁盘存储的操作,都必须一个一个的存储,这样就可以不用编写加锁的操作,可能有读者会疑惑为什么要进行加锁,因为并发情况下这些存储操作都不是线程安全的,很有可能会把路径修改掉或者产生其他异常行为,但使用了串行队列就完全不需要考虑加锁释放锁,一张图片存储完成才可以进行下一张图片存储的操作。

以上参考了以下文章:
iOS缓存 NSCache详解及SDWebImage缓存策略源码分析
SDWebImage 源码阅读笔记
SDWebImage实现分析
【iOS开源库】SDWebImage源码阅读&原理解析

转载请备注原文出处,不得用于商业传播——凡几多

你可能感兴趣的:(SDWebImage原理)