IOS源码解析:SDWeblmage (下)

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

续文见上篇:IOS源码解析:SDWeblmage(上)

目录

  • 四、SDImageCache 缓存
    • 1、NSCache 与 NSURLCache
    • 2、SDImageCacheConfig:缓存策略配置对象
    • 3、SDMemoryCache:内存缓存
    • 4、SDDiskCache:磁盘缓存
    • 5、SDImageCache中提供的属性
    • 6、单例的创建
    • 7、存入缓存图片
    • 8、查询并获取图片
    • 9、清除缓存图片
  • 五、SDWebImageManager 管理者
    • 1、SDWebImageCombinedOperation:管理读取缓存和下载图片
    • 2、获取url对应的缓存key
    • 3、核心方法:loadImageWithURL
    • 4、查询图片缓存流程
  • 六、UIImageView+WebCache 封装成分类方法
    • 1、调用真正设置图片的方法
    • 2、如果 url 存在则从 Manager 查询缓存或者下载图片
    • 3、如果 url 为 nil 则返回错误信息
  • Demo
  • 参考文献

四、SDImageCache 缓存

作用
  • SDImageCache 管理着一个内存缓存和磁盘缓存(可选),同时在写入磁盘缓存时采取异步执行,所以不会阻塞主线程,影响用户体验
  • 以空间换时间,提升用户体验:加载同一张图片,读取缓存是肯定比远程下载的速度要快得多的
  • 减少不必要的网络请求,提升性能,节省流量。一般来讲,同一张图片的 URL 是不会经常变化的,所以没有必要重复下载。另外,现在的手机存储空间都比较大,相对于流量来,缓存占的那点空间算不了什么
问题
  • 从读取速度和保存时间上来考虑,缓存该怎么存?key 怎么定?
  • 内存缓存怎么存?
  • 磁盘缓存怎么存?路径、文件名怎么定?
  • 使用时怎么读取缓存?
  • 什么时候需要移除缓存?怎么移除?
  • 如何判断一个图片的格式是PNG还是 JPG

1、NSCache 与 NSURLCache

NSCache 的优点
  • 类似字典,使用方便
  • 线程安全
  • 当内存不足时会自动释放存储对象
  • NSCachekey 不会被拷贝,不需要实现 Copying 协议
缓存对象的释放时机
  • totalCostLimit / countLimit 的限制
  • 手动调用 remove
  • APP进入到后台
  • 收到内存警告

a、NSCache 提供的属性和方法
// 当超过了规定的总的消耗大小时候会自动对内存进行削减操作
@property NSUInteger totalCostLimit;    
// 同上,但是无法确定丢弃的对象顺序
@property NSUInteger countLimit;     

// 当不确定cost的时候,调用该方法,系统会自动将cost设置为0
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; 
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;

b、实现 NSCacheDelegate 协议:通知对象将要被移除了
// 遵循 NSCacheDelegate 协议
@interface CacheIOP : NSObject<NSCacheDelegate>

@end

@implementation CacheIOP

// 实现协议方法
- (void)cache:(NSCache *)cache willEvictObject:(id)obj
{
    NSLog(@"对象:%@ 将要从缓存中移除了:%@",obj,cache);
}

@end

c、初始化缓存对象
- (void)initCache
{
    _cacheIOP = [CacheIOP new];
    _cache = [[NSCache alloc] init];
    _cache.countLimit = 5;// 设置当前最大缓存数量为5
    _cache.delegate = _cacheIOP;// 设置缓存的委托对象
}

d、添加缓存对象
- (void)addCacheObject
{
    // 由于设置了最大缓存数量为5,所以在添加第6个对象的时候会先释放缓存再添加
    for (int i = 0; i < 10; I++)
    {
        [_cache setObject:[NSString stringWithFormat:@"过年收到的红包:%d",i] forKey:[NSString stringWithFormat:@"day%d",I]];
    }
}

输出结果

2021-02-04 11:16:14.081684+0800 SDWebImageSourceCodeAnalysis[14481:12313005] 对象:过年收到的红包:0 将要从缓存中移除了:<NSCache: 0x600003a72640>
2021-02-04 11:16:14.081794+0800 SDWebImageSourceCodeAnalysis[14481:12313005] 对象:过年收到的红包:1 将要从缓存中移除了:<NSCache: 0x600003a72640>
2021-02-04 11:16:14.081885+0800 SDWebImageSourceCodeAnalysis[14481:12313005] 对象:过年收到的红包:2 将要从缓存中移除了:<NSCache: 0x600003a72640>
2021-02-04 11:16:14.081975+0800 SDWebImageSourceCodeAnalysis[14481:12313005] 对象:过年收到的红包:3 将要从缓存中移除了:<NSCache: 0x600003a72640>
2021-02-04 11:16:14.082045+0800 SDWebImageSourceCodeAnalysis[14481:12313005] 对象:过年收到的红包:4 将要从缓存中移除了:<NSCache: 0x600003a72640>

e、获取缓存中的对象
- (void)getCacheObject
{
    // 前面5个缓存对象被释放掉了,只剩下后面5个
    for (int i = 0; i < 10; I++)
    {
        NSLog(@"缓存对象:%@, 索引位置:%d",[_cache objectForKey:[NSString stringWithFormat:@"day%d",i]],i);
    }
}

