iOS——SDWebImage解读

前言

在iOS的图片加载框架中,SDWebImage占据了大半壁江山。它提供了UIImageView的一个分类,支持从网络中下载且缓存图片,并设置图片到对应的UIImageView控件或者UIButton控件。在项目中使用SDWebImage来管理图片加载相关操作可以极大地提高开发效率,让我们更加专注于业务逻辑实现。

SDWebImage简介

功能特性

  • 提供了一个UIImageView的category用来加载网络图片并且对网络图片的缓存进行管理
  • 采用异步方式来下载网络图片
  • 采用异步方式,使用内存+磁盘来缓存网络图片,拥有自动的缓存过期处理机制
  • 支持的图片格式包括 PNG,JPEG,GIF,Webp等
  • 支持GIF动图(4.0 之前的动图效果并不是太好,4.0 以后基于 FLAnimatedImage加载动图)
  • 支持后台图片解压缩处理
  • 支持Arm64
  • 同一个URL的图片不被重复下载
  • 失效,虚假的URL不会被无限重试
  • 耗时操作都在子线程,确保不会阻塞主线程
  • 使用GCD和ARC

官方图解

主序列图(Main Sequence Diagram)

请添加图片描述

顶层API图(Top Level API Diagram)

请添加图片描述

整体类图(Overall Class Diagram)

请添加图片描述

SDWebImage功能

缓存

为了减少网络流量的消耗,我们都希望下载下来的图片缓存到本地,下次再去获取同一张图片时,可以直接从本地获取,而不再从远程服务器获取。这样做的另一个好处是提升了用户体验,用户第二次查看同一幅图片时,能快速从本地获取图片直接呈现给用户,不用经过繁琐的网络请求。

SDWebImage 提供了对图片缓存的支持,而该功能是由SDImageCache 类来完成的。该类负责处理内存缓存及一个可选的磁盘缓存。其中磁盘缓存的写操作是异步的,这样就不会对 UI 操作造成影响。

SDWebImage 的图片缓存采用的是 Memory(内存)Disk(硬盘) 双重 Cache 机制,SDImageCache 中有一个叫做 memCache 的属性,它是一个 NSCache 对象,用于实现我们对图片的 Memory Cache内存缓存,其实就是接受系统的内存警告通知,然后清除掉自身的图片缓存。对于 Disk Cache磁盘缓存SDWebImage 会将图片存放到 NSCachesDirectory 目录中,然后为每一个缓存文件生成一个 md5 文件名,存放到文件中。

查找缓存机制如下:

  1. Memory(内存)中查找:SDImageCache 类的 queryDiskCacheForKey 方法用于查询图片缓存。queryDiskCacheForKey 方法先会查询 Memory Cache ,如果查找到就直接返回,反之进入下面的硬盘查找。

  2. Disk(磁盘) 中查找:如果 Memory Cache 查找不到, 就会查询 Disk Cache。就是如果 Disk Cache 查询成功,会把得到的图片再次设置到 Memory Cache 中, 以便最大化那些高频率展现图片的效率。如果找不到就进入下面的网络下载。

  3. 网络下载: imageDownloader 属性负责请求网络,下载图片数据。 如果下载失败,会把失败的图片地址写入 failedURLs 集合。这个集合用于存放所有加载失败图片的URL,用于 SDWebImage 的一个机制:对上次加载失败的图片拒绝再次加载。 也就是说,一张图片在本次会话加载失败了,如果再次加载就会直接拒绝,这样提高了图片加载的性能。如果下载图片成功了,接下来就会使用 [self.imageCache storeImage] 方法将它写入缓存 ,同时也会写入硬盘,并且调用 completedBlock 告诉前端显示图片。

  4. Disk(硬盘)缓存清理策略:SDWebImage 会在每次 APP 结束的时候执行清理任务。 清理缓存的规则分两步进行: 第一步先清除掉过期的缓存文件。 如果清除掉过期的缓存之后,空间还不够,那么执行第二步:按文件时间从早到晚排序,先清除最早的缓存文件,直到剩余空间达到要求。

内存缓存

内存缓存的处理是使用 NSCache 对象来实现的。NSCache 是一个类似于集合的容器。它存储 key-value 对,这一点类似于 NSDictionary 类。

我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃,所以有Disk(硬盘)缓存清理策略。

磁盘缓存

SDWebImage 使用 NSFileManager 对象来实现磁盘缓存,图片存储的位置位于Cache文件夹。另外,SDImageCache 还定义了一个串行队列,来异步存储图片。

