一,简介
一个异步图片下载及缓存的库
特性:
- 一个扩展UIImageView分类的库,支持加载网络图片并缓存图片
- 异步图片下载器
- 异步图片缓存和自动图片有效期限管理
- 支持GIF动态图片
- 支持WebP
- 背景图片减压
- 保证同一个URL不会再次下载
- 保证无效的URL不会重新加载
- 保证主线程不会死锁
- 性能优越
- 使用GCD和ARC
- 支持ARM64位处理器
二,原理
只要有图片的url,就能下载图片,使用SDWebImage的好处就是缓存机制,每次取图片先判断是否在内存中,再到缓存中查找 ,找到了直接加载,在缓存找不到才重新下载。url也会被记录,是否是失效的url,是则不会再尝试。下载的图片会缓存,用于下次可以直接加载。图片的下载,解码,转码都异步进行,不会阻塞主线程。
概念:
图片的缓存
指将已经下载的图片保存在内存或磁盘中,以便在后续的加载请求中快速获取,避免重复下载和提高加载速度。可以减轻网络负担并改善用户体验。在 iOS 开发中,常用的图片缓存方案是将图片存储在内存缓存和磁盘缓存中。
图片下载:
图片下载是指从网络获取图片数据的过程。下载图片的步骤包括创建请求、发送请求、接收响应和处理响应数据。
图片解码:
图片解码是将下载的图片数据解析为可供应用程序使用的图像格式(像素数据)的过程。常见的图像格式包括 JPEG、PNG、GIF、WebP 等。
图片转码
将图像从一种格式转换为另一种格式的过程。在某些情况下,您可能需要将下载或解码后的图像进行转码,以便与特定的需求或平台兼容。
三,SDWebImage组织架构
类的作用
- SDImageCache
负责图片的缓存,设置缓存的类型,方式,路径等
- SDWebImageCompat
兼容类,定义了很多宏和一个转换图片的方法
- SDWebImageDecoder
解码器,让图片色彩转换(涉及到color space)
- SDWebImageDownloader
负责图片的下载队列,下载器,设置下载相关,要用到SDWebImageDownloaderOperation,
- SDWebImageDownloaderOperation
负责正真的图片下载请求,下载器的操作,
- SDWebImageManager
管理图片下载,取消操作,判断url是否已缓存等,是总的管理类,维护了SDWebImageDownloader实例和一个SDImageCache实例,是下载和缓存的桥梁。
- SDWebImageOperation
图片操作,后面很多类都要用到
- SDWebImagePrefetcher
预抓取器,预先下载urls中的图片
- UIButton+WebCache
按钮图片的缓存
- UIImage+GIF
缓存gif
- NSData+ImageContentType
判断图片的类型,png/jpeg/gif/webp
- UIImage+MultiFormat
缓存多种格式的图片,要用到NSData+ImageContentType的判断图片类型方法和UIImage+GIF的判断是否为gif图片方法,以及ImageIO里面的方法
- UIImageView+HighlightedWebCache
缓存高亮图片
- UIImageView+WebCache
主要用到这个,加载及缓存UIImageView的图片,和其他的拓展都是与用户直接打交道的
- UIView+WebCacheOperation
缓存的操作,有缓存,取消操作,移除缓存
其中,最重要的三个类就是SDWebImageDownloader、SDImageCache、SDWebImageManager
框架结构:
- UIImageView+WebCache和UIButton+WebCache直接为表层的 UIKit框架提供接口
- SDWebImageManger负责处理和协调SDWebImageDownloader和SDWebImageCache, 并与 UIKit层进行交互。
- SDWebImageDownloaderOperation真正执行下载请求;最底层的两个类为高层抽象提供支持。
四,流程
在SDWebImage的使用例子中,给UIImageView设置图片的代码是
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:[_objects objectAtIndex:indexPath.row]]
placeholderImage:[UIImage imageNamed:@"placeholder"] options:indexPath.row == 0 ? SDWebImageRefreshCached : 0];
SDWebImage只用一行代码,就可以实现网络图片的加载和缓存,这行代码的背后,会执行下列操作
UIImageView+WebCache
categories会从管理类SDWebImageManager
找图片,并刷新UIImageView。SDWebImagemanager
向从缓存类SDImageCache
找URL对应的图片缓存,如果没有找到,起用SDWebImageDownloader
下载图片。- 缓存类
SDImageCache
会先在内存NSCache中找图片,如果内存中没有找到,就在磁盘上找,在磁盘找到了,把图片放入内存。 SDWebImageDownLoader
会创建一个SDWebImageDownloaderOperation
操作队列下载图片,下载后缓存在内存和磁盘上。SDWebImageDownloaderOperation
操作队列使用NSURLconnection
在后台发起请求,下载图片,反馈进度和加载图片。
五,主要类的源码
SDImageCache
SDImageCache类管理着内存缓存,提供了一个方便的单例shareImageCache。如果不想使用default缓存空间,而想创建你自己的SDImageCache对象来指定其他命名空间初始化来管理缓存
1. (SDImageCache *)sharedImageCache {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
经典的iOS单例,使用dispatch_once防止多线程环境下生成多个实例。
- (id)init {
return [self initWithNamespace:@"default"];
}
- (id)initWithNamespace:(NSString *)ns {
NSString *path = [self makeDiskCachePath:ns];
return [self initWithNamespace:ns diskCacheDirectory:path];
}
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
if ((self = [super init])) {
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
// 初始化PNG标记数据
kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
// 创建ioQueue串行队列负责对硬盘的读写
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
// 初始化默认的最大缓存时间
_maxCacheAge = kDefaultCacheMaxCacheAge;
// 初始化内存缓存,详见接下来解析的内存缓存类
_memCache = [[AutoPurgeCache alloc] init];
_memCache.name = fullNamespace;
// 初始化磁盘缓存
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
// 设置默认解压缩图片
_shouldDecompressImages = YES;
// 设置默认开启内存缓存
_shouldCacheImagesInMemory = YES;
// 设置默认不使用iCloud
_shouldDisableiCloud = YES;
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
#if TARGET_OS_IPHONE
// app事件注册,内存警告事件,程序被终止事件,已经进入后台模式事件,详见后文的解析:app事件注册。
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}
return self;
}
内存缓存类
@interface AutoPurgeCache : NSCache
@end
@implementation AutoPurgeCache
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
@end
这就是上面初始化的内存缓存类AutoPurgeCache,使用NSCache派生得到。整个类只有一个逻辑,就是添加观察者,在内存警告时,调用NSCache的@selector(removeAllObjects),清空内存缓存。
app事件注册
app事件注册使用经典的观察者模式,当观察到内存警告,程序被终止,程序进入后台这些事件时,程序将自动调用相应的方法处理。
内存警告
当收到UIApplicationDidReceiveMemoryWarningNotification
时,调用的@selector(clearMemory),在方法中调用内存缓存类AutoPurgeCache的方法removeAllObject。
程序被终止
当收到UIApplicationWillTerminateNotification
时,SDImageCache将会使用ioQueue
异步地清理磁盘缓存。
具体清理逻辑:
- 先清除已超过最大缓存时间的缓存文件(最大缓存时间默认为一星期)
- 在第一轮清除的过程中保存文件属性,特别是缓存文件大小
- 在第一轮清除后,如果设置了最大缓存并且保留下来的磁盘缓存文件仍然超过了配置的最大缓存,那么进行第二轮以大小为基础的清除。
- 首先删除最老的文件,直到达到期望的总的缓存大小,即最大缓存的一半。
程序进入后台
当收到UIApplicationDidEnterBackgroundNotification
时,在手机系统后台进行如上面描述的异步磁盘缓存清理。这里利用Objective-C的动态语言特性,得到UIApplication的单例sharedApplication,使用sharedApplication开启后台任务cleanDiskWithCompletionBlock:。
缓存中取图片
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
// 如果 doneBlock 为空,则直接返回 nil
return nil;
}
if (!key) {
// 如果 key 为空,则执行 doneBlock,并返回 nil
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// 首先查询内存缓存
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
// 如果内存缓存中存在该图片,则执行 doneBlock,并返回 nil
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
// 从磁盘缓存中获取图片
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
// 如果磁盘缓存中存在该图片且内存缓存开启,则将图片保存到内存缓存中
//将图片保存到BSCache,并把图像像素大小作为该对象的cost值
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
// 在主队列中执行 doneBlock,并返回磁盘缓存中的图片
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
// 从内存缓存中获取指定 key 的图片
return [self.memCache objectForKey:key];
}
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
// 计算图片在内存缓存中的成本(cost),即图片像素大小
return image.size.height * image.size.width * image.scale * image.scale;
}
传入的Block定义是:
typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);
先从内存中取图片,内存中没有的时候再从磁盘中取,通过Block返回取到的图片和获取图片的方式。
SDImageCacheType的定义如下:
typedef NS_ENUM(NSInteger, SDImageCacheType)
{
/**
* The image wasn't available the SDWebImage caches, but was downloaded from the web.
*/
SDImageCacheTypeNone,
/**
* The image was obtained from the disk cache.
*/
SDImageCacheTypeDisk,
/**
* The image was obtained from the memory cache.
*/
SDImageCacheTypeMemory
};
当然,也可能磁盘中也没有缓存,此时doneBlock
中的diskImage
的值是nil,处理方式doneBlock
将在SDWebImageManager
讲到。
缓存中取图片
static unsigned char kPNGSignatureBytes[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
static NSData *kPNGSignatureData = nil;
BOOL ImageDataHasPNGPreffix(NSData *data);
BOOL ImageDataHasPNGPreffix(NSData *data) {
NSUInteger pngSignatureLength = [kPNGSignatureData length];
if ([data length] >= pngSignatureLength) {
if ([[data subdataWithRange:NSMakeRange(0, pngSignatureLength)] isEqualToData:kPNGSignatureData]) {
return YES;
}
}
return NO;
}
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
// if memory cache is enabled
//如果启用了内存缓存
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 我们需要判断图片是PNG还是JPEG格式。PNG图片很容易检测,因为它们拥有一个独特的签名<http://www.w3.org/TR/PNG-Structure.html>。PNG文件的前八字节经常包含如下(十进制)的数值:137 80 78 71 13 10 26 10
// 如果imageData为nil(也就是说,如果试图直接保存一个UIImage或者图片是由下载转换得来)并且图片有alpha通道,我们将认为它是PNG文件以避免丢失透明度信息。
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// 但是如果我们有image data,我们将查询数据前缀
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
if (data) {
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// 获得对应图像key的完整缓存路径
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// 转换成NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
// 关闭iCloud备份
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
});
}
}
- (void)storeImage:(UIImage *)image forKey:(NSString *)key {
[self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:YES];
}
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk {
[self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:toDisk];
}
存储一个图片到缓存中,可以使用方法storeImage:forKey:method:
,默认,图片会存储到内存缓存中,也会异步地保存到磁盘缓存中,如果只想使用内存缓存,可以使用另外一个方法storeImage:forKey:toDisk
,第三个参数传入false
值就好了。
SDWebImageDownloader
SDWebImageDownloader是下载管理类,是一个单例类,图片的下载在一个NSOperationQueue
队列中完成。
@property (strong, nonatomic) NSOperationQueue *downloadQueue;
下载图片的消息是
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
该方法会创建一个SDWebImageDownloaderOperation
操作队列来执行下载操作。传入的两个Block用于网络下载的回调,progressBlock
为下载进度回调,completeBlock
为下载完成回调,回调信息存储在URLCallblacks
中,为保证只有一个线程操作URLCallbacks,SDWebImageloader
把这些操作放入一个barrierQueue队列中。
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
// 创建一个并发队列 _barrierQueue,用于执行同步栅栏操作。
// 这个方法用于添加下载进度回调和完成回调。
// 参数:
// - progressBlock:下载进度的回调块
// - completedBlock:下载完成的回调块
// - url:要添加回调的 URL
// - createCallback:回调创建时的块
// 在这个方法中,我们使用了同步栅栏操作来确保在添加回调时线程安全。
// - 首先,我们检查 self.URLCallbacks 字典中是否存在以 url 为键的数组。如果不存在,则创建一个空的数组,并将 first 标志设置为 YES。
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback
{
···
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 的单个下载或同时下载请求
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;
// 接下来,我们将进度回调和完成回调添加到以 url 为键的字典中。
// - 首先,我们创建一个可变字典 callbacks。
// - 如果 progressBlock 存在,则将其拷贝到 callbacks 中。
// - 如果 completedBlock 存在,则将其拷贝到 callbacks 中。
// - 然后,我们获取 self.URLCallbacks[url] 的可变数组,并将 callbacks 添加到该数组中。
// - 最后,我们将更新后的数组重新赋值给 self.URLCallbacks[url]。
if (first){
createCallback();
}
// 最后,在同步栅栏操作的闭包中,我们检查 first 标志。如果该标志为 YES,则调用 createCallback 块。
});
}
SDWebImageDownloader还提供了两种下载任务调度方法(先进先出和后进先出)
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder)
{
/**
* Default value. All download operations will execute in queue style (first-in-first-out).
*/
SDWebImageDownloaderFIFOExecutionOrder,
/**
* All download operations will execute in stack style (last-in-first-out).
*/
SDWebImageDownloaderLIFOExecutionOrder
};
通过修改execution可改变下载方式:
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock{
···
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder)
{
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
···
}
SDWebImageDownloaderOperation
SDWebImageDownloaderOperation是下载操作队列,继承自NSOperation,并采用了SDWebImageOperation协议,该协议只有一个cancel方法。只暴露了一个方法。
- (id)initWithRequest:(NSURLRequest *)request
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock;
该方法的progressBlock与completeBlock
与SDWebImageDownloader
下载管理类对应。
SDWebImageDownloaderOperation
使用start
和done
来控制状态,而不使用main图片的下载使用NSURLConnection
,在协议中接收数据并回调Block通知Block通知下载进度和下载完成。
SDWebImageManager
SDWebImageManager
是一个单例管理类负责协调图片的缓存和图片的下载,隐藏在UIImageView + WebCache
背后,是对SDImageCache
和SDWebImageDownloader
的封装。
@property (strong, nonatomic, readwrite) SDImageCache *imageCache;
@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;
在一般使用中,我们并不直接使用SDImageCache和SDWebImageDownloader
,而使用SDWebImageManager
的核心方法是
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
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;
Categories(分类)
UIImageView+WebCache
在Categories目录下实现了多个分类,实现方法是一致的。其中使用最多的是UIImageView+WebCache,针对UIIMageView扩展了一些方法。
//UIImageView+WebCache
- (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 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 SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:nil
progress:progressBlock
completed:completedBlock];
}
- (void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url];
UIImage *lastPreviousCachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:key];
[self sd_setImageWithURL:url placeholderImage:lastPreviousCachedImage ?: placeholder options:options progress:progressBlock completed:completedBlock];
}
在使用调用的方法是
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock
该方法依赖与SDWebIamgeManager,从SDWebImageManager管理类中获取图片并刷新显示,至于图片是从缓存中得到还是从网络上下载的对UIImageView是透明的。
UIView+WebCache
新版本还给UIView增加了分类,即UIView+WebCache,最终上述方法会走到下面的方法去具体操作,比如下载图片等。
//UIView+WebCache
- (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 {
return [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options operationKey:operationKey setImageBlock:setImageBlock progress:progressBlock completed:completedBlock context:nil];
}
//参数一:url 图像的url。
//参数二:placeholder 最初要设置的图像,直到图像请求完成。
//参数三:options 下载图像时要使用的选项。
//参数四:context 上下文包含用于执行指定更改或过程的不同选项
//参数五:setImageBlock 块用于自定义设置图像代码。
//参数六:progressBlock 下载图像时调用的块。进度块是在后台队列上执行的。
/*这个块没有返回值,并以请求的UIImage作为第一个参数,NSData表示作为第二个参数。
如果发生错误,image参数为nil,第三个参数可能包含一个NSError。
第四个参数是一个“SDImageCacheType”enum,表示图像是从本地缓存、内存缓存还是网络检索到的。
第五个参数通常总是“YES”。但是,如果你提供SDWebImageAvoidAutoSetImage与SDWebImageProgressiveLoad选项,以启用渐进下载和设置自己的映像。因此,对部分图像重复调用该块。当图像完全下载后,最后一次调用该块,并将最后一个参数设置为YES。
最后一个参数是原始图像URL。*/
六,SDWebImage的使用
1.使用UIImageVie+WebCache category来加载UItableView中的cell的图片
2.使用Blocks,采用这个方案可以在网络图片加载过程中得知图片的下载进度和图片加载成功与否
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://img1.cache.netease.com/catchpic/5/51/5132C377F99EEEE927697E62C26DDFB1.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
// ... completion code here ...
}];
3.使用SDWebImageManager,SDWebImageManager为UIImageView+WebCache category的实现提供接口。
SDWebImageManager *manager = [SDWebImageManager sharedManager] ;
[manager downloadImageWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image) {
// do something with image
}
}];
4.加载图片还有使用SDWebImageDownloader和SDImageCache方式
5.key的来源
// 利用Image的URL生成一个缓存时需要的key.
// 这里有两种情况,第一种是如果检测到cacheKeyFilter不为空时,利用cacheKeyFilter来处理URL生成一个key.
// 如果为空,那么直接返回URL的string内容,当做key.
- (NSString *)cacheKeyForURL:(NSURL *)url {
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
}
else {
return [url absoluteString];
}
}
SDWebImage的流程
SDWebImage解析
- 入口
setImageWithURL:placeholderImage:options:
会先把placeholderImage显示,然后 SDWebImageManager根据URL开始处理图片。 - 进入
SDWebImageManager
-downloadWithURL:delegate:options:userInfo:,交给SDImageCache
从缓存查找图片是否已经下载queryDiskCacheForKey:delegate:userInfo:。 - 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,
SDImageCacheDelegate
回调imageCache:didFindImage:forKey:userInfo:到SDWebImageManager。 SDWebImageManagerDelegate
回调webImageManager:didFinishWithImage:到UIImageView+WebCache等前端展示图片。- 如果内存缓存中没有,生成
NSInvocationOperation
添加到队列开始从硬盘查找图片是否已经缓存。 - 根据URLKey在硬盘缓存目录下尝试读取图片文件。这一步是在NSOperation进行的操作,所以回主线程进行结果回调notifyDelegate:。
- 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate回调imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。
- 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调
imageCache:didNotFindImageForKey:userInfo:
。 - 共享或重新生成一个下载器
SDWebImageDownloader
开始下载图片。 - 图片下载由
NSURLConnection
来做,实现相关delegate来判断图片下载中、下载完成和下载失败。 - connection:didReceiveData:中利用ImageIO做了按图片下载进度加载效果。
connectionDidFinishLoading:
数据下载完成后交给SDWebImageDecoder
做图片解码处理。- 图片解码处理在一个
NSOperationQueue
完成,不会拖慢主线程 UI
。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。 - 在主线程notifyDelegateOnMainThreadWithInfo:宣告解码完成,
imageDecoder:didFinishDecodingImage:userInfo:
回调给SDWebImageDownloader
。 imageDownloader:didFinishWithImage:
回调给SDWebImageManager
告知图片下载完成。- 通知所有的downloadDelegates下载完成,回调给需要的地方展示图片。
- 将图片保存到
SDImageCache
中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独NSInvocationOperation完成,避免拖慢主线程。 - SDImageCache在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
- SDWebImage也提供了UIButton+WebCache和MKAnnotationView+WebCache,方便使用。
- SDWebImagePrefetcher可以预先下载图片,方便后续使用。
从上面流程可以看出,当你调用setImageWithURL:方法的时候,他会自动去给你干这么多事,当你需要在某一具体时刻做事情的时候,你可以覆盖这些方法。比如在下载某个图片的过程中要响应一个事件,就覆盖这个方法:
// 覆盖方法,指哪打哪,这个方法是下载imagePath2的时候响应
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadImageWithURL:imagePath2 options:SDWebImageRetryFailed progress:^(NSInteger receivedSize, NSInteger expectedSize) {
NSLog(@"显示当前进度");
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
NSLog(@"下载完成");
}];