输出结果

2021-02-04 11:20:56.000841+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 缓存对象:(null), 索引位置:0
2021-02-04 11:20:56.000918+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 缓存对象:(null), 索引位置:1
2021-02-04 11:20:56.000988+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 缓存对象:(null), 索引位置:2
2021-02-04 11:20:56.001057+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 缓存对象:(null), 索引位置:3
2021-02-04 11:20:56.001115+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 缓存对象:(null), 索引位置:4
2021-02-04 11:20:56.001190+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 缓存对象:过年收到的红包:5, 索引位置:5
2021-02-04 11:20:56.001266+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 缓存对象:过年收到的红包:6, 索引位置:6
2021-02-04 11:20:56.008719+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 缓存对象:过年收到的红包:7, 索引位置:7
2021-02-04 11:20:56.008808+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 缓存对象:过年收到的红包:8, 索引位置:8
2021-02-04 11:20:56.008896+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 缓存对象:过年收到的红包:9, 索引位置:9
进入到后台后面的缓存对象也会全部被自动移除
2021-02-04 11:23:25.517456+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 对象:过年收到的红包:5 将要从缓存中移除了:<NSCache: 0x600003b32580>
2021-02-04 11:23:25.517582+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 对象:过年收到的红包:6 将要从缓存中移除了:<NSCache: 0x600003b32580>
2021-02-04 11:23:25.517679+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 对象:过年收到的红包:7 将要从缓存中移除了:<NSCache: 0x600003b32580>
2021-02-04 11:23:25.517773+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 对象:过年收到的红包:8 将要从缓存中移除了:<NSCache: 0x600003b32580>
2021-02-04 11:23:25.517853+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 对象:过年收到的红包:9 将要从缓存中移除了:<NSCache: 0x600003b32580>

f、收到内存警告的时候只会自动移除部分缓存对象
image.png
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWaring:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

- (void)didReceiveMemoryWaring:(NSNotification *)notification
{
    NSLog(@"内存警告通知:%@",notification);
}

输出结果显示系统只会自动移除部分缓存对象,并不会全部移除掉

2021-02-04 11:30:40.021066+0800 SDWebImageSourceCodeAnalysis[14682:12334458] 内存警告通知:NSConcreteNotification 0x600001a86f70 {name = UIApplicationDidReceiveMemoryWarningNotification; object = <UIApplication: 0x7fa63ac04fe0>}
2021-02-04 11:30:40.022680+0800 SDWebImageSourceCodeAnalysis[14682:12334936] 对象:过年收到的红包:5 将要从缓存中移除了:<NSCache: 0x6000001f87c0>
2021-02-04 11:30:40.022871+0800 SDWebImageSourceCodeAnalysis[14682:12334936] 对象:过年收到的红包:6 将要从缓存中移除了:<NSCache: 0x6000001f87c0>
2021-02-04 11:30:40.022941+0800 SDWebImageSourceCodeAnalysis[14682:12334936] 对象:过年收到的红包:7 将要从缓存中移除了:<NSCache: 0x6000001f87c0>

g、NSURLCache
❶ 设置网络缓存策略
// 将缓存策略设置为不使用缓存直接请求原始数据
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];

// 默认使用HTTP协议的缓存策略来进行缓存 NSURLRequestUseProtocolCachePolicy
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
❷ 和服务器进行比较,判断资源是否更新
// lastModified:使用日期来判断是否服务端数据发生了改变
if (self.lastModified)
{
    [request setValue:self.lastModified forHTTPHeaderField:@"If-Modified-Since"];
}

// etag: 使用hash值来判断是否服务端数据发生了改变
if (self.etag)
{
    [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 点击重新请求网络,并且不使用缓存直接请求原始数据,以此验证资源是否更新
    [self httpCache];
}
❸ 从缓存当中读取数据
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if (error)
    {
        NSLog(@"请求网络失败: %@",error);
    }
    else
    {
        self.lastModified = [(NSHTTPURLResponse *)response allHeaderFields][@"Last-Modified"];
        //self.etag = [(NSHTTPURLResponse *)response allHeaderFields][@"Etag"];

        NSData *tempData = data;
        NSString *responseString = [[NSString alloc] initWithData:tempData encoding:NSUTF8StringEncoding];
  
        NSLog(@"lastModified: %@",self.lastModified);
        NSLog(@"response: %@",response);
        NSLog(@"responseString: %@",responseString);
    }
}] resume];
❹ Cache-Control
  • max-age:缓存时间
  • public:谁都可以缓存
  • no-cache:服务端进行缓存确认
  • no-store:禁止使用缓存