SDImageCache 提供了大量方法来缓存、获取、移除及清空图片。而对于每个图片,为了方便地在内存或磁盘中对它进行这些操作,我们需要一个 key 值来索引它。 在内存中,我们将其作为 NSCachekey 值,而在磁盘中,我们用这个 key 作为图片的文件名。 对于一个远程服务器下载的图片,其 url 理所当然作为这个 key 值。

组织架构

请添加图片描述

核心类

  • SDWebImageDownloader: 负责维持图片的下载队列,是一个单例对象
  • SDWebImageDownloaderOperation: 负责真正的图片下载请求,一个自定义的并行Operation子类
  • SDImageCache: 负责SDWebImage的缓存工作,是一个单例对象
  • SDWebImageManager: 是总的管理类,维护了一个SDWebImageDownloader实例和一个SDImageCache实例,是下载与缓存的桥梁
  • SDWebImageDecoder: 负责图片的解压缩
  • SDWebImagePrefetcher: 负责图片的预取
  • UIImageView+WebCache: 和其他的扩展都是与用户直接打交道的

概念框架

请添加图片描述

  • UIImageView+WebCacheUIButton+WebCache直接为表层的UIKit框架提供接口
  • SDWebImageManger负责处理和协调SDWebImageDownloaderSDWebImageCache,并与 UIKit层进行交互
  • SDWebImageDownloaderOperation真正执行下载请求,最底层的两个类为高层抽象提供支持

源码解读

从使用开始说UIImageView+WebCache

  • 引用 #import "UIImageView+WebCache.h" 头文件
  • 加载图片地址
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:imageView];
    
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.xxx.com/image.jpg"]];
  • SDWebImage也提供了下列其他的加载方法:
- (void)sd_setImageWithURL:(nullable NSURL *)url {
    [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}

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

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

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

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

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

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

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:options context:nil progress:progressBlock completed:completedBlock];
}               
  • 这些最终都是调用一个全能方法。全能方法除了必需的的图片地址,还提供了占位图、可选项、加载进度和完成回调。
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 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 {
    [self sd_setImageWithURL:url 
    		placeholderImage:placeholder 
    				 options:options 
    				 context:nil 
    				progress:progressBlock 
    			   completed:completedBlock];
}
  • 可以发现,全能方法并没有什么实际的实现,还是对另一个方法的封装,这个方法在另一个分类UIView+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);
                               }
                           }];
}
  • 这个方法也是调用了一个全能方法,下面是其核心实现:
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock {
    if (context) {
        // copy to avoid mutable object
        // 复制以避免可变对象
        context = [context copy];
    } else {
        context = [NSDictionary dictionary];
    }
    // 生成一个有效的操作密钥
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    // 如果传入了参数就用传入的,否则就用当前类的类名
    if (!validOperationKey) {
        // pass through the operation key to downstream, which can used for tracing operation or image view class
        // 通过操作键传递到下游,可用于跟踪操作或图像视图类
        validOperationKey = NSStringFromClass([self class]);
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        context = [mutableContext copy];
    }
    self.sd_latestOperationKey = validOperationKey;
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    self.sd_imageURL = url;
    // 如果没有选择延迟加载占位图
    if (!(options & SDWebImageDelayPlaceholder)) {
    	// 在主线程主队列中设置占位图
        dispatch_main_async_safe(^{
        	// 作为图片下载完成之前的替代图片
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
    
    // 如果传入了图片链接
    if (url) {
        // reset the progress
        // 重置进度
        NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
        // 获取图像加载进度
        if (imageProgress) {
        // 初始化图片加载进度
            imageProgress.totalUnitCount = 0;
            imageProgress.completedUnitCount = 0;
        }
        
#if SD_UIKIT || SD_MAC
        // check and start image indicator
        // 是否显示进度条(小菊花)
        [self sd_startImageIndicator];
        id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
		// 生成图片管理者对象,如果context中有就用context中的
        SDWebImageManager *manager;
        SDWebImageManager *manager = context[SDWebImageContextCustomManager];
		// 否则就直接生成SDWebImageManager单例对象
        if (!manager) {
            manager = [SDWebImageManager sharedManager];
        } else {
            // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
            // 删除此管理器以避免保留周期(管理器 -> 加载程序 -> 操作 -> 上下文 -> 管理器)
            SDWebImageMutableContext *mutableContext = [context mutableCopy];
            mutableContext[SDWebImageContextCustomManager] = nil;
            context = [mutableContext copy];
        }
        
        // 生成一个代码块用来在下载图片的方法中监听进度并进行回调
        SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            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); // 0.0 - 1.0
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imageIndicator updateIndicatorProgress:progress];
                });
            }
