【iOS】SDWebImage源码

文章目录

SDWebImage

简介

是iOS上最流行的异步图片下载和缓存框架,主要功能有异步加载网络图片、自动缓存图片(支持内存缓存和磁盘缓存)、占位图处理(加载前显示默认图片)、GIF支持(可显示动图)、下载进度回调(可以显示下载进度或加载动画)、方便集成UIIMageView/UIButton

其中的耗时操作都在子线程中执行,以确保主线程的流畅性。使用的是GCD和ARC。支持后台图片解压缩处理。

组织架构


图片解码机制:后台线程高效处理

iOS系统在渲染图片时需要将其解码为位图格式,这个过程默认在主线程进行,可能导致界面卡顿,于是SDWebImage做了如下优化:

  • 解码时机与线程策略:当图片从磁盘加载或者网络下载完成之后,就会立即在后台线程进行解码,解码器使用专门的NSOperationQueue避免解码任务阻塞主线程
  • 空间换时间的缓存策略:解码后的位图数据会被缓存到内存中,当统一图片再次请求时,可以直接使用缓存的解码结果。
  • 渐进式解码支持:对于网络下载的大图片,SDWebImage支持渐进式解码,即图片在下载过程中逐步显示,用户可以较快的看到图片内容,提升等待体验

缓存机制:智能的双层存储系统(核心保障)

  • 内存缓存:基于NSCache实现,具有自动清理机制,当系统内存紧张时NSCache自动释放部分缓存。默认情况下不限制内缓存缓存大小,但是可以使用totalCostLimit和countLimit进行自定义。
objc 复制代码
#import <SDWebImage/SDWebImage.h>

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
	NSUInteger physicalMemory = (NSUInteger)[NSProcessInfo processInfo].physicalMemory;
	// 例如使用物理内存的 1/8 作为图片内存缓存上限
	NSUInteger memoryCacheLimit = physicalMemory / 8;
	SDImageCacheConfig *config = [SDImageCache sharedImageCache].config;
	config.maxMemoryCost = memoryCacheLimit;
	config.maxMemoryCount = 300;
  return YES;
}
  • 磁盘缓存:图片以文件形式存储在cache目录中,文件名经过MD5哈希处理确保唯一性和安全性。
  • 默认最大缓存大小:可以设置SDWebImage的磁盘缓存大小为100MB,当缓存超过此限制时,SDWebImage会基于文件的最后访问时间进行清理,优先移除最久未访问的图片,同样可以在SDImageCacheConfig中自定义。

缓存清理机制:灵活的资源管理

  • 自动清理机制:
    • 基于时间的清理:默认情况下会清理超过一周的缓存文件
    • 基于大小的清理:当缓存超过设定的大小时,自动清理最旧的图片文件
  • 手动清理接口:
objc 复制代码
// 清理所有内存缓存
[[SDImageCache sharedImageCache] clearMemory];

// 清理所有磁盘缓存(异步)
[[SDImageCache sharedImageCache] clearDiskOnCompletion:nil];

// 清理过期的磁盘缓存(异步)
[[SDImageCache sharedImageCache] deleteOldFilesWithCompletionBlock:nil];
  • 细粒度控制:基于特定URL或key清理缓存,实现更精准的缓存管理
  1. 同时清理磁盘、内存缓存
objc 复制代码
#import <SDWebImage/SDWebImage.h>

- (void)removeCacheForURL:(NSURL *)url {
    if (!url) return;

    NSString *cacheKey =
        [[SDWebImageManager sharedManager] cacheKeyForURL:url];

    [[SDImageCache sharedImageCache] removeImageForKey:cacheKey
                                             cacheType:SDImageCacheTypeAll
                                            completion:^{
        NSLog(@"已清理指定 URL 的内存和磁盘缓存");
    }];
}
  1. 清理自定义键
objc 复制代码
//存储形式:
NSString *key = @"infrared-visible-fusion-preview-001";

[[SDImageCache sharedImageCache] storeImage:image imageData:nil forKey:key cacheType:SDImageCacheTypeAll completion:nil];

//清理形式:
[[SDImageCache sharedImageCache] removeImageForKey:key cacheType:SDImageCacheTypeAll completion:^{
    NSLog(@"已清理指定 key 的缓存");
}];

动态图支持:从GIF到现代动画格式

早期版本通过将GIF分解为帧序列,使用UIImage的动画API播放,这种方式内存占用过高并且功能有限。

从SDWebImage5.0开始引入了SDAnimatedImage协议,提供统一的动画图片接口,支持多种格式。(GIF、APNG、WebP、HEIC)

SDAnimatedImageView采用了惰性解码策略,仅仅解码当前显示和预加载的帧,大幅度降低内存占用,同时还可以通过maxBufferSize属性控制缓冲帧数,寻找流畅度和内存消耗的平衡点

试图可见性触发加载:按需加载

SDWeb~与UIKit协同工作:

objc 复制代码
// 在cellForRowAtIndexPath中设置图片
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@&#34;Cell&#34;];
    // 获取图片URL
    NSURL *imageURL = [self imageURLForIndexPath:indexPath];
    // 使用SDWebImage加载图片
    [cell.imageView sd_setImageWithURL:imageURL  placeholderImage:[UIImage imageNamed:@"pop.png"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
        // 加载完成处理
    }];
    return cell;
}

// 在prepareForReuse中取消未完成的加载
- (void)prepareForReuse {
    [super prepareForReuse];
    [self.imageView sd_cancelCurrentImageLoad];
}

SDWebImage内部会管理加载队列,优先处理可见cell的请求。

同时还可以使用预加载技术提前加载即将显示的图片:

objc 复制代码
// 1. 预加载:提前加载即将显示的图片
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell  forRowAtIndexPath:(NSIndexPath *)indexPath {
    // 预加载下一张图片
        NSURL *preloadURL = @"....";
        [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:@[preloadURL]];
}

// 2. 设置不同的加载优先级
SDWebImageOptions options = SDWebImageLowPriority | SDWebImageProgressiveLoad;
[cell.imageView sd_setImageWithURL:imageURL placeholderImage:nil options:options];

如果用户上下滚动,willdisplay方法会被多次触发,导致同一个url重复提交给prefetcher。但是我们不能这样无节制的频繁提交预加载任务,消耗性能。可以通过一个集合纪律以经预加载过的url。
同时prefetchURLs方法会返回SDWebImagePrefetchToken,我们可以使用这个token处理预加载任务的取消。如果用户快速反向滑动,之前的预加载可能就没有意义了,此时可以取消部分预取或者进行类似操作。

objc 复制代码
[[SDWebImagePrefetcher sharedImagePrefetcher] cancelPrefetching];
objc 复制代码
- (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
    for (NSIndexPath *indexPath in indexPaths) {
        if (indexPath.row >= self.dataSource.count) {
            continue;
        }
        NSURL *url = [self imageURLForIndexPath:indexPath];
        if (!url) {
            continue;
        }
        // 避免重复预加载同一个 indexPath
        if (self.prefetchTokenMap[indexPath]) {
            continue;
        }
        SDWebImagePrefetchToken *token = [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:@[url] options:SDWebImageLowPriority context:nil progress:nil completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
            // 预加载完成后移除 token,避免字典无限增长
            dispatch_async(dispatch_get_main_queue(), ^{
                [self.prefetchTokenMap removeObjectForKey:indexPath];
            });
        }];
        if (token) {
            self.prefetchTokenMap[indexPath] = token;
        }
    }
}

- (void)tableView:(UITableView *)tableView cancelPrefetchingForRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
    for (NSIndexPath *indexPath in indexPaths) {
        SDWebImagePrefetchToken *token = self.prefetchTokenMap[indexPath];
        if (token) {
            [token cancel];
            [self.prefetchTokenMap removeObjectForKey:indexPath];
        }
    }
}