缓存目录
使用数据库打开本地缓存
2021-02-04 14:10:52.902660+0800 SDWebImageSourceCodeAnalysis[16236:12479083] response: <NSHTTPURLResponse: 0x600003d81720> { URL: http://via.placeholder.com/50x50.jpg } { Status Code: 200, Headers {
    "Accept-Ranges" =     (
        bytes
    );
    "Cache-Control" =     (
        "max-age=604800"
    );
    Connection =     (
        "keep-alive"
    );
    "Content-Length" =     (
        807
    );
    "Content-Type" =     (
        "image/jpeg"
    );
    Date =     (
        "Thu, 04 Feb 2021 06:10:47 GMT"
    );
    Etag =     (
        "\"5febdf22-327\""
    );
    Expires =     (
        "Thu, 11 Feb 2021 06:10:47 GMT"
    );
    "Last-Modified" =     (
        "Wed, 30 Dec 2020 02:00:02 GMT"
    );
    Server =     (
        "nginx/1.6.2"
    );
    "X-Cache" =     (
        L1
    );
} }
2021-02-04 14:10:52.902762+0800 SDWebImageSourceCodeAnalysis[16236:12479083] lastModified: Wed, 30 Dec 2020 02:00:02 GMT
2021-02-04 14:10:52.902844+0800 SDWebImageSourceCodeAnalysis[16236:12479083] responseString: (null)
// 304表示服务端资源并没有更新,所以从本地缓存中加载数据
response: <NSHTTPURLResponse: 0x6000019b6d40> { URL: http://via.placeholder.com/50x50.jpg } { Status Code: 304, Headers {
    "Cache-Control" =     (
        "max-age=604800"
    );

2、SDImageCacheConfig:缓存策略配置对象

枚举:以什么方式来计算图片的过期时间
typedef NS_ENUM(NSUInteger, SDImageCacheConfigExpireType)
{
    // 图片最近访问的时间
    SDImageCacheConfigExpireTypeAccessDate,

    // 默认:图片最近修改的时间
    SDImageCacheConfigExpireTypeModificationDate,

    // 图片的创建时间
    SDImageCacheConfigExpireTypeCreationDate,
};
控制开关
// 默认缓存策略配置
@property (nonatomic, class, readonly, nonnull) SDImageCacheConfig *defaultCacheConfig;

// 是否应该取消iCloud备份,默认是YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;

// 是否使用内存缓存,默认是YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;

// 是否开启SDMemoryCache内部维护的一张图片弱引用表
// 开启的好处是当收到内存警告,SDMemoryCache会移除图片的缓存
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;

// 在进入应用程序时是否删除过期的磁盘数据
@property (assign, nonatomic) BOOL shouldRemoveExpiredDataWhenEnterBackground;
配置选项
// 硬盘图片读取的配置选项
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;

// 把图片存入硬盘的配置选项,默认NSDataWritingAtomic原子操作
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
过期时间和限制大小
// 图片最大的缓存时间,默认1星期
// 在清除缓存的时候会先把缓存时间过期的图片清理掉再清除图片到总缓存大小在最大占用空间一半以下
@property (assign, nonatomic) NSTimeInterval maxDiskAge;

// 能够占用的最大磁盘空间
@property (assign, nonatomic) NSUInteger maxDiskSize;

// 能够占用的最大内存空间
@property (assign, nonatomic) NSUInteger maxMemoryCost;

// 缓存能够保存的key-value个数的最大数量
@property (assign, nonatomic) NSUInteger maxMemoryCount;

// 硬盘缓存图片过期时间的计算方式,默认是最近修改的时间
@property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType;

// 存储图片到硬盘的文件管理者
@property (strong, nonatomic, nullable) NSFileManager *fileManager;

3、SDMemoryCache:内存缓存

a、属性
// 多线程锁保证多线程环境下weakCache数据安全
SD_LOCK_DECLARE(_weakCacheLock);

// 弱引用表
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache;

b、初始化
❶ 最简单的初始化方法
- (instancetype)init
{
    self = [super init];
    if (self)
    {
        _config = [[SDImageCacheConfig alloc] init];
        [self commonInit];
    }
    return self;
}
❷ 使用自定义的缓存策略配置进行初始化
- (instancetype)initWithConfig:(SDImageCacheConfig *)config
{
    self = [super init];
    if (self)
    {
        _config = config;
        [self commonInit];
    }
    return self;
}
❸ 真正的初始化方法

当收到内存警告,内存缓存虽然被清理,但是有些图片已经被其他对象强引用着。这时weakCache维持这些图片的弱引用,如果需要获取这些图片就不用去硬盘获取了。

- (void)commonInit
{
    SDImageCacheConfig *config = self.config;
    self.totalCostLimit = config.maxMemoryCost;
    self.countLimit = config.maxMemoryCount;

    [config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCost)) options:0 context:SDMemoryCacheContext];
    [config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCount)) options:0 context:SDMemoryCacheContext];

    // 初始化弱引用表
    //NSPointerFunctionsWeakMemory,对值进行弱引用,不会对引用计数+1
    self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
    SD_LOCK_INIT(_weakCacheLock);

    // 监听内存警告通知
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(didReceiveMemoryWarning:)
                                                 name:UIApplicationDidReceiveMemoryWarningNotification
                                               object:nil];
}
❹ 当收到内存警告通知,移除内存中缓存的图片
- (void)didReceiveMemoryWarning:(NSNotification *)notification
{
    // 仅仅移除内存中缓存的图片,仍然保留weakCache,维持对被强引用着的图片的访问
    [super removeAllObjects];
}