#endif
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        @weakify(self);

		// 生成图片操作对象,并开始下载图片
        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) {
            @strongify(self);
            if (!self) { return; }
            // if the progress not been updated, mark it to complete state
            // 如果已经完成并且没有错误,并且进度没有更新,就将进度状态设为未知
            if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
                imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            
#if SD_UIKIT || SD_MAC
            // check and stop image indicator
            // 检查和停止图像指示器(如果有加载小菊花的话就移除掉)
            if (finished) {
                [self sd_stopImageIndicator];
            }
#endif
            // 是否应该回调完成block: 如果已经完成或者设置了在设置图片前处理
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            // 是否应该不设置图片: 如果有图片但设置了在设置图片前处理,或者没有图片并且没有设置延迟加载占位图
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            // 生成完成回调代码块
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
            	// 如果没有生成强引用的self就终止执行 
                if (!self) { return; }
                // 如果需要设置图片就直接刷新视图
                if (!shouldNotSetImage) {
                    [self sd_setNeedsLayout];
                }
                // 如果传入了回调block并且应该进行回调,就直接回调
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, data, error, cacheType, finished, url);
                }
            };
            
            // 如果不需要设置图片就在主线程主队列中调用上面生成的完成回调代码块,并且不再向下执行
            if (shouldNotSetImage) {
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;
            }
            
            // 生成变量保存数据
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            if (image) {
                // 如果图片下载成功就用变量保存图片
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // 如果图片下载失败并且设置了延迟加载占位图,就保存占位图
                targetImage = placeholder;
                targetData = nil;
            }
            
#if SD_UIKIT || SD_MAC
            SDWebImageTransition *transition = nil;
            BOOL shouldUseTransition = NO;
            if (options & SDWebImageForceTransition) {
                // Always
                shouldUseTransition = YES;
            } else if (cacheType == SDImageCacheTypeNone) {
                // From network
                shouldUseTransition = YES;
            } else {
                // From disk (and, user don't use sync query)
                if (cacheType == SDImageCacheTypeMemory) {
                    shouldUseTransition = NO;
                } else if (cacheType == SDImageCacheTypeDisk) {
                	// SDWebImageQueryMemoryDataSync:
                    // 默认情况下,当您仅指定“SDWebImageQueryMemoryData”时,我们会异步查询内存映像数据。
                    // 将此掩码也组合在一起,以同步查询内存图像数据
                    // 不建议同步查询数据,除非您要确保在同一 runloop 中加载映像以避免在单元重用期间闪烁。
                    // SDWebImageQueryDiskDataSync:
                    // 默认情况下,当内存缓存未命中时,我们会异步查询磁盘缓存。此掩码可以强制同步查询磁盘缓存(当内存缓存未命中时)。
                    // 这两个选项打开则NO。
                    if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) {
                        shouldUseTransition = NO;
                    } else {
                        shouldUseTransition = YES;
                    }
                } else {
                    // Not valid cache type, fallback
                    shouldUseTransition = NO;
                }
            }
            // 检查一下是否应该转换图片:如果下载完成,并且设置了图片强制转换或者图片缓存类型是不缓存直接从网络加载,就进行强制转换
            if (finished && shouldUseTransition) {
                transition = self.sd_imageTransition;
            }
#endif
            dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
				// 在主线程主队列中设置图片
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
				// 如果用户没有设置调度组,就直接在主线程主队列中设置图片和调用完成回调代码块
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
                callCompletedBlockClojure();
            });
        }];
        // 根据密钥保存下载图片的操作
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
#if SD_UIKIT || SD_MAC
		// 如果没传入图片链接,就在主线程主队列移除加载小菊花
        [self sd_stopImageIndicator];
#endif
        dispatch_main_async_safe(^{
        	// 如果传入了完成回调block就回调错误信息
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
            }
        });
    }
}

整个过程看起来繁琐复杂。下面,我们讨论一下这个方法中的核心步骤。

取消当前正在进行的异步下载

取消当前正在进行的异步下载,确保每个UIImageView对象中永远只存在一个operation,当前只允许一个图片网络请求,该operation负责从缓存中获取image或者是重新下载image。