系统自留的预加载接口也可以使用:

objc 复制代码
@interface ViewController () <UITableViewDataSourcePrefetching>

@property (nonatomic, strong) NSMutableDictionary<NSIndexPath *, SDWebImagePrefetchToken *> *prefetchTokens;

@end

- (void)tableView:(UITableView *)tableView
prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
    for (NSIndexPath *indexPath in indexPaths) {
        NSURL *url = [self imageURLForIndexPath:indexPath];
        if (!url) {
            continue;
        }
        SDWebImagePrefetchToken *token = [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:@[url]];
        if (token) {
            self.prefetchTokens[indexPath] = token;
        }
    }
}

- (void)tableView:(UITableView *)tableView cancelPrefetchingForRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths {
    for (NSIndexPath *indexPath in indexPaths) {
        SDWebImagePrefetchToken *token = self.prefetchTokens[indexPath];
        [token cancel];
        [self.prefetchTokens removeObjectForKey:indexPath];
    }
}

加载优先级

objc 复制代码
SDWebImageOptions options = SDWebImageLowPriority | SDWebImageProgressiveLoad//渐进性加载;
[cell.imageView sd_setImageWithURL:imageURL placeholderImage:nil options:options];

正在显示的图片尽量不要使用低优先级,预加载的可以

静态图与动态图选择

  • 静态图片场景:
    • 使用标准的UIImageView
    • 通过sd_setImageWithURL:方法加载
    • 性能最佳,内存占用最低
  • 动态图片场景
    • 使用SDAnimatedImageView
    • 通过sd_setImageWithURL:方法加载(SDWebImage会自动检测图片类型)
    • 支持GIF、APNG、WebP等多种动态格式

像是用户上传未知类型图片情况使用SDAnimatedImageView。有的敏感场景可以考虑先拿图片的元信息再决定类型。

图片转换器

objc 复制代码
SDImageResizingTransformer *resizeTransformer =
[SDImageResizingTransformer transformerWithSize:CGSizeMake(100, 100)
                                      scaleMode:SDImageScaleModeAspectFill];

SDImageRoundCornerTransformer *roundTransformer =
[SDImageRoundCornerTransformer transformerWithRadius:10
                                             corners:UIRectCornerAllCorners
                                         borderWidth:1
                                         borderColor:[UIColor whiteColor]];

SDImagePipelineTransformer *pipelineTransformer =
[SDImagePipelineTransformer transformerWithTransformers:@[
    resizeTransformer,
    roundTransformer
]];

[imageView sd_setImageWithURL:url
             placeholderImage:[UIImage imageNamed:@"image_placeholder"]
                      options:0
                      context:@{
    SDWebImageContextImageTransformer: pipelineTransformer
}];

transformer不仅处理显示,还参与缓存,每个 transformer 都必须有自己的 transformerKey,这个 key 会追加到原始 URL 生成的缓存 key 后面;同一张输入图片加同一个 transformer key,应该生成同一张输出图片

性能监控

objc 复制代码
NSUInteger memCost = [[SDImageCache sharedImageCache] totalMemoryCost];//大概率不能用这个,没有公开
NSUInteger diskCount = [[SDImageCache sharedImageCache] totalDiskCount];//当前磁盘缓存中图片数量
NSUInteger diskSize = [[SDImageCache sharedImageCache] totalDiskSize];//磁盘缓存总大小,byte

//上面的方式直接写是同步遍历磁盘,下面是异步遍历磁盘
- (void)printSDWebImageDiskCacheInfoAsync {
    [[SDImageCache sharedImageCache] calculateSizeWithCompletionBlock:^(NSUInteger fileCount, NSUInteger totalSize) {
        NSLog(@"SDWebImage 磁盘缓存数量: %lu", (unsigned long)fileCount);
        NSLog(@"SDWebImage 磁盘缓存大小: %.2f MB", totalSize / 1024.0 / 1024.0);
    }];
}

[SDWebImageManager.sharedManager setCacheKeyFilter:^NSString * _Nullable(NSURL * _Nullable url) {
    // 记录加载时间
    CFTimeInterval startTime = CACurrentMediaTime();
    return [url absoluteString];
}];
//cacheKeyFilter 只负责生成缓存 key。它不等于图片开始下载,也不等于图片开始显示。官方示例中也是用它去去掉 URL query 参数,然后返回新的缓存 key

UIImageView+WebCache 的接口本身支持 progress block 和 completed block;progress block 在下载过程中回调,completed block 在加载完成后回调,并且 completion 里会返回 SDImageCacheType,用于判断图片来自内存缓存、磁盘缓存还是网络。

objc 复制代码
#import <SDWebImage/SDWebImage.h>
#import <QuartzCore/QuartzCore.h>

- (void)setImageWithURL:(NSURL *)url imageView:(UIImageView *)imageView {
    if (!url) {
        imageView.image = nil;
        return;
    }

    CFTimeInterval startTime = CACurrentMediaTime();

    [imageView sd_setImageWithURL:url placeholderImage:nil options:0  progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
        if (expectedSize > 0) {
            CGFloat progress = (CGFloat)receivedSize / (CGFloat)expectedSize;
            NSLog(@"图片下载进度: %.2f%%, URL: %@", progress * 100, targetURL.absoluteString);
        }

    } completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, SURL * _Nullable imageURL) {
        CFTimeInterval endTime = CACurrentMediaTime();
        CFTimeInterval cost = endTime - startTime;
        NSString *source = @"Unknown";
        switch (cacheType) {
            case SDImageCacheTypeNone:
                source = @"Network";
                break;

            case SDImageCacheTypeDisk:
                source = @"Disk Cache";
                break;

            case SDImageCacheTypeMemory:
                source = @"Memory Cache";
                break;

            case SDImageCacheTypeAll:
                source = @"All";
                break;
        }

        if (error) {
            NSLog(@"图片加载失败: %@, 耗时: %.3f 秒, URL: %@",
                  error.localizedDescription,
                  cost,
                  imageURL.absoluteString);
        } else {
            NSLog(@"图片加载成功, 来源: %@, 耗时: %.3f 秒, URL: %@",
                  source,
                  cost,
                  imageURL.absoluteString);
        }
    }];
}
  • SDWebImageDownloader:维持图片的下载队列,管理所有的网络图片请求

    • 处理图片的异步加载
    • 支持并发下载,内部维护队列
    • 可以设置下载的优先级、最大并发数、下载超时
  • SDWebImageDownloaderOperation:负责图片的下载请求操作类,是NSOperation的子类

    • 每一个下载请求都会对应一个该类的实例
    • 支持取消下载和合并重复请求
    • 如果想自定义下载行为(如自定义缓存策略、HTTP headers)可以继承这个类
  • SDImageCache:负责图片的缓存

    • 管理缓存内存(NSCache)和磁盘缓存(文件存储)
    • 根据缓存策略判断是否直接从缓存加载图片
    • 支持手动清理缓存
    • 通常也是通过SDWebImageManager自动使用缓存。可以自己设置缓存过期时间、大小限制。
  • SDWebImageManager:下载与缓存的桥梁,核心管理类

    • 维护一个SDWebImageDownloader实例

    • 维护一个SDImageCache实例

    • 提供一个统一的接口

    • 如果我们直接使用UIImageView+WebCache,其实内部就是调用这个类,我们可以自定义manager替换缓存或者下载器

  • SDWebImageDecoder:负责图片的解码与压缩

    • 将下载的NSData转换成UIImage
    • 对大图进行解压缩,提高显示性能,避免UI卡顿
    • 支持同步、异步解码,自动处理GIF、WebP等格式
  • SDWebImageprefetcher:负责图片的预取

    • 可以提前下载一些图片并缓存
    • 适用于滚动列表提前加载图片,提高流畅度
  • UIImageView + WebCache:与UI直接交互的拓展

    • 提供给UIImageView添加异步加载的网络图片能力
    • 内部调用SDWeb Manager