c、存入弱引用表

SDMemoryCache继承自NSCacheNSCache可以设置totalCostLimit来限制缓存的总成本消耗,所以我们在添加缓存的时候需要通过cost来指定缓存对象消耗的成本。SDImageCache用图片的像素点(宽缩放比例)来计算图片的消耗成本。

- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g
{
    [super setObject:obj forKey:key cost:g];
    
    if (!self.config.shouldUseWeakMemoryCache)
    {
        return;
    }
    
    if (key && obj)
    {
        // 存入弱引用表
        SD_LOCK(_weakCacheLock);
        [self.weakCache setObject:obj forKey:key];
        SD_UNLOCK(_weakCacheLock);
    }
}

d、把通过弱引用表获取的图片添加到内存缓存中
- (id)objectForKey:(id)key
{
    id obj = [super objectForKey:key];
    if (!self.config.shouldUseWeakMemoryCache)
    {
        return obj;
    }
    if (key && !obj)
    {
        // 检查弱引用表
        SD_LOCK(_weakCacheLock);
        obj = [self.weakCache objectForKey:key];
        SD_UNLOCK(_weakCacheLock);
        if (obj)
        {
            // 把通过弱引用表获取的图片添加到内存缓存中
            NSUInteger cost = 0;
            if ([obj isKindOfClass:[  1UIImage class]]) {
                cost = [(UIImage *)obj sd_memoryCost];
            }
            [super setObject:obj forKey:key cost:cost];
        }
    }
    return obj;
}

4、SDDiskCache:磁盘缓存

a、使用MD5处理URL,生成文件名

如果进入沙盒查看缓存的图片,可以发现文件名是用过md5的格式来命名。将图片数据存储到磁盘(沙盒)时,需要提供一个包含文件名的路径,这个文件名是一个对 key 进行 MD5 处理后生成的字符串。

static inline NSString * _Nonnull SDDiskCacheFileNameForKey(NSString * _Nullable key)
{
    const char *str = key.UTF8String;

    // 计算key的md5值
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSURL *keyURL = [NSURL URLWithString:key];
    NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
    
    // md5值拼接文件后缀
    // X 表示以十六进制形式输出
    // 02 表示不足两位,前面补0输出;超过两位,不影响
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
    // 加密后的文件名
    return filename;
}