// 生成一个有效的操作密钥
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    // 如果传入了参数就用传入的,否则就用当前类的类名
    if (!validOperationKey) {
        validOperationKey = NSStringFromClass([self class]);
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        context = [mutableContext copy];
    }
// 取消先前下载的任务
[self sd_cancelImageLoadOperationWithKey:validOperationKey];

... // 下载图片操作

// 将生成的加载操作赋值给UIView的自定义属性
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

上述方法定义在UIView+WebCacheOperation类中:

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        // 从队列中取消正在进行的下载程序

		// 获取添加在UIView的自定义属性
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary]; 
        id<SDWebImageOperation> operation;

        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            // 实现了SDWebImageOperation的协议
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                [operation cancel];
            }
            @synchronized (self) {
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}
- (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];
            }
        }
    }
}

实际上,所有的操作都是由一个实际上,所有的操作都是由一个operationDictionary字典维护的,执行新的操作之前,cancel所有的operation

占位图策略

作为图片下载完成之前的替代图片。dispatch_main_async_safe是一个宏,保证在主线程安全执行。

if (!(options & SDWebImageDelayPlaceholder)) {
	// 在主线程主队列中设置占位图
    dispatch_main_async_safe(^{
        // 设置占位图
        [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
    });
}

判断url是否合法

如果url合法,则进行图片下载操作,否则直接block回调失败。

if (url) {
    // 下载图片操作
} else {
    dispatch_main_async_safe(^{
#if SD_UIKIT
        [self sd_removeActivityIndicator];
#endif
        if (completedBlock) {
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
            completedBlock(nil, error, SDImageCacheTypeNone, url);
        }
    });
}

下载图片操作

下载图片的操作是由SDWebImageManager完成的,它是一个单例。

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock;

下载完成之后刷新UIImageView的图片。

// 根据枚举类型,判断是否需要设置图片
shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                          (!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
    if (!sself) { return; }
    if (!shouldNotSetImage) {
    	// 设置图片
        [sself sd_setNeedsLayout];  
    }
    if (completedBlock && shouldCallCompletedBlock) {
        completedBlock(image, error, cacheType, url);
    }
};

// 不要自动设置图片,则调用block传入image对象
if (shouldNotSetImage) {    
    dispatch_main_async_safe(callCompletedBlockClojure);
    return;
}

// 设置图片操作
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
    [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
    [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
    callCompletedBlockClojure();
});

最后,把返回的 operation 添加到operationDictionary中,方便后续的cancel。

// 将生成的加载操作赋值给UIView的自定义属性
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

SDWebImageManager

SDWebImageManager的官方介绍:

  • The SDWebImageManager is the class behind the UIImageView+WebCache category and likes.
  • It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache).
  • You can use this class directly to benefit from web image downloading with caching in another context than a UIView.

翻译:

  • SDWebImageManager是UIImageView+WebCache类别背后的类。
  • 它将异步下载程序(SDWebImageDownloader)与图像缓存存储(SDImageCache)绑定。
  • 您可以直接使用这个类,在另一个地方中使用缓存下载web图像,而不是一个UIView。

总结一下:

SDWebImageManager是隐藏在UIImageView+WebCache之后,用于处理异步下载和图片缓存的类。同时你也可以直接使用SDWebImageManager的如下方法来直接下载图片。

- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                            completed:(nullable SDInternalCompletionBlock)completedBlock;

下面我们看看SDWebImageManager.h里的内容。

  • 首先定义了一些枚举类型的SDWebImageOptions。
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions ) {
    SDWebImageRetryFailed = 1 < < 0,
    SDWebImageLowPriority = 1 < < 1,
    SDWebImageCacheMemoryOnly = 1 < < 2,
    SDWebImageProgressiveDownload = 1 < < 3,
    SDWebImageRefreshCached = 1 < < 4,
    SDWebImageContinueInBackground = 1 < < 5,
    SDWebImageHandleCookies = 1 < < 6,
    SDWebImageAllowInvalidSSLCertificates = 1 < < 7,
    SDWebImageHighPriority = 1 < < 8,
    SDWebImageDelayPlaceholder = 1 < < 9,
    SDWebImageTransformAnimatedImage = 1 < < 10,
    SDWebImageAvoidAutoSetImage = 1 < < 11,
    SDWebImageScaleDownLargeImages = 1 < < 12,
};