层级框架

最上层的UImageView+WebCache和UIButton+WebCache为最表层接口,直接和UI交互,提供给开发者直接调用

使用示例代码:

objc 复制代码
[imageView sd_setImageWithURL:url placeholderImage:[UI[imageView sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:@"placeholder"]];
[button sd_setImageWithURL:url forState:UIControlStateNormal placeholderImage:[UIImage imageNamed:@"placeholder"]];Image imageNamed:@"placeholder"]]

中间的Manager负责处理和协调下载器和缓存器,并与UIKit层交互

最底层的SDWebImageOperation,负责为两个高层抽象类提供下载支持

详解

sd_setImageWithURL

UIImageView + WebCache举例讲解

该类提供了一些列接口如下:

objc 复制代码
- (void)sd_setImageWithURL:(nullable NSURL *)url;
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder;
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options;
- (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock;

这些接口最终都会调用:

objc 复制代码
- (void)sd_setImageWithURL:(nullable NSURL* )url placeholderImage:(nullable UIImage* )placeholder options:(SDWebImageOptions)options progress:(nullable SDWebImageDownloaderProgressBloack)progressBlock completed:(nullable SDExxternalCompletionBloack)completedBlock;

内部执行流程:

  1. 立即显示占位图
  2. 取消之前的请求,避免发生图片混乱
  3. 先检查内存缓存再检查磁盘缓存
  4. 如果缓存命中直接返回图片并显示
  5. 如果缓存未命中,开始网络下载
  6. 下载完成后对图片进行解码和缓存
  7. 主线程显示最终图片

sd_internalSetImageWithURL

新版本为UIView添加了分类即UIView+WebCache,上述方法最终会调用下述方法:

objc 复制代码
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock
                           context:(nullable NSDictionary<NSString *, id> *)context;
  • 占位图策略:其中的dispatch_main_async_safe是一个宏,保证在主线程安全执行
objc 复制代码
if (!(options & SDWebImageDelayPlaceholder)) {
	dispatch_main_async_safe(^{
    [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
  });    
}

我们看一下这个宏:

objc 复制代码
#define dispatch_main_async_safe(block)\
	if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label)(dispatch_get_main_queue())) {\ 
//如果当前是主线程,同步执行,避免线程切换开销
		block();\
	} else {
    //如果是后台线程,异步派发到主线程
		dispatch_async(dispatch_get_main_queue(), block);
	}

由于只能处理主队列并且不够灵活已经被弃用了

新方案

objc 复制代码
SDCallbackQueue* mainQueue = [SDCallbackQueue mainQueue];
[mainQueue async:^{
	self.image = image;
}];
  • RunLoop问题微拓展

这里有一个runloop延时问题,简单了解了下就是,GCD的主队列是一个串行队列,通过RunLoop来执行任务,当你使用dispatch_async(dispatch_get_main_queue())时,任务被添加到主队列的待执行队列中,但是需要等待当前RunLoop周期结束后才会被处理

RunLoop工作周期

objc 复制代码
//简化流程
while (running) {
	__CFRunLoopServiceMachPort()//阶段1:等待休息,内核级休眠
  __CFRunLoopWakeUp()//被唤醒。处理消息
  __CFRunLoopDoSources0()//开始处理等待事件
  __CFRunLoopDoSources1()
  __CFRunLoopDoBlocks()//处理GCD主队列的block
	__CFRunLoopDoObservers()//执行Observer回调
    
  __CFRunLoopDoObservers()//处理界面更新回调
    //进入下一个周期
}

当我们调用dispatch_async时:

objc 复制代码
// 当前 RunLoop 周期正在执行中
NSLog(@"开始执行任务");

dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"异步添加的任务");
});

NSLog(@"继续执行其他代码");

// 实际执行顺序:
// 1. "开始执行任务"
// 2. "继续执行其他代码" 
// 3. "异步添加的任务" - 在下一个周期执行!
  • 判断url是否合法:如果合法则进行图片下载操作,否则直接block回调失败。生成唯一的validOperationKey,用于表示标识和管理当前图片的记载操作。会将url作为属性绑定到view上
objc 复制代码
if ([url iskindOfClass:NSString.class]) {
	url = [NSURL URLWithString:(NSString* )url];
}
if (![url isKindOfClass:NSURL.class]) {
	url = nil;
}
if (context) {
  context = [context copy];
} else {
  context = [NSDictionary dictionary];
}
NSString* validOperationKey = context[SDWebImageContextSetImageOperationKey];
  • 取消历史申请操作,确保只有一个线程在进行申请
objc 复制代码
NSString* validOperationKey = context[SDWebImageContextSetImageOperationKey];
//获取一个对应的key
	if (!validOperationKey) {//当validOperationKey为空时,生成默认key
		validOperaionKey = NSStringFromClass([self class]);//默认使用当前实例的类名作为key,这就意味着不同类的view不会相互取消彼此的加载
    SDWebImageMutableContext* mutableContext = [context mutableCopy];//避免直接修改传入的不可变context,所以在这里做一个可变拷贝
    mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
    context = [mutableContext copy];
  }
self.sd_latestOperationKey = validOperationKey;//这是一个通过OC关联对象实现的属性,用作记录最新set_image操作的key,便于后续检查与调试
if (!(SD_OPTIONS_CONTAINS(options, SDWebImageAvoidCancleImage))) {//判断条件中的是一个函数用来检查当前options是否包含SDWebImageAvoidCancleImage标志,如果包含避免自动取消那就不取消之前相同key的任务反之则取消。
	[self sd_cancleImageLoadOperationWithKey:validOperationKey];
}
//根据key找到当前对象上保存的图片加载任务,然后取消它,并从任务字典移除
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // 如果外部没有传入 key,就使用当前对象的类名作为默认 key。
    // 例如 UIImageView 的默认 key 就是 @"UIImageView"。
    // 这样可以保证即使 key 为 nil,也能找到一个默认的任务标识。
    if (!key) {
        key = NSStringFromClass(self.class);
    }
    // 获取当前对象上保存图片加载任务的字典。
    // 这个字典通常是通过关联对象绑定到 self 上的。
    // key 用来区分不同的图片加载任务,value 是具体的加载操作对象。
    SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
    // 保存根据 key 找到的图片加载任务。
    id<SDWebImageOperation> operation;
    // 加锁读取任务字典。
    // 因为 NSMutableDictionary 不是线程安全的,
    // 多个线程同时读写可能会导致数据竞争甚至崩溃。
    @synchronized (self) {
        operation = [operationDictionary objectForKey:key];
    }
    // 如果根据 key 找到了对应的图片加载任务
    if (operation) {
        // 判断这个任务对象是否实现了 cancel 方法。
        // 虽然它遵守 SDWebImageOperation 协议,
        // 但这里仍然做一次安全判断,避免对象不支持 cancel 时崩溃。
        if ([operation respondsToSelector:@selector(cancel)]) {
            // 取消当前图片加载任务。
            // 可能是取消下载,也可能是取消解码、查询缓存等操作。
            [operation cancel];
        }
        // 加锁修改任务字典。
        // 将已经取消的任务从字典中移除,
        // 避免后续重复取消,也避免字典继续持有无效任务。
        @synchronized (self) {
            [operationDictionary removeObjectForKey:key];
        }
    }
}
  • 初始化一个manager:从 context 里取出用户自定义的 SDWebImageManager,如果有就用用户指定的 manager;如果没有,就使用全局默认的 sharedManager。取出之后,还要把 SDWebImageContextCustomManager 从 context 里移除,避免继续往下传
