SDWebImage源码详解 - 缓存
缓存的实现可以显著的减少网络流量的消耗,先将下载的图片缓存到本地,下次获取同一张图片的时候,可以直接在本地缓存中获取,而不用访问服务器重新获取图片,这样不仅可以减少网络流量的消耗,并且提升了用户体验(图片加载速度快)。SDWebImage的缓存由SDImageCache类来实现,这是一个单例类,该类负责处理内存缓存及一个可选的磁盘缓存,其中磁盘缓存的写操作是异步的,这样就不会对UI操作造成影响。此外还提供了若干属性和接口来配置和操作缓存对象。
先来看看SDImageCache的头文件内容
//定义三个枚举常量,以控制缓存的存储选项
typedef NS_ENUM(NSInteger, SDImageCacheType) {
//不使用缓存策略,从网络下载
SDImageCacheTypeNone,
//从磁盘中缓存中获取图片
SDImageCacheTypeDisk,
//从内存中获取图片
SDImageCacheTypeMemory
};
//回调函数类型变量
typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);
typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);
typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);
@interface SDImageCache : NSObject
//是否在缓存之前解压图片,此项操作可以提升性能,但是会消耗较多的内存,默认是YES。注意:如果内存不足,可以置为NO
@property (assign, nonatomic) BOOL shouldDecompressImages;
//是否禁止iCloud备份,默认是YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;
//是否启用内存缓存 默认是YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
//内存最大容量
@property (assign, nonatomic) NSUInteger maxMemoryCost;
//内存对象的最大数目
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
//磁盘缓存保留的最长时间
@property (assign, nonatomic) NSInteger maxCacheAge;
//磁盘缓存最大容量,以字节为单位
@property (assign, nonatomic) NSUInteger maxCacheSize;
//返回缓存对象的单例
+ (SDImageCache *)sharedImageCache;
//以ns为缓存空间名字初始化缓存
- (id)initWithNamespace:(NSString *)ns;
//在directory目录下,以ns为缓存空间名字初始化缓存
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
//返回磁盘缓存空间的路径
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace;
//添加只读内存空间路径,一般用在图片已经下载置相应的缓存目录
- (void)addReadOnlyCachePath:(NSString *)path;
//以key为键值将图片image存储置缓存中
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
//以key为键值将图片image存储置缓存中,toDisk控制是否写入磁盘缓存
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
//以key为键值将图片image存储置缓存中,toDisk控制是否写入磁盘缓存,此外如果recalculate为YES或imageData有数据,则将imageData存储置磁盘缓存中
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
//在内存或磁盘缓存中以key为键值查找图片缓存,如果找到则执行doneBlock回调
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
//在内存缓存中查找图片缓存,并返回图片对象
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
//在硬盘缓存中查找图片缓存,并返回图片对象
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
//在内存或硬盘缓存中删除指定key缓存
- (void)removeImageForKey:(NSString *)key;
//在内存或硬盘缓存中删除指定key缓存,完成后执行响应回调
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
//在内存或硬盘缓存中删除指定key缓存,fromDisk控制是否删除磁盘缓存对象
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
//在内存或硬盘缓存中删除指定key缓存,完成后执行响应回调,fromDisk控制是否删除磁盘缓存对象
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
//清除内存缓存
- (void)clearMemory;
//清除磁盘缓存,完成后执行回调
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk;
//清除过期缓存,如果缓存容量超过限制,则清除部分缓存直至达到预期目标为止
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)cleanDisk;
//返回磁盘缓存的大小
- (NSUInteger)getSize;
//返回磁盘缓存对象的数目
- (NSUInteger)getDiskCount;
//异步计算磁盘缓存所需大小
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
//异步查看磁盘缓存中是否存在指定key的图片,完成后执行回调
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (BOOL)diskImageExistsWithKey:(NSString *)key;
//返货指定路径path下的key对象的缓存路径
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
//返回默认路径下key对象的缓存路径
- (NSString *)defaultCachePathForKey:(NSString *)key;
从头文件可以看出,SDWebImage的缓存对象提供了几个属性(缓存时间,缓存大小限制等)和若干函数来对缓存对象进行操作(获取、移除及清空图片)。对于这么多的函数,有些其实仅仅是调用而已,只需关注几个主要函数即可,稍后我们将会针对几个主要函数进行讲解。
</br>
SDWebImage缓存的主要实现分别采用了内存缓存和磁盘缓存,内存缓存使用NSCash对象来实现,NSCache是一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类,NSCache类的详细用法,这里不过多介绍,以后有机会专门介绍。磁盘缓存则使用NSFileManager对象来实现。图片存储的位置是位于app的Cache文件夹下。另外,SDImageCache还定义了一个串行队列,来异步存储图片。接下我们就代码的执行流程来详细的看一下代码的实现:
初始化缓存空间
//获取内存对象的单例
+ (SDImageCache *)sharedImageCache {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
ImageCache单例对象由函数new来初始换,而new函数默认调用init函数。
- (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];
// 创建IO 串行对垒
_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;
_shouldDisableiCloud = YES;
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
#if TARGET_OS_IPHONE
// 注册系统通知事件
[[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;
}
通过代码可以看出,ImageCache对象的初始化工作,分别创建了内存缓存空间和磁盘缓存空间,这里面有一个函数-(NSString *)makeDiskCachePath:(NSString*)fullNamespace
木有出现,这个函数的主要作用就是返回app的缓存目录。
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace{
//获取app的缓存文件夹
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
//返回缓存文件夹下以fullNamespace命名的路径
return [paths[0] stringByAppendingPathComponent:fullNamespace];
}
保存图片
虽然ImageCache对外提供了许多保存图片置缓存的函数,但是这么多函数都调用一个基础函数- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
,具体实现如下:
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
// 如果保存置内存缓存属性为YES,则将图片保留在内存缓存中
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
//如果需要保存在磁盘缓存中,则将写人磁盘缓存的队列放入创建的串行队列ioQueue中
if (toDisk) {
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
//如果recalculate为YES或者data数据为空,但是image有数据,则对iamge图片做处理
//如果recalculate为YES并且data数据非空,则直接对data数据进行保存
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 需要确定图片是PNG还是JPEG。PNG图片容易检测,因为有一个唯一签名。
// PNG图像的前8个字节总是包含以下值:137 80 78 71 13 10 26 10
// 在imageData为nil的情况下假定图像为PNG。我们将其当作PNG以避免丢失透明度。
//而当有图片数据时,我们检测其前缀,确定图片的类型
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// But if we have an image data, we will look at the preffix
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];
}
//以图片的URL做MD5转换后的文件名创建缓存文件
NSString *cachePathForKey = [self defaultCachePathForKey:key];
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];
}
}
});
}
}
查询图片
ImageCache对外提供了三个查询缓存图片的接口函数
//在内存和磁盘缓存中查找key指定的图片
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
//在内存缓存中查找key指定的图片
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
//先在内存缓存中查找,然后在磁盘缓存中查找key指定的图片
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
这里看一下第一个函数的实现,其他类似
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// 先在内存缓存中查找
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
//如果内存缓存中没有找到,则去磁盘缓存中去查找- (UIImage *)diskImageForKey:(NSString *)key
//在磁盘缓存中找到后,同时更新置内存缓存中
//有回调则调用doneBlock回调
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
移除图片
ImageCache对外提供了四个删除缓存图片的函数,
- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
移除函数比较简单,也有一个基础函数- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
,这个函数比较简单,删除内存缓存,删除磁盘下的缓存文件,看一看代码就明白什么意思,这里就不过多说明
清理缓存
清理缓存图片的清理操作有内存清理和磁盘缓存清理,而磁盘缓存又可以分为完全清空和部分清理。完全清空操作是直接把缓存的文件夹移除,清空操作有以下三个方法:
- (void)clearMemory;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk
这三个函数比较简单,也不过多介绍
接下来我们详细介绍一下部分清理,部分清空针对磁盘缓存,根据我们设定的一些参数值来移除一些文件,这里主要有两个指标:文件的缓存有效期及最大缓存空间大小。文件的缓存有效期可以通过maxCacheAge属性来设置,默认是1周的时间。如果文件的缓存时间超过这个时间值,则将其移除。而最大缓存空间大小是通过maxCacheSize属性来设置的,如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序排序,循环移除那些较早的文件,直到磁盘缓存的实际大小小于或等于我们设置的空间预设目标,这里设为最大缓存大小的一半。清理的操作在-cleanDiskWithCompletionBlock:方法中
,其实现如下:
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 该枚举器预先获取缓存文件的有用的属性,文件夹,修改时间,文件大小
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// 枚举缓存文件夹中所有文件,
//该迭代有两个目的:移除比过期日期更老的文件;存储文件属性以备后面执行基于缓存大小的清理操作
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// 跳过文件夹
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 将需要删除的文件,加入需要删除的数组urlsToDelete中
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
//存储有效期内的文件大小,留作备用
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
//删除过期缓存
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 如果磁盘缓存的大小大于我们配置的最大大小,则执行基于文件大小的清理,首先删除最老的文件
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// 以设置的最大缓存大小的一半作为清理目标
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
// 按照最后修改时间来排序剩下的缓存文件
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// 循环删除文件,直到缓存总大小降到我们期望的大小
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
//有回调则执行回调
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
到这里缓存的实现就讲解的差不多了,这里我们主要分析了SDWebImage的SDImageCache缓存类的相关操作,着重介绍了几个主要的操作,另外SDImageCache还提供了一些其他的辅助方法如获取缓存大小、缓存中图片的数量、判断缓存中是否存在某个key指定的图片,具体的实现可以参照源码,实现都不怎么复杂。
</br>
下一节我们主要介绍一下异步下载器的实现。
网友评论