b、把图片资源存入磁盘
- (void)setData:(NSData *)data forKey:(NSString *)key
{
    // 如果还没有缓存目录,通过fileManager生成缓存目录
    if (![self.fileManager fileExistsAtPath:self.diskCachePath])
    {
        [self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // 获取key对应的完整缓存路径
    NSString *cachePathForKey = [self cachePathForKey:key];
    // 转换成URL
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    // 把数据存入路径保存到硬盘
    [data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
    
    // 禁止iCloud备份
    if (self.config.shouldDisableiCloud)
    {
        // 给文件添加到运行存储到iCloud属性
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

5、SDImageCache中提供的属性

枚举:缓存图片的方式
typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions)
{
    // 当内存有图片,查询内存缓存
    SDImageCacheQueryMemoryData = 1 << 0,

    // 同步的方式来获取内存缓存(默认异步)
    SDImageCacheQueryMemoryDataSync = 1 << 1,

    // 同步的方式来获取硬盘缓存(默认异步)
    SDImageCacheQueryDiskDataSync = 1 << 2,

    // 缩小大图(>60M)
    SDImageCacheScaleDownLargeImages = 1 << 3,

    // 避免解码图片
    SDImageCacheAvoidDecodeImage = 1 << 4,
};
公开的属性
// 缓存策略配置对象
@property (nonatomic, copy, nonnull, readonly) SDImageCacheConfig *config;

// 使用SDMemoryCache(继承自NSCache)来实现内存缓存
@property (nonatomic, strong, readonly, nonnull) id<SDMemoryCache> memoryCache;

// 使用SDDiskCache来实现磁盘缓存
@property (nonatomic, strong, readonly, nonnull) id<SDDiskCache> diskCache;

// 获取图片默认的磁盘缓存路径
@property (nonatomic, copy, nonnull, readonly) NSString *diskCachePath;
单例和初始化方法
// 暴露的单例对象
@property (nonatomic, class, readonly, nonnull) SDImageCache *sharedImageCache;

// 默认的磁盘缓存目录
@property (nonatomic, class, readwrite, null_resettable) NSString *defaultDiskCacheDirectory;

// 指定命名空间,图片存到对应的沙盒目录中
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;

// 指定命名空间和沙盒目录
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nullable NSString *)directory;

// 指定命名空间、沙盒目录、缓存策略配置
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nullable NSString *)directory
                                   config:(nullable SDImageCacheConfig *)config 
图片缓存路径
// 指定key,获取图片的缓存路径
- (nullable NSString *)cachePathForKey:(nullable NSString *)key;
进行缓存图片
// 把图片二进制数据存入内存
- (void)storeImageToMemory:(nullable UIImage*)image
                    forKey:(nullable NSString *)key;

// 把图片二进制数据存入硬盘
- (void)storeImageDataToDisk:(nullable NSData *)imageData
                      forKey:(nullable NSString *)key;

// 异步缓存图片到内存和磁盘
- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;
检查是否存在缓存图片
// 异步的方式查询硬盘中是否有key对应的缓存图片
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDImageCacheCheckCompletionBlock)completionBlock;

// 同步的方式查询硬盘中是否有key对应的缓存图片
- (BOOL)diskImageDataExistsWithKey:(nullable NSString *)key;
查询并获取缓存图片
// 同步的方式获取硬盘缓存的图片二进制数据
- (nullable NSData *)diskImageDataForKey:(nullable NSString *)key;

// 异步的方式来获取硬盘缓存的图片二进制数据
- (void)diskImageDataQueryForKey:(nullable NSString *)key completion:(nullable SDImageCacheQueryDataCompletionBlock)completionBlock;

// 异步的方式来获取硬盘缓存的图片
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;

// 异步的方式来获取硬盘缓存的图片
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;

// 同步的方式来获取内存缓存的图片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key;

// 同步的方式获取硬盘缓存的图片
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key;

// 同步的方式,先查询内存中有没有缓存的图片,如果没有再查询硬盘中有没有缓存的图片
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;
移除缓存中的图片
// 异步的方式移除缓存中的图片,包括内存和硬盘
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;

// 异步的方式移除缓存中的图片,包括内存和硬盘(可选,fromDisk为YES移除硬盘缓存)
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;

// 移除内存中的图片
- (void)removeImageFromMemoryForKey:(nullable NSString *)key;

// 移除磁盘中的图片
- (void)removeImageFromDiskForKey:(nullable NSString *)key;
清除缓存
// 清除内存缓存
- (void)clearMemory;

// 异步方式清除硬盘缓存
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;

// 异步方式清除过期的图片
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;
获取缓存信息
// 同步方式计算缓存目录的大小
- (NSUInteger)totalDiskSize;

// 同步方式计算缓存的图片数量
- (NSUInteger)totalDiskCount;

// 异步的方式获取缓存图片数量和大小
- (void)calculateSizeWithCompletionBlock:(nullable SDImageCacheCalculateSizeBlock)completionBlock;
私有属性
// 内存缓存
@property (nonatomic, strong, readwrite, nonnull) id<SDMemoryCache> memoryCache;

// 磁盘缓存
@property (nonatomic, strong, readwrite, nonnull) id<SDDiskCache> diskCache;

// 缓存策略配置
@property (nonatomic, copy, readwrite, nonnull) SDImageCacheConfig *config;

// 磁盘缓存路径
@property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath;

// 访问操作硬盘缓存时用到的串行队列
@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue;

6、单例的创建

单例的创建最终会调用的方法
// 
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nullable NSString *)directory
                                   config:(nullable SDImageCacheConfig *)config
{
    if ((self = [super init]))
    {
        ...
    }
}
❶ 初始化一个串行的dispatch_queue_t
_ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL);
❷ 初始化缓存策略配置对象
if (!config)
{
    config = SDImageCacheConfig.defaultCacheConfig;
}
_config = [config copy];
❸ 初始化内存缓存对象
_memoryCache = [[config.memoryCacheClass alloc] initWithConfig:_config];
❹ 初始化磁盘
// 初始化磁盘缓存路径
if (!directory)
{
    directory = [self.class defaultDiskCacheDirectory];
}
_diskCachePath = [directory stringByAppendingPathComponent:ns];

// 初始化磁盘缓存对象
_diskCache = [[config.diskCacheClass alloc] initWithCachePath:_diskCachePath config:_config];
❺ 监听通知来清除过期的图片缓存数据
// 当应用终止的时候,清除老数据
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil];

// 当应用进入后台的时候,在后台删除老数据
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];

7、存入缓存图片

在存储缓存数据时,先计算图片像素大小,并存储到内存缓存中去,然后如果需要存到磁盘(沙盒)中,就开启异步线程将图片的二进制数据存储到磁盘(沙盒)中。

a、把一张图片存入缓存的具体实现
  • image:缓存的图片对象
  • imageData:缓存的图片数据
  • key:缓存对应的key
  • toDisk:是否缓存到瓷片
  • completionBlock:缓存完成回调
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
          toMemory:(BOOL)toMemory
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock
{
    ...
}
❶ 如果允许内存缓存,先把图片缓存到内存
if (toMemory && self.config.shouldCacheImagesInMemory)
{
    // 计算缓存数据的大小(图片像素大小)
    NSUInteger cost = image.sd_memoryCost;
    // 并存储到内存缓存中去
    [self.memoryCache setObject:image forKey:key cost:cost];
}
❷ 在一个线性队列中做磁盘缓存操作

一般图片的大小都不会很小,对图片进行编码过程中也会产出一些开销不小的临时对象。在子线程中添加自动释放池,可以提前释放这些对象,缓解内存压力。