objc 复制代码
SDWebImageManager* manager = context[SDWebImageContextCustomManager];
if (!manager) {
  manager = [SDWebImageManager sharedManager];
} else {
	SDwebImageMutableContext* mutableContext = [context mutableCopy];
  mutableContext[SDWebImageContextCustomManager] = nil;
  context = [mutableContext copy];
}
/*
context通常是一个字典,允许调用者额外传入参数
SDWebImageContextCustomManager是一个key用来指定要使用哪一个SDWebImageManager,例如不同配置或者不同缓存策略的manager
为什么持有的话需要快速移除?
我大概了解了一下这个流程:如果发现manager上下文存在,就会出现manager强引用loader,loader会强引用operation,operation会强引用context,context强引用manager的现象,形成循环引用。
*/
  • 判断是否需要一个弱缓存,并依据placeholder显示图片

弱 ? 强引用缓存解析

在iOS中,弱引用缓存与强引用缓存是两种不同的内存管理策略,他们的区别在于如何持有对象以及何时释放对象

特性 弱引用缓存 (Weak Cache) 强引用缓存 (Strong Cache)
持有方式 不增加对象的引用计数 增加对象的引用计数
释放时机 对象无其他强引用时,立即被释放 对象会一直被保留,直到缓存主动移除或清空
内存影响 内存占用低,自动清理 内存占用高,可能需手动管理
典型场景 图片/视图等可重建的大对象缓存 昂贵计算的结果、配置信息等
数据结构 NSMapTable(弱引用值) 或 NSCache(可弱引用) NSDictionary, NSMutableDictionary, NSCache(默认强引用)

SDwebImage的内存缓存实际上是混合策略 ,既有NSCache的默认强引用缓存 ,也有NSMapTable作为弱引用缓存

objc 复制代码
@property(strong, nonatomic)NSCache* memoryCache;//强引用缓存
@property(strong, nonatomic)NSMapTable* weakMemoryCache;//弱引用缓存
//从内存缓存获取图片
UIImage* image = [self.memoryCache objectForKey:key];
if (!image) {
  //检查弱引用缓存,虽然强缓存没了,但是对应的imageview还在强引用它,所以image其实还是没有释放的,仍然可以通过弱缓存读取
  image = [self.weakCache objectForKey:key];
  if (image) {
    //重新放入强应用缓存
    [self.memoryCache setObject:image forKey:key];
  }
}

弱引用缓存使用场景:大对象缓存像图片视频之类的、可以轻松重建的对象、内存敏感的对象、需要自动清理的临时缓存

强引用缓存使用场景:计算成本高的结果、网络请求响应、配置信息、用户数据、需要控制生命周期的对象

objc 复制代码
/*
如果开启了弱引用缓存,在显示展位图之前,先主动检查一次内存缓存,让弱缓存中的图片有机会重新同步回强缓存,避免图片因为被占位图替换而彻底释放。
*/
BOOL shouldUseWeakCache = NO;//表示是否启用了弱内存缓存
if ([manager.imageCache isKindOfClass:SDImageCache.class]) {//只有满足该类型要求才能访问其config那些属性
  //弱引用缓存通常用来存放"可被系统回收但是希望快速重建"的内存对象,一方面可以降低内存压力(被系统回收时自动释放),另一方面在对象尚未回收时可以加速命中
	shouldUseWeakCache = ((SDImageCache* )manager.imageCache).config.shouldUseWeakMemoryCache;
}
if (!(options & SDWebImageDelayPlaceholder)) {//这里取反表示当没有设置延迟显示占位图时,进入if块里脊显示占位图,如果设置了就跳过。
  if (shouldUseWeakCache) {
		NSString* key = [manager cacheKeyForURL:url context:context];
    [((SDImageCache* )manager.imageCache) imageFromMemoryCacheForKey:key];
    /*
    第一步构造该URL+context对应的缓存键,然后直接调用imageFrom...查询内存缓存,注意这里只是为了触发"弱缓存的同步逻辑",而不是为了使用其返回值。
    SDImageCache在实现弱引用缓存时,通常需要将弱引用缓存和强引用缓存做到某种同步状态(即这里的调用imageFromMemoryCacheForKey:来触发WeakCache内部的side-effect,让强弱缓存保持一致),因为后续的常规缓存创建查询会更正式地使用返回值,所以在这里先触发一次短暂的同步提高命中的准确性或者避免race(竞态条件)
    */
  }
  dispatch_main_async_safe(^{
		[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
  });
}

SDIMageCache内部维护了两份内存缓存:

  • memoryCache---强引用---正常的LRU缓存,不希望被随意释放
  • weakMemoryCache---弱引用---可以被系统回收,用于降低内存占用

NSMutableDictionary* memoryCache;

NSMapTable* weakMemoryCache;

图片第一次加载时放入强缓存

当SD清理内存时,强缓存清空,但是弱缓存中仍然可能弱引用着图片对象,如果对象没被系统回收就能被再次获取,反之取到nil

  • 判断url是否存在,如果存在就开始正式的一个加载流程,重置NSProgress、SDWebImageIndicator,完成下方配置的时候,进入下方配置的时候,进入我们的loadImageWithURL函数。当下载完成的时候根据需要决定有没有过度图片,加载结束后通过block返回图片就可以了。
objc 复制代码
if (url) {
    // reset the progress
  //重置下载进度,确保UI中进度干净
    NSProgress *imageProgress = loadState.progress; 
    if (imageProgress) {
        imageProgress.totalUnitCount = 0;
        imageProgress.completedUnitCount = 0;
    }
    
#if SD_UIKIT || SD_MAC
    [self sd_startImageIndicator];//启动一个进度指示器,这里会立即显示,底层自动添加到UIImageView上
    id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif

    SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
      //更新NSProgress,在这里更新的原因是因为SDWebImageDownloader可能每收到少量数据就调用progress,所以我们必须同步更新NSProgress
        if (imageProgress) {
            imageProgress.totalUnitCount = expectedSize;
            imageProgress.completedUnitCount = receivedSize;
        }

#if SD_UIKIT || SD_MAC
        if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
            double progress = 0;
            if (expectedSize != 0) {
                progress = (double)receivedSize / expectedSize;
            }
            progress = MAX(MIN(progress, 1), 0);
            dispatch_async(dispatch_get_main_queue(), ^{
                [imageIndicator updateIndicatorProgress:progress];
            });
        }
#endif

        if (progressBlock) {
            progressBlock(receivedSize, expectedSize, targetURL);
        }//组合回调,就是说SDK内部用progress,但是SDK的使用者也能收到进度
    };

    @weakify(self);
    operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        // 这个方法实现查询内存缓存、磁盘缓存、是否需要下、调用downloader、progressive解码、回调completed、线程调度处理,返回的operation是一个可以去取消的对象
    }];
	//....省略部分代码
    [self sd_setImageLoadOperation:operation forKey:validOperationKey];//这是整个SDWebImage的核心机制之一,将operation挂到UIImageView上用于管理,主要目的是为了防止imageView复用后任务还在跑以及为调用者提供取消功能,同时如果我们传自己的key就能同时加载多个图  
}

时序图

sd_setImageWithURL:

  1. 重置任务进度NSProgress
  2. 显示菊花start indicator
  3. 构建combinedProgressBlock
    1. 更新NSProgress
    2. 更新indictor UI
    3. 回调用户progressBlock
  4. weakify(self)
  5. 调用manager loadImageWithURL
    1. 查缓存、内存
    2. 可能下载
      1. 多次调用progressBlock
    3. 最终调用completedBlock
  6. 保存operationUIImageView

loadImageWithURL(SDWebManager层)

loadImageWithURL是SDWebImageManager中的方法,SDWebImageManager是一个单例,初始化的时候同时初始化了SDImageCache和SDWebImageloader。SDWebImageManager的作用就是调度两个两个对象进行缓存与下载

  • 解析url的正确性