具体含义参考SDWebImageOptions常量说明,大致表达了图片加载过程中对于图片的一些状态,如SDWebImageRetryFailed表示即使某个url下载失败了,SDWebImage还是会尝试再次下载它;SDWebImageLowPriority表示禁止图片在交互发生的时候下载(如滑动tableview)等等。

  • 接下来声明了四个block。
// 操作完成的回调,被上层的扩展调用。
typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);

// 被SDWebImageManager调用。
// 如果使用了SDWebImageProgressiveDownload标记,这个block可能会被重复调用,直到图片完全下载结束,
// finished=true,再最后调用一次这个block。
typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);

// SDWebImageManager每次把URL转换为cache key的时候调用,可以删除一些image URL中的动态部分。
typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);

typedef NSData * _Nullable(^SDWebImageCacheSerializerBlock)(UIImage * _Nonnull image, NSData * _Nullable data, NSURL * _Nullable imageURL);
  • 然后定义了SDWebImageManagerDelegate协议。
@protocol SDWebImageManagerDelegate 

@optional
// 控制在cache中没有找到image时 是否应该去下载。
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

// 在下载之后,缓存之前转换图片。在全局队列中操作,不阻塞主线程
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

@end

SDWebImageManager是单例使用的,分别维护了一个SDImageCache实例和一个SDWebImageDownloader实例。其中包含一些方法:

// 初始化SDWebImageManager单例,在init方法中已经初始化了cache单例和downloader单例。
- (instancetype)initWithCache:(SDImageCache *)cache downloader:(SDWebImageDownloader *)downloader;
// 下载图片
- (id )downloadImageWithURL:(NSURL *)url
                    options:(SDWebImageOptions)options
                   progress:(SDWebImageDownloaderProgressBlock)progressBlock
                  completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
// 缓存给定URL的图片
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
// 取消当前所有的操作
- (void)cancelAll;
// 监测当前是否有进行中的操作
- (BOOL)isRunning;
// 监测图片是否在缓存中, 先在memory cache里面找  再到disk cache里面找
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
// 监测图片是否缓存在disk里
- (BOOL)diskImageExistsForURL:(NSURL *)url;
// 监测图片是否在缓存中,监测结束后调用completionBlock
- (void)cachedImageExistsForURL:(NSURL *)url
                     completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
// 监测图片是否缓存在disk里,监测结束后调用completionBlock
- (void)diskImageExistsForURL:(NSURL *)url
                   completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//返回给定URL的cache key
- (NSString *)cacheKeyForURL:(NSURL *)url;

下面我们看看下载图片的主要过程,也就是下面这个方法:

- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                            completed:(nullable SDInternalCompletionBlock)completedBlock;

判断url是否合法

if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
// 防止应用程序在参数类型错误(如发送NSNull而不是NSURL)时崩溃
if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}

这里为了防止很多用户直接传递NSString,先将NSString类型url转换成NSURL类型,然后防止一下程序因参数类型错误导致的crash。

判断已加载失败的url

BOOL isFailedUrl = NO;
if (url) {  
	// 判断url是否是加载失败过的url
    LOCK(self.failedURLsLock);
    isFailedUrl = [self.failedURLs containsObject:url];
    UNLOCK(self.failedURLsLock);
}
// 如果url为空或者url下载失败并且设置了不再重试
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
    [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
    return operation;
}

这里使用了集合failedURLs,用于保存之前加载失败的urls。如果url为空或者url之前失败过且有SDWebImageRetryFailed枚举类型,直接调用completedBlock返回错误。

保存操作

LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);

使用可变数组runningOperations,用于保存所有的operation,并且监测是否有operation在执行,即判断running状态。

查找缓存

SDWebImageManager会首先在内存缓存和磁盘缓存的 cache 中查找是否下载过相同的照片,即调用imageCache的下面方法:

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock;

如果操作取消,则直接返回:

__strong __typeof(weakOperation) strongOperation = weakOperation;
// operation取消,那么将下载任务从下载队列中直接移除
if (!strongOperation || strongOperation.isCancelled) {
    [self safelyRemoveOperationFromRunning:strongOperation];
    return;
}

如果没有在缓存中找到图片,或者不管是否找到图片,只要operationSDWebImageRefreshCached标记,那么若SDWebImageManagerDelegateshouldDownloadImageForURL方法返回 true,即允许下载时,都使用 imageDownloader的下载方法:
O

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

如果下载有错误,直接调用completedBlock返回错误,并且视情况将url添加到failedURLs里面:

dispatch_main_sync_safe(^{
	// 下载有误,直接调用completedBlock返回错误
    if (strongOperation && !strongOperation.isCancelled) {
        completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
    }
});
//视error情况将url添加到failedURLs
if (error.code != NSURLErrorNotConnectedToInternet
 && error.code != NSURLErrorCancelled
 && error.code != NSURLErrorTimedOut
 && error.code != NSURLErrorInternationalRoamingOff
 && error.code != NSURLErrorDataNotAllowed
 && error.code != NSURLErrorCannotFindHost
 && error.code != NSURLErrorCannotConnectToHost) {
      @synchronized (self.failedURLs) {
          [self.failedURLs addObject:url];
      }
}

如果下载成功,若支持失败重试,将urlfailURLs里删除:

if ((options & SDWebImageRetryFailed)) {
    @synchronized (self.failedURLs) {
         [self.failedURLs removeObject:url];
    }
}

如果delegate中实现了imageManager:transformDownloadedImage:withURL:方法,图片在缓存之前,需要做转换(在全局队列中调用,不阻塞主线程)。转化成功后下载全部结束,图片存入缓存,调用completedBlock回调。其中第一个参数是转换后的image

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

    if (transformedImage && finished) {
        BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
        //缓存图片
        [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
    }
    dispatch_main_sync_safe(^{
        if (strongOperation && !strongOperation.isCancelled) {
            completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
        }
    });
});

存入缓存都是调用imageCache的下面方法:

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;

如果没有在缓存找到图片,且不允许下载,直接调用completedBlock,第一个参数为nil。

dispatch_main_sync_safe(^{
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    if (strongOperation && !weakOperation.isCancelled) {
        completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
    }
});

最后将这个operation从runningOperations里删除。

@synchronized (self.runningOperations) {
    [self.runningOperations removeObject:operation];
 }

可以看到,SDWebImageManger负责处理和协调SDWebImageDownloaderSDWebImageCache,并与 UIKit层进行交互(传入图片)。

SDWebImageDownloader

SDWebImage的下载模块主要包括两个类:SDWebImageDownloaderSDWebImageDownloaderOperation

SDWebImageDownloader负责对所有下载任务的管理,为下载图片提供专用和优化的异步下载器。

SDWebImageDownloaderOperation负责具体的一个下载任务的执行,且为了拓展下载功能,还支持实现SDWebImageDownloaderOperationInterface协议来自定义具体下载。

这里我们先看看SDWebImageDownloader

SDWebImageDownloader基本属性

  • currentDownloadCount:显示当前仍然需要下载的大小
  • downloadTimeout:下载操作的超时时间(单位为秒)。默认值:15.0
  • executionOrder:修改下载操作的执行顺序。默认值为SDWebImageDownloaderFIFOExecutionOrder
  • headerFilter:设置过滤器用来挑选下载图片的HTTP请求的头。这个块在每个图片下载请求时被调用,返回在相应的HTTP请求中用于HTTP头的NSDictionary
  • maxConcurrentDownloads:最大的并发下载数
  • username:设置用户名
  • password:设置密码
  • sessionConfigurationNSURLSession内部使用的配置
  • shouldDecompressImages:默认为YES,解压已经下载和缓存的图片可以提高性能,但是会消耗很多的内容。如果你遇到由于大量的消耗内存导致崩溃,建议设置为NO
  • urlCredential:为请求操作设置默认的URL证书
//显示当前仍然需要下载的大小
@property (readonly, nonatomic) NSUInteger currentDownloadCount;

//下载操作的超时时间(单位为秒)。默认值:15.0
@property (assign, nonatomic) NSTimeInterval downloadTimeout;

//修改下载操作的执行顺序。默认值为SDWebImageDownloaderFIFOExecutionOrder
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;

//设置过滤器用来挑选下载图片的HTTP请求的头。这个块在每个图片下载请求时被调用,返回在相应的HTTP请求中用于HTTP头的NSDictionary
@property (nonatomic, copy, nullable) SDWebImageDownloaderHeadersFilterBlock headersFilter;

//最大的并发下载数
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;

//设置用户名
@property (strong, nonatomic, nullable) NSString *username;

//设置密码
@property (strong, nonatomic, nullable) NSString *password;

//NSURLSession内部使用的配置
@property (readonly, nonatomic, nonnull) NSURLSessionConfiguration *sessionConfiguration;