// 如果需要存储到沙盒的话,就异步执行磁盘缓存操作
dispatch_async(self.ioQueue, ^{
    @autoreleasepool
    {
        ...
    }
}
❸ 获取图片的类型GIF/PNG等

如果需要在存储之前将传进来的 image 转成 NSData,而不是直接使用传入的imageData,那么就要针对 iOS 系统下,按不同的图片格式来转成对应的 NSData 对象。那么图片格式是怎么判断的呢?这里是根据是否有 alpha 通道以及图片数据的前 8 位字节来判断是不是 PNG 图片,不是 PNG 的话就按照 JPG 来处理。

SDImageFormat format = image.sd_imageFormat;
if (format == SDImageFormatUndefined)
{
    if (image.sd_isAnimated)
    {
        format = SDImageFormatGIF;
    }
    else
    {
        // 如果 imageData 为 nil,就根据 image 是否有 alpha 通道来判断图片是否是 PNG 格式的
        // 如果 imageData 不为 nil,就根据 imageData 的前 8 位字节来判断是不是 PNG 格式的
        // 因为 PNG 图片有一个唯一签名,前 8 位字节是(十进制): 137 80 78 71 13 10 26 10
        format = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage] ? SDImageFormatPNG : SDImageFormatJPEG;
    }
}
❹ 根据指定的SDImageFormat把图片进行编码,得到可以存储的二进制数据
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
❺ 把处理好了的数据存入磁盘
[self _storeImageDataToDisk:data forKey:key];
❻ 在主线程调用回调闭包
if (completionBlock)
{
    dispatch_async(dispatch_get_main_queue(), ^{
        completionBlock();
    });
}

b、把图片资源存入磁盘
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key
{
    if (!imageData || !key)
    {
        return;
    }
    
    [self.diskCache setData:imageData forKey:key];
}

8、查询并获取图片

memoryCache 中去找,如果找到了对应的图片(一个 UIImage 对象),就直接回调 doneBlock,并直接返回。 如果内存缓存中没有找到对应的图片,就开启异步队列,调用 diskImageDataBySearchingAllPathsForKey 读取磁盘缓存,读取成功之后,再保存到内存缓存,最后再回到主队列,回调 doneBlock

a、在缓存中查询对应key的数据
  • key:要查询的key
  • doneBlock:查询结束以后的Block
  • return:返回做查询操作的Block
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock
{
    ...
}
❶ 先检查内存缓存,如果找到了对应的图片就回调 doneBlock,并直接返回
UIImage *image;
if (queryCacheType != SDImageCacheTypeDisk)
{
    image = [self imageFromMemoryCacheForKey:key];
}
❷ 如果内存缓存中没有找到对应的图片,开启异步队列,读取硬盘缓存
NSOperation *operation = [NSOperation new];
❸ 从磁盘获取图片,这一步包含了图片解码
// 在一个自动释放池中处理图片从磁盘加载
@autoreleasepool
{
    NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
    UIImage *diskImage;
    if (image)
    {
        diskImage = image;
    }
}
❹ 把从磁盘取出的缓存图片加入内存缓存中
if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory)
{
    NSUInteger cost = diskImage.sd_memoryCost;
    [self.memoryCache setObject:diskImage forKey:key cost:cost];
}
❺ 图片处理完成以后回到主队列回调Block
if (doneBlock)
{
    if (shouldQueryDiskSync)
    {
        doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
    }
    else
    {
        dispatch_async(dispatch_get_main_queue(), ^{
            doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
        });
    }
}

b、根据key获取缓存在内存中的图片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key
{
    return [self.memoryCache objectForKey:key];
}

c、根据指定的key获取存储在磁盘上的数据
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context
{
    ...
}
❶ 从磁盘中获取到缓存图片
NSData *data = [self diskImageDataForKey:key];
UIImage *diskImage = [self diskImageForKey:key data:data options:options context:context];
❷ 将图片保存到内存
if (diskImage && self.config.shouldCacheImagesInMemory && shouldCacheToMomery)
{
    NSUInteger cost = diskImage.sd_memoryCost;
    [self.memoryCache setObject:diskImage forKey:key cost:cost];
}

9、清除缓存图片

每新加载一张图片,就会新增一份缓存,时间一长,磁盘上的缓存只会越来越多,所以我们需要定期清除部分缓存。清扫磁盘缓存有两个指标:一是缓存有效期,二是缓存体积最大限制。值得注意的是,清扫磁盘缓存和清空磁盘缓存是两个不同的概念,清空是删除整个缓存目录,清扫只是删除部分缓存文件。

a、清空缓存
❶ 清空内存缓存
- (void)clearMemory
{
    [self.memoryCache removeAllObjects];
}
❷ 清空磁盘缓存
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion
{
    dispatch_async(self.ioQueue, ^{
        [self.diskCache removeAllData];
        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }
    });
}