objc 复制代码
if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
  • 创建操作对象,管理下载与缓存的部分
objc 复制代码
SDWebImageCombinedOperation* operation = [SDWebImageCombinedOperation new];
operation.manager = self;
  • 判断当前url是否曾经下载失败过,如果失败过就避免再次重复发起无意义的下载,防止出现多次失败下载。通过递归锁保证了对共享资源的安全访问
objc 复制代码
BOOL isFailedUrl = NO;
    if (url) {//如果url存在就判断一次
        SD_LOCK(_failedURLsLock); //防止并发读写导致的数据竞态或崩溃
        isFailedUrl = [self.failedURLs containsObject:url]; 
        SD_UNLOCK(_failedURLsLock);
    }
//避免重复请求浪费网络/CPU去请求一个失败的URL,使用黑名单策略,提高效率
  • 在真正加载图片之前,生成处理结果对象,统一处理options和context,确保后续所有调用都是标准格式
objc 复制代码
//预处理选项和上下文参数确定最终的结果
    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
  • 如果URL是无效的或是失败的URL没有重复重试选项,立即完成回调并返回错误信息
objc 复制代码
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
        NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
  
  //回调block,通知外部失败
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] queue:result.context[SDWebImageContextCallbackQueue] url:url];
        return operation;
    }

我们详细了解一下第二个短路条件:

options & SDWebImageRetryFailed:判断用户有没有设置允许重试以前下载失败的URL,这是SDwebImage的失败URL黑名单机制

内部根据不同失败形式返回不同的错误码,并且回调错误给调用者:调用用户本来传进来的completion block,同时传入NSError

如果上述操作没有问题就将当前操作加到操作队列中并预处理最终结果

objc 复制代码
SD_LOCK(_runningOperationsLock);
[self.runningOperations addObject"operation];
 SD_UNLOCK(_runningOperationsLock);
  • 最后进入callCacheProcessforOperation这个函数,核心作用就是在SDWebImage真正下载图片之前,先去缓存中查找,如果缓存中有图片,就带着缓存结果进入后续流程,如果缓存没有就继续走下载流程。
objc 复制代码
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock {
    // Grab the image cache to use
    //获取需要查询的缓存图像,如果外部传了自定义缓存就直接使用,否则使用默认缓存策略
    id<SDImageCache> imageCache = context[SDWebImageContextImageCache];
    if (!imageCache) {
        imageCache = self.imageCache;
    }
    // Get the query cache type
    //获取缓存查询类型,默认查询所有类型的缓存(内存和磁盘)
    SDImageCacheType queryCacheType = SDImageCacheTypeAll;
    if (context[SDWebImageContextQueryCacheType]) {
        queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
    }
    
    // Check whether we should query cache
    //检查是否应该查询缓存
    BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
    if (shouldQueryCache) {
        // transformed cache key
        // 根据url与上下文生成缓存键
        NSString *key = [self cacheKeyForURL:url context:context];
        // to avoid the SDImageCache's sync logic use the mismatched cache key
        // we should strip the `thumbnail` related context
        //为了避免SDImageCache的同步逻辑使用不匹配的缓存键,我们需要移除与缩略图相关的上下文
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextImageThumbnailPixelSize] = nil;
        mutableContext[SDWebImageContextImagePreserveAspectRatio] = nil;
        @weakify(operation);
        //查询缓存的操作
        operation.cacheOperation = [imageCache queryImageForKey:key options:options context:mutableContext cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
            @strongify(operation);
            if (!operation || operation.isCancelled) {
                // 如果操作被取消或是不存在则构建一个错误
                // Image combined operation cancelled by user
                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] queue:context[SDWebImageContextCallbackQueue] url:url];
                // 安全从运行操作列表中移除操作(SDwebImageManager内部会维护一个正在运行的操作列表)
                [self safelyRemoveOperationFromRunning:operation];
                return;
            } else if (!cachedImage) { //如果缓存中图片不存在,再去查询原始缓存
                NSString *originKey = [self originalCacheKeyForURL:url context:context];
                BOOL mayInOriginalCache = ![key isEqualToString:originKey];
                // Have a chance to query original cache instead of downloading, then applying transform
                // Thumbnail decoding is done inside SDImageCache's decoding part, which does not need post processing for transform
                if (mayInOriginalCache) {// 可能存在在原始缓存中,就用原始缓存查询流程
                    [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
                    return;
                }
            }
            // Continue download process
            //启用下载流程
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {
        // 直接启用下载流程
        // Continue download process
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
    }
}

获取将要使用的缓存对象

确定查询缓存的类型,然后判断是否毒药查询缓存,如果设置了SDWebImageFromLoaderOnly就直接跳转到下载流程,反之先构建缓存key:会移除缩略图等信息,避免缩略信息重复处理等。然后执行缓存查询,如果找到缓存图像就直接进入下载或者处理流程。如果没有找到,会先尝试查找原始缓存key,看有没有原图缓存。如果还是没有就进入下载流程。

qureyCacheOperationForKey

  • 通过queryImageForKey调用qureyCacheOperationForKey这个函数。我们先了解一下有关缓存配置的类别,保存缓存策略信息。
objc 复制代码
@implementation SDImageCacheConfig
- (instancetype)init {
    if (self = [super init]) {
        _shouldDisableiCloud = YES;//缓存文件写入磁盘时,决定是否需要备份到iCloud,因为图片缓存通常是可再生数据。
        _shouldCacheImagesInMemory = YES;//决定是否启用强引用的内存缓存,内存缓存可以显著提高加载速度并且减少解码、磁盘I/O,代价就是占用内存
        _shouldUseWeakMemoryCache = NO;//决定是否开启弱引用的内存缓存,如果设置为NO的话就是使用默认的强引用缓存,如果设置为yes的话,实现上会把缓存同时保存在一个弱引用容器中,这样在内存紧张时系统可以释放这些对象
        _shouldRemoveExpiredDataWhenEnterBackground = YES;//当app进入后台时是否清理过期磁盘缓存
        _shouldRemoveExpiredDataWhenTerminate = YES;//当app终止时是否清理过期磁盘缓存,与上面的属性共同决定是否在合适的生命周期时间节点清理过期数据,维持磁盘缓存的整洁与节省存储空间
        _diskCacheReadingOptions = 0;
        _diskCacheWritingOptions = NSDataWritingAtomic;
        _maxDiskAge = kDefaultCacheMaxDiskAge;//磁盘中的最大年龄
        _maxDiskSize = 0;//磁盘缓存允许的最大字节缓存数,0为不限制
        _diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate;//决定判断文件过期的时间属性
        _fileManager = nil;//用于文件操作的NSFileManager实例,nil会使用库内部默认的NSFileManager,如果想自定义可以在外部传入自定义实例
        if (@available(iOS 10.0, tvOS 10.0, macOS 10.12, watchOS 3.0, *)) {
          //带自动释放池的串行队列属性。对非ARC或需要在队列任务中自动管理autorelease对象时很有用,可以减少内存的瞬时峰值
            _ioQueueAttributes = DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL; // DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM(一种更细粒度的autorelease策略)
        } else {
            _ioQueueAttributes = DISPATCH_QUEUE_SERIAL; // NULL
        }//整体目的就是为了在I/O操作的串行队列上保证正确的内存管理和稳定性
        _memoryCacheClass = [SDMemoryCache class];
        _diskCacheClass = [SDDiskCache class];
      //指定用来实际实现内存缓存和崔磁盘缓存的类,默认指向库自带的SDMemoryCache和SDDiskCache
    }
    return self;
}

几种内存策略

objc 复制代码
typedef NS_ENUM(NSInetger, SDImageCachType) {
  SDImageCacheTypeNone,//图片需要从网络下载
  SDImageCacheTypeDisk,//图片来源于磁盘缓存
  SDImageCacheTypeMemory,//图片来源于内存缓存
  SDImageCacheTypeAll//表示请求操作,同时查询内存和磁盘缓存
};

SDImageCache缓存类的初始化方法

  • 首先我们看方法签名
objc 复制代码
// SDImageCache 的初始化方法
// ns:缓存命名空间,用于区分不同缓存目录,例如 default、avatar、banner 等
// directory:磁盘缓存根目录,如果为 nil,则使用 SDWebImage 默认缓存目录
// config:缓存配置对象,如果为 nil,则使用默认配置
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns diskCacheDirectory:(nullable NSString *)directory config:(nullable SDImageCacheConfig *)config {
    // 调用父类 init,初始化 NSObject 基础部分
    if ((self = [super init])) {
        // 缓存命名空间不能为空
        // 因为后面会用 ns 拼接磁盘缓存路径
        NSAssert(ns, @"Cache namespace should not be nil");
        // 如果外部没有传入缓存配置,就使用默认配置
        if (!config) {
            config = SDImageCacheConfig.defaultCacheConfig;
        }
        
        // 拷贝一份配置,防止外部后续修改 config 影响当前缓存对象
        _config = [config copy];
        // 创建磁盘 IO 队列
        // 从配置中取出 IO 队列属性
        // 通常用于决定队列是串行还是并发、QoS 等
        dispatch_queue_attr_t ioQueueAttributes = _config.ioQueueAttributes;
        
        // 创建一个专门处理磁盘缓存读写的队列
        // 磁盘读取、写入、删除、清理等操作会放到这个队列中执行
        _ioQueue = dispatch_queue_create("com.hackemist.SDImageCache.ioQueue", ioQueueAttributes);
        
        // 断言 IO 队列创建成功
        // 如果这里失败,通常说明配置的 ioQueueAttributes 有问题
        NSAssert(_ioQueue, @"The IO queue should not be nil. Your configured `ioQueueAttributes` may be wrong");
        
        //初始化内存缓存   
        // 检查自定义的内存缓存类是否遵守 SDMemoryCache 协议
        // SDWebImage 允许外部替换内存缓存实现,但必须符合协议规范
        NSAssert([config.memoryCacheClass conformsToProtocol:@protocol(SDMemoryCache)],
                 @"Custom memory cache class must conform to `SDMemoryCache` protocol");
        
        // 根据配置中的 memoryCacheClass 创建内存缓存对象
        // 默认一般是 SDMemoryCache
        _memoryCache = [[config.memoryCacheClass alloc] initWithConfig:_config];
        
        // ================================
        // 3. 初始化磁盘缓存路径
        // ================================
        
        // 如果外部没有传入磁盘缓存目录,就使用默认缓存目录
        if (!directory) {
            // 获取 SDWebImage 默认磁盘缓存根目录
            directory = [self.class defaultDiskCacheDirectory];
        }
        
        // 拼接最终磁盘缓存目录
        // 例如:
        // directory = .../Library/Caches/com.hackemist.SDImageCache/default
        // ns = @"default"
        // _diskCachePath = .../Library/Caches/com.hackemist.SDImageCache/default/default
        //
        // ns 的作用是把不同业务或不同缓存实例的数据隔离开
        _diskCachePath = [directory stringByAppendingPathComponent:ns];
        
        // ================================
        // 4. 初始化磁盘缓存
        // ================================
        
        // 检查自定义磁盘缓存类是否遵守 SDDiskCache 协议
        // SDWebImage 允许外部替换磁盘缓存实现,但必须符合协议规范
        NSAssert([config.diskCacheClass conformsToProtocol:@protocol(SDDiskCache)],
                 @"Custom disk cache class must conform to `SDDiskCache` protocol");
        
        // 根据配置中的 diskCacheClass 创建磁盘缓存对象
        // cachePath 是最终缓存路径
        // config 是缓存配置,例如最大缓存大小、最大缓存时间、文件管理策略等
        _diskCache = [[config.diskCacheClass alloc] initWithCachePath:_diskCachePath
                                                               config:_config];
        
        // ================================
        // 5. 检查并迁移旧缓存目录
        // ================================
        
        // 用于兼容旧版本 SDWebImage 的缓存目录结构
        // 如果检测到旧路径存在,可能会把旧缓存迁移到新的缓存路径
        [self migrateDiskCacheDirectory];

#if SD_UIKIT
        // 监听应用即将终止通知
        // 应用退出前可以清理过期缓存、完成必要的磁盘操作
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil];

        // 监听应用进入后台通知
        // 进入后台时通常会触发磁盘缓存清理,避免缓存无限增长
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:)name:UIApplicationDidEnterBackgroundNotification object:nil];
#endif

