一、使用
我们在项目中使用SDWebImage时首先会配置项目缓存策略
// 设置图片缓存时间 3天 ===> 默认是7天
[SDImageCacheConfig defaultCacheConfig].maxDiskAge = 3*60*60*24;
// 设置图片本地缓存 最多 250M===> 默认为0,无限制
[SDImageCacheConfig defaultCacheConfig].maxDiskSize = 1024*250;
// 设置缓存图片个数最多1000张===> 默认为0,无限制
[SDImageCacheConfig defaultCacheConfig].maxMemoryCount = 1000;
其次是使用
[self.ImageView sd_setImageWithURL:[NSURL URLWithString:urlStr] placeholderImage:[UIImage imageNamed:@"me_login_avatar"]];
二、实现原理
下面是通过源码来解读SDWebImage如何实现图片下载、图片解码、图片内存缓存、图片磁盘缓存、图片磁盘定期清理功能的?
1. imageView调用方法sd_setImageWithURL:
时,内部是调用UIView+WebCache
中的sd_internalSetImageWithURL:
, 里面会做的事情有:
// UIImageView+WebCache调用下面方法
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder;
// 下面是简化的UIView+WebCache中的方法
- (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 {
// 1.查找自身View的最近的下载操作,如果有则取消
validOperationKey = NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// 2.设置占位图
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
// 3.通过[SDWebImageManager sharedManager];创建下载操作opration
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options
context:context progress:combinedProgressBlock
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType) {
//...
}];
// 4.将创建好的下载操作opration保存在字典中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];//validOperationKey是自身类名
}
2. 在创建下载任务operation
时做了什么?
-
SDWebImageManager
创建下载操作opration
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock {
//1.创建SDWebImageCombinedOperation* operation里面负责取消下载操作功能
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
//2.将 operation存放到Manager的正在下载操作数组中
[self.runningOperations addObject:operation];
//3.调用callCacheProcessForOperation:去执行查找缓存和下载
[self callCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
}
- 由
self.imageCache
执行查找缓存
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 1.判断是否设置的只允许新的下载
BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
if (shouldQueryCache) {
// 2.根据url得到查找缓存的key,未设置cacheKeyFilter则为url.absoluteString
NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
@weakify(operation);
// 3.让self.imageCache去执行缓存查找,默认cache = [SDImageCache sharedImageCache];
operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
if (!operation || operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
// 4.去执行下载任务
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
} else {
// 2.去执行下载任务
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}
}
- 创建下载任务并将回调completedBlock
- (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 {
if (shouldDownload) {
// 1.创建下载任务:self.imageLoader = [SDWebImageDownloader sharedDownloader];
operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
// ...
}];
} else if (cachedImage) {
//2.不需要下载且有缓存图片:调用completedBlock并且将operation移除
}else{
// 3.不需要下载且没有缓存图片:调用completedBlock并且将operation移除
}
}
- SDWebImageDownloader下载图片任务创建
/// 上一步中调用来到这里(- requestImageWithURL:options:context:progress:completed:)
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
// 1.根据url取出对应的下载操作
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
if (!operation || operation.isFinished || operation.isCancelled) {
// 2.没有URL对应的下载操作,则新建一个下载opration == SDWebImageDownloaderOperation类型
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
operation.completionBlock = ^{// 任务完成后将operation从self.URLOperations移除
[self.URLOperations removeObjectForKey:url];
};
// 3.将任务保存在self.URLOperations
self.URLOperations[url] = operation;
// 4.将下载操作添加到downloadQueue,添加到queue中时就开始下载了
[self.downloadQueue addOperation:operation];
}
// 5.创建SDWebImageDownloadToken *token来负责记录url、request
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
token.url = url;
token.request = operation.request;
token.downloadOperationCancelToken = downloadOperationCancelToken;
token.downloader = self;
}
- 创建下载任务
SDWebImageDownloaderOperation
细节
- (NSOperation *)createDownloaderOperationWithUrl:(nonnull NSURL *)ur
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context {
// 1.创建mutableRequest
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
// 2. 创建SDWebImageDownloaderOperation对象operation
operationClass = [SDWebImageDownloaderOperation class];
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
// 3.将self.config.urlCredential证书赋值给operation.credential
operation.credential = self.config.urlCredential;
}
- SDWebImageDownloaderOperation如何写下载任务的?
/// 重写了start方法,SDWebImageDownloaderOperation对象添加到queue时就会调用start
- (void)start {
// 1. 创建下载任务NSURLSessionDataTask
self.dataTask = [session dataTaskWithRequest:self.request];
[self.dataTask resume];// 任务开启
}
- 下载请求时session代理回调的处理, 因为里面主要是做下载进度和结果回调处理的、图片解码
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
// 简化过了
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
[self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
}
});
}
/// 解码图片:在self.coderQueue中执行,如果不解码直接设置为Imageview.image,系统会在主线程解码导致滑动卡顿。
3. 下载任务operation
完成后做了什么?
// 下载成功后completed的block会调用下面方法
- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
downloadedImage:(nullable UIImage *)downloadedImage
downloadedData:(nullable NSData *)downloadedData
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 这里简写了逻辑
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:storeCacheType completion:nil];
completionBlock(image, data, err0or, cacheType, finished, url);
}
4. 下载完后completionBlock中做了什么? 将图片设置到view上。
5. 下载任务创建前查找缓存时做了什么?
- (NSOperation *)queryCacheOperationForKey:(NSString *)key options:(SDImageCacheOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(SDImageCacheQueryCompletionBlock)doneBlock {
UIImage *image;
// 1. 首先从内存中根据key查找:key是url.absoluteString
if (queryCacheType != SDImageCacheTypeDisk) {
image = [self imageFromMemoryCacheForKey:key];
}
// 2.写好queryDiskBlock
void(^queryDiskBlock)(void) = ^{
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) {
//2.1 如果是MemoryCache中有,则使用内存缓存的图片
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
//2.2 将从磁盘中找出的图片解码为diskImage
cacheType = SDImageCacheTypeDisk;
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
// 2.3将解码后的图片设置到MemoryCache中
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
// 2.4 回调查找完成block
if (doneBlock) {
doneBlock(diskImage, diskData, cacheType);
}
}
};
// 3.将任务添加到self.ioQueue中,让子线程执行queryDiskBlock
if (shouldQueryDiskSync) {
dispatch_sync(self.ioQueue, queryDiskBlock);
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
}
- 从内存中查找图片的细节
SDMemoryCache类,继承于NSCache类,用法是一样的,这个类中新增的功能
[self.memCache objectForKey:key];
- 从磁盘中查找的细节
- (NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
NSData *data = [self.diskCache dataForKey:key];
if (data) {
return data;
}
}
// SDDiskCache类中
- (NSData *)dataForKey:(NSString *)key {
// 根据这个key,创建filepath:
NSString *filePath = [self cachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
if (data) {
return data;
}
return nil;
}
// filePath是怎么创建的?文件夹+文件名
if (directory != nil) {
// 1.首先是文件夹:如果有自己设置则是自己设置的文件夹/ns
_diskCachePath = [directory stringByAppendingPathComponent:ns];
} else {
// 2.默认创建的文件夹~/Library/Caches/com.hackemist.SDImageCache/default
NSString *path = [[[self userCacheDirectory] stringByAppendingPathComponent:@"com.hackemist.SDImageCache"] stringByAppendingPathComponent:ns];
_diskCachePath = path;
}
// 文件名:因为key=url. absoluteString,所以这里的文件名就是url经过MD5后+文件类型名后缀
static inline NSString *SDDiskCacheFileNameForKey(NSString * key) {
const char *str = key.UTF8String;
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSURL *keyURL = [NSURL URLWithString:key];
// ext==文件类型名
NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
return filename;
}
三、缓存策略的实现
SDWebImage缓存部分实现源码解析这篇博客已经讲得很详细了。这里罗列一下重要事项:
- 有内存缓存SDMemoryCache、内存缓存弱引用weakCache和磁盘缓存SDDiskCache三种方式。
- 对于内存缓存, 监听了系统内存警告的通知,收到通知时清楚所有的图片内存缓存。
/// 清除的时候在内存中仍然维持着一份弱引用weakCache,只要这些弱引用的对象仍然被其他对象(比如UIImageView)所持有,那仍然会在该类中找到。
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveMemoryWarning:)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
//移除父类的对象
[super removeAllObjects];
}
- 磁盘写入图片时,首先是将图片转化成NSData,然后再写入文件中。读取时先读取出NSData,再解码为图片。
- 清除图片缓存的时机是程序进后台和退出的时候
//注册通知,大意就是在程序进后台和退出的时候,清理一下磁盘
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
- (void)applicationWillTerminate:(NSNotification *)notification {
[self deleteOldFilesWithCompletionBlock:nil];
}
- (void)applicationDidEnterBackground:(NSNotification *)notification {
UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
[self deleteOldFilesWithCompletionBlock:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
}
-
删除时是先删除过期的文件,再判断总存储图片的大小来一个一个删,直到符合设定的最大存储的一半.
-
下载时图片最大并发下载数和超时时间默认
- (instancetype)init {
self = [super init];
if (self) {
_maxConcurrentDownloads = 6; // 6张图片
_downloadTimeout = 15.0; // 15秒超时
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;// 下载任务是先进先出
}
return self;
}
四、扩展使用
图片解码的细节,及如何支持gif、webp图片格式的加载。SDWebImage的图片解码源码阅读
阅读源码时会涉及到的技术点:
NSOperation、NSOperationQueue详尽总结
网友评论