//默认为YES,解压已经下载和缓存的图片可以提高性能,但是会消耗很多的内容。如果你遇到由于大量的消耗内存导致崩溃,建议设置为NO
@property (assign, nonatomic) BOOL shouldDecompressImages;

//为请求操作设置默认的URL证书
@property (strong, nonatomic, nullable) NSURLCredential *urlCredential;

SDWebImageDownloader枚举类型

SDWebImageDownloader枚举类型提供了很多下载选项,可以根据情况进行配置。如设置下载优先级、进度、后台下载,图片缩放等,同时支持先进先出,先进后出的下载方式。

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    //下载低优先级
    SDWebImageDownloaderLowPriority = 1 << 0,
    //下载高优先级
    SDWebImageDownloaderHighPriority = 1 << 7,
    // 带有进度
    SDWebImageDownloaderProgressiveDownload = 1 << 1,
    //默认不使用URLCache
    SDWebImageDownloaderUseNSURLCache = 1 << 2,
    //如果图片是在NSURLCAche中读取时,调用completion block时,返回nil,配合SDWebImageDownloaderUseNSURLCache使用
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    //支持后台下载
    SDWebImageDownloaderContinueInBackground = 1 << 4,
    //支持NSHTTPCookieStore的cookie信息,进而设置NSMutableURLRequest.HTTPShouldHandleCookies=YES
    SDWebImageDownloaderHandleCookies = 1 << 5,
    //允许不信任SSL证书
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
    //缩放大图片
    SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
     //先进先出
    SDWebImageDownloaderFIFOExecutionOrder,
    //先进后出
    SDWebImageDownloaderLIFOExecutionOrder
};

核心方法

先看方法:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    //如果URL为空,直接执行完成回调block并传入nil参数,结束本次请求
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
  //NSMutableDicitonary不是线程安全的,利用GCD的barrier 保证线程安全,确保字典不会同时存取
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same URL
        //为URL创建一个唯一对应的callbacks,并赋值给self.URLCallBacks
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;

        if (first) {
            createCallback();
        }
    });
}

再看结构图:
在这里插入图片描述
URLCallBacks字典存储的每个请求的callbacksForURL,走完上面这个函数,我们就能确保每个请求都能和它的progressBlockcompletedBlock回调一一对应。

再看下载的核心方法- downloadImageWithURL: options: progress: completed:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
         __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
      //NSURLRequest生成
        NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];
        //设置cookie信息
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        //设置header信息
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
        }
        else {
            request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
        }
        //组建SDWebImageDownloaderOperation
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        //设置加密信息
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        //设置优先级
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        //设置队列
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

此方法用于对外的暴露,实际上进行了SDWebImageDownloaderOperation的生成的相关设置工作。NSURLRequest生成并设置的header信息、cookie信息设置。NSURLRequestsession、配置options生成DownloaderOperationDownloaderOperation设置加密、优先级、队列。最后通过addProgressCallback方法将operation缓存,并添加到操作队列中addOperation

SDWebImageDownloaderOperation

前面说过,SDWebImageDownloaderOperation负责下载任务的具体实现,我们直接看核心方法。

核心方法

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        __strong __typeof (wself) sself = wself;
        
        // 设置超时时间
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        // 创建request,针对不同缓存策略不同处理
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        
        // 设置请求头部
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        
        // 创建操作对象
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        // 给操作对象设置urlCredential
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        // 设置操作级别
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }

        // 把操作添加到队列
        [sself.downloadQueue addOperation:operation];
        
        // 根据executionOrder设置,设置依赖
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            // 模拟后进先出执行顺序
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

取消某个操作

- (void)cancel:(nullable SDWebImageDownloadToken *)token {
    dispatch_barrier_async(self.barrierQueue, ^{
        SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
        BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
        if (canceled) {
            [self.URLOperations removeObjectForKey:token.url];
        }
    });
}

全部暂停或取消

- (void)setSuspended:(BOOL)suspended {
    (self.downloadQueue).suspended = suspended;
}

- (void)cancelAllDownloads {
    [self.downloadQueue cancelAllOperations];
}

相关问题