#if SD_MAC
        // macOS 应用即将退出时,执行对应的缓存处理逻辑
      [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:nil];
#endif
    }
    // 返回初始化完成的 SDImageCache 实例
    return self;
}

NSAssert底层实现机制:

objc 复制代码
[[NSAssertionHandler currentHandler] 
    handleFailureInMethod:_cmd 
    object:self 
    file:[NSString stringWithUTF8String:__FILE__]
    lineNumber:__LINE__
    description:@"Cache namespace should not be nil"];

DEBUG模式:用于开发阶段运行App,可以看到完整日志,可以使用断点调试,NSAssert有效时会检查会崩溃,较快且准确

RELEASE模式:用于发布到App Store或生产环境的版本,不含调试符号,优化编译速度与运行速度,NSAssert失效不执行,代码更快

简而言之就是用来调试的,开发时需要,发布后为了避免对用户产生影响就不让他执行报错。
运行时检查机制:

objc 复制代码
BOOL conforms = NO;
Class cls = config.memoryCacheClass;
while (cls) {
	if (class_conformsToProtocol(cls, @protocol(SDMemoryCache))) {
		conforms = YES;
    break;
  }
  cls = class_getSuperclass(cls);
}

接下来进入缓存的核心逻辑,先查内存缓存,必要时查询磁盘缓存,并支持同步/异步查询、取消操作、回调队列切换等操作:

objc 复制代码
- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
    if (!key) {//没有查询键直接返回
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    if (queryCacheType == SDImageCacheTypeNone) {//查询类型是none也直接结束
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    UIImage *image;
    if (queryCacheType != SDImageCacheTypeDisk) {//先查内存缓存
        image = [self imageFromMemoryCacheForKey:key];
    }

    if (image) {
        if (options & SDImageCacheDecodeFirstFrameOnly) {//处理动画图,只解码动画图的第一帧
            if (image.sd_imageFrameCount > 1) {
#if SD_MAC
                image = [[NSImage alloc] initWithCGImage:image.CGImage
                                                   scale:image.scale
                                             orientation:kCGImagePropertyOrientationUp];
#else
                image = [[UIImage alloc] initWithCGImage:image.CGImage
                                                   scale:image.scale
                                             orientation:image.imageOrientation];
#endif
            }
        } else if (options & SDImageCacheMatchAnimatedImageClass) {//检查缓存中取出的图片类型是否是我们需要的类型
            Class animatedImageClass = image.class;
            Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
            if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
                image = nil;//直接置空意味着取缓存失败
            }
        }
    }

    BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));//判断是否可以直接结束,不用查询磁盘

    if (shouldQueryMemoryOnly) {//如果只查内存就直接回调
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }

  //如果执行到这里,说明好还是需要查询磁盘缓存,所以创建一个查询token
    SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];
    SDImageCacheToken *operation = [[SDImageCacheToken alloc] initWithDoneBlock:doneBlock];
    operation.key = key;
    operation.callbackQueue = queue;

  //判断查询是同步还是异步,如果内存已经命中,但是还要同步拿磁盘data。如果内存未命中,还是要同步查询磁盘data
    BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                                (!image && options & SDImageCacheQueryDiskDataSync));