b、应用进入后台的时候,调用这个方法清除缓存图片
- (void)applicationDidEnterBackground:(NSNotification *)notification
{
    ...
}
❶ 如果backgroundTask对应的时间结束了,任务还没有处理完成则直接终止任务
UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
    
    // 当任务非正常终止的时候,做清理工作
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];
❷ 图片清理结束以后,处理完成终止任务
[self deleteOldFilesWithCompletionBlock:^{
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];

c、当应用终止的时候,清除老数据
- (void)applicationWillTerminate:(NSNotification *)notification
{
    [self deleteOldFilesWithCompletionBlock:nil];
}

d、当应用终止或者进入后台都回调用这个方法来清除缓存图片
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock
{
    dispatch_async(self.ioQueue, ^{
        // 移除过期数据
        [self.diskCache removeExpiredData];
        
        // 执行完毕,主线程回调
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

e、这里会根据图片存储时间来清理图片。默认是一周,从最老的图片开始清理

清扫磁盘缓存的逻辑是,先遍历所有缓存文件,并根据文件的修改时间来删除过期的文件,同时记录剩下的文件的属性和总体积大小,如果设置了 maxCacheAge 属性的话,接下来就把剩下的文件按修改时间从小到大排序(最早的排最前面),最后再遍历这个文件数组,一个一个删,直到总体积小于 desiredCacheSize 为止,也就是 maxCacheSize 的一半。

- (void)removeExpiredData
{
    ...
}
❶ 获取磁盘缓存的默认根目录
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
❷ 获取文件迭代器和过期时间
// 创建文件迭代器
// 第二个参数制定了需要获取的属性集合
// 第三个参数表示不迭代隐藏文件
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                           includingPropertiesForKeys:resourceKeys
                                                              options:NSDirectoryEnumerationSkipsHiddenFiles
                                                         errorHandler:NULL];

// 根据文件的修改时间来删除过期的文件
NSDate *expirationDate = (self.config.maxDiskAge < 0) ? nil: [NSDate dateWithTimeIntervalSinceNow:-self.config.maxDiskAge];
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
// 同时记录剩下的文件的属性和总体积大小
NSUInteger currentCacheSize = 0;
❸ 获取指定url对应文件
// 删除比指定日期更老的图片
// 记录文件的大小,以提供给后面删除使用
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
// 遍历所有缓存文件
for (NSURL *fileURL in fileEnumerator)
{
    NSError *error;
    // 获取指定url对应文件
    NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
    
    // 如果是文件夹则返回
    if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue])
    {
        continue;
    }
    ...
}
❺ 如果修改日期大于指定日期,则加入要移除的数组里
// 获取指定url文件对应的修改日期
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
// 如果修改日期大于指定日期,则加入要移除的数组里
if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate])
{
    [urlsToDelete addObject:fileURL];
    continue;
}
❻ 获取指定的url对应的文件的大小,并且把url与对应大小存入一个字典中
// 同时记录剩下的文件的属性和总体积大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
❼ 删除所有最后修改日期大于指定日期的所有文件
for (NSURL *fileURL in urlsToDelete)
{
    [self.fileManager removeItemAtURL:fileURL error:nil];
}
❽ 如果我们当前缓存的大小超过了默认大小,则按照日期删除,直到缓存大小<默认大小的一半
NSUInteger maxDiskSize = self.config.maxDiskSize;
if (maxDiskSize > 0 && currentCacheSize > maxDiskSize)
{
    const NSUInteger desiredCacheSize = maxDiskSize / 2;
    
    // 接下来就把剩下的文件按修改时间从小到大排序(最早的排最前面)
    NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                             usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                 return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
                                                             }];
    
    // 迭代删除缓存,直到缓存大小是默认缓存大小的一半
    for (NSURL *fileURL in sortedFiles)
    {
        if ([self.fileManager removeItemAtURL:fileURL error:nil])
        {
            NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            
            // 总的缓存大小减去当前要删除文件的大小
            currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
            
            if (currentCacheSize < desiredCacheSize)
            {
                break;
            }
        }
    }
}

五、SDWebImageManager 管理者

初始化SDImageCache和SDWebImageDownloader对象
- (nonnull instancetype)initWithCache:(nonnull id<SDImageCache>)cache loader:(nonnull id<SDImageLoader>)loader
{
    if ((self = [super init]))
    {
        _imageCache = cache;
        _imageLoader = loader;
        
        // 用于保存加载失败的url集合
        _failedURLs = [NSMutableSet new];
        SD_LOCK_INIT(_failedURLsLock);
        
        // 用于保存当前正在加载的Operation
        _runningOperations = [NSMutableSet new];
        SD_LOCK_INIT(_runningOperationsLock);
    }
    return self;
}

1、SDWebImageCombinedOperation:管理读取缓存和下载图片

a、属性
// 用来取消当前加载任务的
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;

// 用来取消读取缓存操作
@property (strong, nonatomic, readwrite, nullable) id<SDWebImageOperation> cacheOperation;

// 管理者
@property (weak, nonatomic, nullable) SDWebImageManager *manager;

b、cancel 方法:取消缓存任务或者加载任务
- (void)cancel
{
    @synchronized(self)
    {
        if (self.isCancelled)
        {
            return;
        }

        self.cancelled = YES;
        if (self.cacheOperation)
        {
            [self.cacheOperation cancel];
            self.cacheOperation = nil;
        }
        if (self.loaderOperation)
        {
            [self.loaderOperation cancel];
            self.loaderOperation = nil;
        }
        [self.manager safelyRemoveOperationFromRunning:self];
    }
}

2、获取url对应的缓存key

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url
{
    if (!url)
    {
        return @"";
    }
    
    NSString *key;
    // 如果有实现根据指定的url获取key的Block,则用这个方式获取key
    id<SDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
    if (cacheKeyFilter)
    {
        key = [cacheKeyFilter cacheKeyForURL:url];
    }
    // 否则直接用url的绝对值为key
    else
    {
        key = url.absoluteString;
    }
    
    return key;
}