SDWebImage使用流程

  1. 入口setImageWithURL:placeholderImage:options:先显示预览图placeholderImage,然后 SDWebImageManager 根据URL开始处理图片
  2. SDWebImageManager调用downloadWithURL:delegate:options:userInfo:方法,进入SDImageCachequeryDiskCacheForKey:delegate:userInfo:方法,从缓存查找图片是否已经下载
  3. 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate回调imageCache:didFindImage:forKey:userInfo:SDWebImageManager
  4. SDWebImageManagerDelegate回调webImageManager:didFinishWithImage:方法,以在UIImageView+WebCacheUIButton+WebCache等前端展示图片
  5. 如果内存缓存中没有,生成NSInvocationOperation添加到队列开始从硬盘查找图片是否已经缓存
  6. 根据URLKey在硬盘缓存目录下尝试读取图片文件。这一步在NSOperation进行,所以要回主线程进行结果回调:使用notifyDelegate:方法
  7. 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate回调imageCache:didFindImage:forKey:userInfo:,进而回调展示图片
  8. 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调imageCache:didNotFindImageForKey:userInfo:
  9. 共享或重新生成一个下载器SDWebImageDownloader开始下载图片
  10. 图片下载由NSURLConnection来执行,实现相关delegate来判断图片下载中、下载完成和下载失败
  11. connection:didReceiveData:中利用ImageIO提供了加载效果,展示图片下载进度
  12. connectionDidFinishLoading:数据下载完成后交给SDWebImageDecoder做图片解码处理
  13. 图片解码处理在一个NSOperationQueue完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多
  14. 在主线程notifyDelegateOnMainThreadWithInfo:宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo:回调给SDWebImageDownloader
  15. imageDownloader:didFinishWithImage:回调给SDWebImageManager告知图片下载完成
  16. 通知所有的downloadDelegates下载完成,回调给需要的地方展示图片
  17. 将图片保存到SDImageCache中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独NSInvocationOperation完成,避免拖慢主线程
  18. SDImageCache在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片
  19. SDWebImagePrefetcher可以预先下载图片,方便后续使用

SDImageCache是怎么做数据管理的?

SDImageCache分两个部分,⼀个是内存层面的,⼀个是硬盘层面的。内存层⾯相当于一个缓存器,以Key-Value的形式存储图片。当内存不够的时候会清除所有缓存图片。用搜索文件系统的方式做管理,文件替换方式是以时间为单位,剔除时间大于⼀周的图片文件。

SDWebImageManagerSDImageCache要资源时,先搜索内存层面的数据:如果有,直接返回;如果没有,去访问磁盘,将图片从磁盘读取出来,然后做Decoder,将图片对象放到内存层面做备份,再返回调用层。

内部做Decoder的原因 (空间换时间)

由于UIImageimageWithData:方法只有每次画图的时候,才将Data解压成ARGB的图像,所以在每次画图的时候,会有一个解压操作,这样效率很低,但是只有瞬时的内存需求。为了提高效率,通过SDWebImageDecoder将包装在Data下的资源解压,然后画在另外一张图片上,这样这张新图片就不再需要重复解压了。

SDWebImage 的内存警告是如何处理

利用通知中心观察:

  • UIApplicationDidReceiveMemoryWarningNotification 监听内存警告通知,执行 clearMemory 方法,清理内存缓存
  • UIApplicationWillTerminateNotification 监听应用程序将要终止通知,执行 cleanDisk 方法,清理磁盘缓存
  • UIApplicationDidEnterBackgroundNotification 监听应用程序进入后台通知,执行 backgroundCleanDisk 方法,后台清理磁盘

通过以上通知监听,能够保证缓存文件的大小始终在控制范围之内。clearDisk 清空磁盘缓存表示:将所有缓存目录中的文件全部删除。实际情况中会将缓存目录直接删除,再次创建一个同名空目录。

其他小问题

  • Q:图片文件缓存的时间有多长?
    A:1周。static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7;
  • Q:SDWebImage 的内存缓存是用什么实现的?
    A:NSCacheSDImageCache类中有一个NSCache类型的memCache属性,用这个属性来进行内存缓存。
    存缓存:[self.memCache setObject:image forKey:key cost:cost];
    取缓存:return [imageFromMemoryCacheForKey:key];
  • Q:SDWebImage 的最大并发数是多少?
    A:maxConcurrentDownloads = 6为程序固定死了,其实可以通过属性进行调整。
  • Q:SDWebImage是如何区分不同格式的图像?
    A:根据图像数据第一个字节来判断的。
  • Q:SDWebImage 缓存图片的名称是怎么确定的?
    A:使用文件名保存,重名的几率很高,因此使用 MD5 的散列函数。对完整的 URL 进行 md5,结果是一个 32 个字符长度的字符串。
  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值