//定义磁盘查询饿block
    NSData* (^queryDiskDataBlock)(void) = ^NSData* {
        @synchronized (operation) {//日过任务已经被取消就直接返回nil,不再查询磁盘
            if (operation.isCancelled) {
                return nil;
            }
        }

        return [self diskImageDataBySearchingAllPathsForKey:key];
    };

  //定义根据data生成图片的block,并在必要时同步写入内存缓存
    UIImage* (^queryDiskImageBlock)(NSData*) = ^UIImage*(NSData* diskData) {
        @synchronized (operation) {
            if (operation.isCancelled) {
                return nil;
            }
        }

        UIImage *diskImage;

        if (image) {//如果内存缓存已经读取到图片了直接使用就行,此时磁盘数据没必要进行解码操作
            diskImage = image;
        } else if (diskData) {//如果内存没有但是磁盘有data就进行解码操作
            BOOL shouldCacheToMemory = YES;//判断是否需要写回内存缓存,默认写
            if (context[SDWebImageContextStoreCacheType]) {//读取上下文判断具体的缓存策略
                SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
                shouldCacheToMemory = (cacheType == SDImageCacheTypeAll ||
                                       cacheType == SDImageCacheTypeMemory);
            }

          //解码前再次检查内存缓存,避免发生重复解码的问题
            if (shouldCacheToMemory && self.config.shouldCacheImagesInMemory) {
                diskImage = [self.memoryCache objectForKey:key];
            }

            if (!diskImage) {
                diskImage = [self diskImageForKey:key data:diskData options:options context:context];

                if (shouldCacheToMemory) {//将解码后的图片同步到内存缓存中
                    [self _syncDiskToMemoryWithImage:diskImage forKey:key];
                }
            }
        }

        return diskImage;
    };

    if (shouldQueryDiskSync) {
      /*
      使用同步串行队列:
      避免多个线程同时读写磁盘缓存;
			保证磁盘缓存操作有序;
			避免文件读写竞争。
      */
        __block NSData* diskData;
        __block UIImage* diskImage;

        dispatch_sync(self.ioQueue, ^{
            diskData = queryDiskDataBlock();
            diskImage = queryDiskImageBlock(diskData);
        });

        if (doneBlock) {
            doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
        }
    } else {
      /*
      异步查询,不阻塞当前线程
      */
        dispatch_async(self.ioQueue, ^{
            NSData* diskData = queryDiskDataBlock();
            UIImage* diskImage = queryDiskImageBlock(diskData);

            @synchronized (operation) {//检查任务是否取消
                if (operation.isCancelled) {
                    return;
                }
            }

            if (doneBlock) {
              //如果context中指定了回调队列,就使用指定的,否则默认主队列
                [(queue ?: SDCallbackQueue.mainQueue) async:^{
                    @synchronized (operation) {//再次判断,IO队列切换到主队列有时间差
                        if (operation.isCancelled) {
                            return;
                        }
                    }

                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                }];
            }
        });
    }

    return operation;
}

我们使用自然语言描述一下这段代码:

  • 首先判断key是否为空
    • 如果为空直接回调结果,返回nil
    • 否则进入下一步
  • 判断queryCacheType是否为None
    • 如果是,直接回调结果返回nil
    • 否则进入下一步
  • 判断是否允许查内存缓存,如果返回是,进入下一步
  • 判断内存缓存是否命中,如果命中就根据options处理动画图/类型匹配
  • 判断是否只需要内存结果
    • 如果是,直接回调image,返回nil
    • 否,进入下一步
  • 创建SDimageCacheToken,判断是否同步查磁盘缓存,如果查询就根据data生成image,必要时同步写回内存缓存中
  • 同步或者异步回调doneBlock
  • 返回operation

删除缓存

删除缓存的过程需要一个fileManager迭代器取获取需要删除的图片文件,默认最大时间限制为1周,计算出需要被清除的时间范围,遍历这个范围内需要删除的图片,从磁盘删除。再根据缓存策略中的最大缓存大小判断是否需要进一步的缓存清理。

callDownloadProcessForOperation

这个方法是缓存查询结束之后,决定是否继续走下载流程。它接收缓存查询结果 cachedImage / cachedData / cacheType,然后根据 options、代理、loader 能力等条件,决定:下载、直接返回缓存图、还是返回空结果

主要有三种情况:

情况一:需要下载 → 走 imageLoader 网络请求

情况二:不下载,但有缓存图 → 直接回调缓存图

情况三:不下载,也没有缓存图 → 回调 nil

objc 复制代码
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                    url:(nonnull NSURL *)url
                                options:(SDWebImageOptions)options
                                context:(SDWebImageContext *)context
                            cachedImage:(nullable UIImage *)cachedImage
                             cachedData:(nullable NSData *)cachedData
                              cacheType:(SDImageCacheType)cacheType
                               progress:(nullable SDImageLoaderProgressBlock)progressBlock
                              completed:(nullable SDInternalCompletionBlock)completedBlock {
    //方法调用时,缓存查询已经结束
    @synchronized (operation) {
        operation.cacheOperation = nil;
    }
    //获取图片加载器
    id<SDImageLoader> imageLoader = context[SDWebImageContextImageLoader];
    if (!imageLoader) {
        imageLoader = self.imageLoader;
    }
    
    BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
    shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
    shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);//给外部代理一个拦截机会,如果代理没有实现这个方法,就默认同意下载
    
    if ([imageLoader respondsToSelector:@selector(canRequestImageForURL:options:context:)]) {
        shouldDownload &= [imageLoader canRequestImageForURL:url options:options context:context];//询问imageLoader能否处理这个url
    } else {
        shouldDownload &= [imageLoader canRequestImageForURL:url];
    }
    
    if (shouldDownload) {
      //如果确定要下载了,也有两种情况,第一种就是有缓存图并且要求刷新。先回调缓存图,然后继续下载新图。可能出现两次回调
        if (cachedImage && options & SDWebImageRefreshCached) {
            [self callCompletionBlockForOperation:operation
                                       completion:completedBlock
                                            image:cachedImage
                                             data:cachedData
                                            error:nil
                                        cacheType:cacheType
                                         finished:YES
                                            queue:context[SDWebImageContextCallbackQueue]
                                              url:url];
            
            SDWebImageMutableContext *mutableContext;
            if (context) {
                mutableContext = [context mutableCopy];
            } else {
                mutableContext = [NSMutableDictionary dictionary];
            }
            mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;//往上下文中塞入缓存图,通知后面的图片加载器。比如使用 HTTP 缓存机制时,loader 可以根据缓存图或者缓存数据判断远端图片是否变化。
            context = [mutableContext copy];
        }
        
        @weakify(operation);
        operation.loaderOperation = [imageLoader requestImageWithURL:url
                                                             options:options
                                                             context:context
                                                            progress:progressBlock
                                                           completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
            @strongify(operation);
            
            if (!operation || operation.isCancelled) {//如果下载任务主动被用户取消,这里就回调一个取消错误
                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] queue:context[SDWebImageContextCallbackQueue] url:url];
            } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
                //服务器返回信息表示图片没变化
            } else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {//也是一个取消错误,与前者不同的是,这里是底层的downloader/loader通知的。这不是URL的问题,不能讲url加入失败黑名单
                [self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];
            } else if (error) {//真正的处理失败
                [self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];
           
                BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error options:options  context:context];//判断是否需要将这个url加入黑名单,避免反复请求失败的url
                
                if (shouldBlockFailedURL) {
                    SD_LOCK(self->_failedURLsLock);
                    [self.failedURLs addObject:url];//共享集合,加锁避免并发修改
                    SD_UNLOCK(self->_failedURLsLock);
                }
            } else {//下载成功
                if ((options & SDWebImageRetryFailed)) {//请求可能是对失败 URL 的重试。既然现在成功了,就要从失败列表里移除
                    SD_LOCK(self->_failedURLsLock);
                    [self.failedURLs removeObject:url];
                    SD_UNLOCK(self->_failedURLsLock);
                }
                
              //处理图片转换
                [self callTransformProcessForOperation:operation
                                                   url:url
                                               options:options
                                               context:context
                                         originalImage:downloadedImage
                                          originalData:downloadedData
                                             cacheType:SDImageCacheTypeNone
                                              finished:finished
                                             completed:completedBlock];
            }
            
            if (finished) {
              //加载已完成,将这个operation从manager任务列表中移除
                [self safelyRemoveOperationFromRunning:operation];
            }
        }];
    } else if (cachedImage) {//不需要网络下载,但是缓存中有图片。直接回调缓存图
        [self callCompletionBlockForOperation:operation
                                   completion:completedBlock
                                        image:cachedImage
                                         data:cachedData
                                        error:nil
                                    cacheType:cacheType
                                     finished:YES
                                        queue:context[SDWebImageContextCallbackQueue]
                                          url:url];
        [self safelyRemoveOperationFromRunning:operation];
    } else {//缓存没图,而且当前又不允许下载。回调空结果
        [self callCompletionBlockForOperation:operation
                                   completion:completedBlock
                                        image:nil
                                         data:nil
                                        error:nil
                                    cacheType:SDImageCacheTypeNone
                                     finished:YES
                                        queue:context[SDWebImageContextCallbackQueue]
                                          url:url];
        [self safelyRemoveOperationFromRunning:operation];
    }
}