3、核心方法:loadImageWithURL

a、UIImageView等这种分类都默认通过调用这个方法来获取数据
  • url:图片的url地址
  • options:获取图片的属性
  • progressBlock:加载进度回调
  • completedBlock:加载完成回调
  • return:返回一个加载的载体对象以便提供给后面取消删除等
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock
{
    ...
    return operation;
}

b、判断传入的url类型
// 如果传入的url是NSString格式的则转换为NSURL类型再处理
if ([url isKindOfClass:NSString.class])
{
    url = [NSURL URLWithString:(NSString *)url];
}

// 如果url不是NSURL类型的对象则置为nil
if (![url isKindOfClass:NSURL.class])
{
    url = nil;
}

c、绑定一个CombinedOperation对象
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;

d、判断是否是曾经下载失败过的url
BOOL isFailedUrl = NO;
if (url)
{
    SD_LOCK(_failedURLsLock);
    isFailedUrl = [self.failedURLs containsObject:url];
    SD_UNLOCK(_failedURLsLock);
}

e、如果这个 url 曾经下载失败过
// 如果这个 url 曾经下载失败过,并且没有设置 SDWebImageRetryFailed,就回调 completedBlock 直接返回
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl))
{
    NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
    NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
    [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
    return operation;
}

f、把加载图片操作添加到runningOperations中
// 里面是所有正在做图片加载过程的operation的集合
SD_LOCK(_runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(_runningOperationsLock);

g、获取图片配置结果并开始从缓存中加载图片
// 获取图片配置结果
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];

// 开始从缓存中加载图片
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];

4、查询图片缓存流程

- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock
{
    ...
}
a、创建缓存任务
// 检查是否应该查询缓存
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache)
{
    // 根据url获取key
    NSString *key = [self cacheKeyForURL:url context:context];

    // 给 SDWebImageCombinedOperation 的缓存任务赋值
    // imageCache 的类型为SDImageCache,调用其根据key查询缓存图片的方法
    operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType)
    {
        ...
    }
}

六、UIImageView+WebCache 封装成分类方法

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                   context:(nullable SDWebImageContext *)context
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock
{
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                             context:context
                       setImageBlock:nil
                            progress:progressBlock
                           completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
                               if (completedBlock) {
                                   completedBlock(image, error, cacheType, imageURL);
                               }
                           }];
}

1、调用真正设置图片的方法

sd_internalSetImageWithURL...
a、判断当前控件上是否有其他任务,如果有就取消掉
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
self.sd_latestOperationKey = validOperationKey;
// 调用了[Operation cancel]
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
self.sd_imageURL = url;

b、如果当前options不是延迟设置Placeholder就在主线程中设置占位图
if (!(options & SDWebImageDelayPlaceholder))
{
    dispatch_main_async_safe(^{
        [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
    });
}

2、如果 url 存在则从 Manager 查询缓存或者下载图片

a、重置下载进度
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
if (imageProgress)
{
    imageProgress.totalUnitCount = 0;
    imageProgress.completedUnitCount = 0;
}
b、检查并启动加载指示器(加载转圈动画)
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
c、开始计算下载进度
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
    if (imageProgress)
    {
        imageProgress.totalUnitCount = expectedSize;
        imageProgress.completedUnitCount = receivedSize;
    }
    
    if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)])
    {
        double progress = 0;
        if (expectedSize != 0)
        {
            progress = (double)receivedSize / expectedSize;
        }
        progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
        dispatch_async(dispatch_get_main_queue(), ^{
            [imageIndicator updateIndicatorProgress:progress];
        });
    }
    
    if (progressBlock)
    {
        progressBlock(receivedSize, expectedSize, targetURL);
    }
};
d、调用 SDWebImageManager 的 loadImageWithURL 方法开始加载图片
// 获取Manager(可由用户自定义)
SDWebImageManager *manager = context[SDWebImageContextCustomManager];

id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {

    // 下载完成停止指示器
    if (finished)
    {
        [self sd_stopImageIndicator];
    }

    if (image)
    {
        // 图片下载成功,设置image
        targetImage = image;
        targetData = data;
    }
    else if (options & SDWebImageDelayPlaceholder)
    {
        // 图片下载失败,设置 placeholder
        targetImage = placeholder;
        targetData = nil;
    }
    
    dispatch_main_async_safe(^{
        [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
    });
}];
e、将当前的 operation 和 key 设置到NSMapTable中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;

- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key
{
    if (key)
    {
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation)
        {
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            @synchronized (self)
            {
                [operationDictionary setObject:operation forKey:key];
            }
        }
    }
}

3、如果 url 为 nil 则返回错误信息

// 停止指示器
[self sd_stopImageIndicator];

dispatch_main_async_safe(^{
    if (completedBlock)
    {
        // 直接回调 completedBlock,返回错误信息
        NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
        completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
    }
});

Demo

Demo在我的Github上,欢迎下载。
SourceCodeAnalysisDemo

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容