其中的requestImageWithURL这个函数会调用downloadImageWithURL,通过这个函数实现核心下载流程:

downloadImageWithURL

根据 URL 创建或复用一个图片下载任务,并把当前调用者的 progressBlock 和 completedBlock 注册到这个下载任务上,最后返回一个可用于取消下载的 token。

objc 复制代码
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    if (url == nil) {//基本判空处理,立刻回调失败
        if (completedBlock) {
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL  userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
            completedBlock(nil, nil, error, YES);
        }
        return nil;
    }

    id downloadOperationCancelToken;//一个在下载操作内部取消操作的标识,一个operation可能被多个地方复用。假设有三个试图复用同一张图片,SDWebImage 不会创建 3 个网络请求,而是创建 1 个下载 operation,然后给这个 operation 挂 3 组回调。所以每一组回调都需要一个自己的 cancel token。
    id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
    NSString *cacheKey;//根据url生成缓存key
    if (cacheKeyFilter) {
        cacheKey = [cacheKeyFilter cacheKeyForURL:url];
    } else {
        cacheKey = url.absoluteString;
    }

    SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey);//根据 context、下载选项和 cacheKey 生成图片解码相关的配置。传key是为了适应不同图片尺寸处理场景

    SD_LOCK(_operationsLock);//加锁保护当前正在下载的任务的执行,下面这些操作都需要被保护:查找 operation、判断能不能复用、创建 operation、写入 URLOperations、添加回调、加入下载队列

    NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];//找当前url是否有对应下载任务正在进行

    BOOL shouldNotReuseOperation;//判断是否不应该复用
    if (operation) {
        @synchronized (operation) {//用 @synchronized 保证读取状态时和 operation 内部状态修改逻辑互斥,避免线程安全问题。
            shouldNotReuseOperation = operation.isFinished || operation.isCancelled;
        }
    } else {
        shouldNotReuseOperation = YES;//没有旧任务必须重建
    }

    if (shouldNotReuseOperation) {//创建新的operation
        operation = [self createDownloaderOperationWithUrl:url
                                                   options:options
                                                   context:context];

        if (!operation) {//创建operation失败,直接回调错误。注意需要先解锁再return,免得锁没释放,卡住后面的任务
            SD_UNLOCK(_operationsLock);

            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain
                                                     code:SDWebImageErrorInvalidDownloadOperation
                                                 userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}];
                completedBlock(nil, nil, error, YES);
            }

            return nil;
        }

        @weakify(self);
        operation.completionBlock = ^{//定义下载任务结束回调block,从字典中移除operation
            @strongify(self);
            if (!self) {
                return;
            }

            SD_LOCK(self->_operationsLock);
            [self.URLOperations removeObjectForKey:url];
            SD_UNLOCK(self->_operationsLock);
        };

        [self.URLOperations setObject:operation forKey:url];

      //给 operation 添加进度回调和完成回调,后续调用
        downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock
                                                               completed:completedBlock
                                                            decodeOptions:decodeOptions];

        [self.downloadQueue addOperation:operation];//真正的将operation加入到下载队列中
    } else {
      //如果当前 URL 已经有正在下载的 operation,就不创建新的网络请求,而是直接把当前调用者的回调加到已有 operation 里,即SDWebImage 下载合并的核心机制。
        @synchronized (operation) {
            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
        }
    }

    SD_UNLOCK(_operationsLock);

    SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];//创建返回给外部的token,提供外部取消下载请求的接口(只取消那一组回调)
    token.url = url;
    token.request = operation.request;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

storeImage

在下载任务执行结束之后,会继续执行转换与缓存管理,核心是保存,代码如下:

objc 复制代码
if (image && toMemory && self.config.shouldCacheImagesInMemory) {
    NSUInteger cost = image.sd_memoryCost;
    [self.memoryCache setObject:image forKey:key cost:cost];//写入缓存
}
//判断是否需要将图片存入缓存以及计算内存成本

接下来判断是否需要存储的磁盘

objc 复制代码
if (!toDisk) {
  if (completionBlock) {
    completionBlock();
  }
  return;
}
//....
dispatch_async(self.ioQueue, ^{
    [self _storeImageDataToDisk:encodedData forKey:key];
    [self _archivedDataWithImage:image forKey:key];
    if (completionBlock) {
        [(queue ?: SDCallbackQueue.mainQueue) async:^{
            completionBlock();
        }];
    }
});

setImage

在下载成功之后,层层回调将数据沿着SDWebImageDownloaderOperation->SDWebImageDownloader->SDWebImageManager->UIView+WebCache链路流动。

sd_internalSetImageWithURL中,将经过一系列外部配置后的图片在主线程中调用sd_setImage更新。

objc 复制代码
if (setImageBlock) {
    finalSetImageBlock = setImageBlock;
} else if ([view isKindOfClass:[UIImageView class]]) {
    UIImageView *imageView = (UIImageView *)view;
    finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
        imageView.image = setImage;
    };
}
#if SD_UIKIT
else if ([view isKindOfClass:[UIButton class]]) {
    UIButton *button = (UIButton *)view;
    finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
        [button setImage:setImage forState:UIControlStateNormal];
    };
}
#endif
相关推荐
时空自由民.3 小时前
ESP ADF音频篇章
macos·音视频·xcode
lijfrank19 小时前
Mac卸载NTFS工具后无法读取硬盘?我的2天排错心路与终极解决方案
macos·ntfs
MonkeyKing1 天前
消息发送与转发流程
ios
吃鱼的灰太狼1 天前
Mac本地部署大模型|Ollama+Gemma4/Qwen3.5新手零失败教程,彻底告别Token消耗✨
macos
代码的小搬运工1 天前
Masonry学习
学习·macos·cocoa
yangSnowy1 天前
mac系统安装hyperf框架swoole扩展
后端·macos·swoole
移动端小伙伴1 天前
我受够了 Xcode 的 SPM 网络问题,写了个脚本一劳永逸
ios
A charmer1 天前
从 C++ 到 Objective-C:零基础平滑转学专栏【总目录】
开发语言·c++·objective-c
人月神话-Lee1 天前
两个改动,让这个iOS OCR SDK识别成功率翻了一倍
ios·ocr·ai编程·身份证识别·银行卡识别