上一篇我们遗留了一些问题:
1、UIImageView+WebCache核心代码中开始停止所有的operation;
2、SDWebImageCombinedOperation类的原因好处以及出现的subOperation的原因 ;
3、SDImageCache缓存类的实现方式 ;
4、SDWebImageDownloader下载类的实现 ;
这一篇我们主要来解决SDImageCache这个类的缓存问题。跟其他类一样,我们先看其.h文件。
SDImageCache.h
(1)枚举
typedef NS_ENUM(NSInteger, SDImageCacheType) {
SDImageCacheTypeNone,//缓存中没有该image
SDImageCacheTypeDisk,//硬盘中的image
SDImageCacheTypeMemory//内存中的image
};
(2)3个blcok
从缓存中查找图片完成的block
typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);
图片是否存在于缓存中的block
typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);
缓存中图片大小的block
typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);
(3)对象方法
//设置硬盘缓存的路径
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace;
//将path路径下的设置为只读
- (void)addReadOnlyCachePath:(NSString *)path;
//根据key存储image
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
//根据key存储image并设置是否存储在硬盘中
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
//是否计算。。。
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
核心方法:在缓存中根据key查询图片处理block
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
...其他方法我们之间看名字就能知道它是干什么的,我们介绍核心方法
SDImageCache.m文件中
(1)属性
NSCache *memCache:内存缓存
NSString *diskCachePath:硬盘缓存路径
NSMutableArray *customPaths:路径数组
dispatch_queue_t ioQueue:队列
(2)方法
第一个方法是初始化方法:
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
if ((self = [super init])) {
//缓存空间的名字
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
// initialise PNG signature data
kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
// Create IO serial queue
//
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
// Init default values
_maxCacheAge = kDefaultCacheMaxCacheAge;
// Init the memory cache
//缓存的时间
_memCache = [[AutoPurgeCache alloc] init];
_memCache.name = fullNamespace;
// Init the disk cache
//初始化硬盘缓存的路径,如果有自定义的路径 之间将名字拼接起来
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
//没有路径,获取到NSCachesDirectory路径然后拼接
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
// Set decompression to YES
//是否进行解压缩
_shouldDecompressImages = YES;
// memory cache enabled
//是否缓存到内存中
_shouldCacheImagesInMemory = YES;
// Disable iCloud
//是否支持云
_shouldDisableiCloud = YES;
//_ioQueue队列中同步执行,创建文件管理器
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
#if TARGET_OS_IPHONE
// Subscribe to app events
//添加一些通知,接收到内存警告
[[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;
}
下面我们来看看几个核心方法
(1)根据key从缓存中查找图片的方法
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// First check the in-memory cache...
//从内存中取出图片 之间调用block,返回nil
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
//采用new的方式只能采用默认的init方法完成初始化,采用alloc的方式可以用其他定制的初始化方法
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);
// cost 被用来计算缓存中所有对象的代价。当内存受限或者所有缓存对象的总代价超过了最大允许的值时,缓存会移除其中的一些对象。
// 通常,精确的 cost 应该是对象占用的字节数。
[self.memCache setObject:diskImage forKey:key cost:cost];
}
//异步函数中调用block
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
//这里主要是从缓存中取出图片的同时创建了NSOperation对象并没有进行其他操作
return operation;
}
我们来重点看下从硬盘中取出图片的方法
- (UIImage )diskImageForKey:(NSString )key
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
//根据图片的key获取到全路径 这个路径是加密路径 从系统定义的路径中添加
NSString *defaultPath = [self defaultCachePathForKey:key];
//转为NSData
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
data = [NSData dataWithContentsOfFile:[defaultPath stringByDeletingPathExtension]];
if (data) {
return data;
}
//如果没有就从路径的数组中便利所有路径取出
NSArray *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath];
if (imageData) {
return imageData;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
imageData = [NSData dataWithContentsOfFile:[filePath stringByDeletingPathExtension]];
if (imageData) {
return imageData;
}
}
return nil;
}
这部分中获取文件名字的时候SDWebimage会根据key来加密(MD5)创建文件名,代码如下
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
str = "";
}
// 使用了MD5进行加密处理
// 开辟一个16字节(128位:md5加密出来就是128bit)的空间
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
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], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];
return filename;
}
小总结:这里是根据图片的key从缓存中取出图片,先从内存中取出,能取到直接调用block,如果取不到就从硬盘中取,能取到就将图片保存在内存中,调用block。
(2)保存图片。调用的核心方法
- (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, ^{
//ioQueue,我们从字面上理解,就是一个磁盘io的dispatch_queue_t。说简单点,就是每个下载来的图片,需要进行磁盘io的过程都放在ioQueue中执行
NSData *data = imageData;
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// We need to determine if the image is a PNG or a JPEG
// PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
// The first eight bytes of a PNG file always contain the following (decimal) values:
// 137 80 78 71 13 10 26 10
// If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download)
// and the image has an alpha channel, we will consider it PNG to avoid losing the transparency
// 我们需要判断image是PNG还是JPEG
// PNG的图片很容易检测出来,因为它们有一个特定的标示 (http://www.w3.org/TR/PNG-Structure.html)
// PNG图片的前8个字节不许符合下面这些值(十进制表示)
// 137 80 78 71 13 10 26 10
//0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A
// 如果imageData为空(举个例子,比如image在下载后需要transform,那么就imageData就会为空)
// 并且image有一个alpha通道, 我们将该image看做PNG以避免透明度(alpha)的丢失(因为JPEG没有透明色)
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]) {
//判断是否是png图片
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
// 当然,如果不是在iPhone平台上,就使用下面这个方法。不过不在我们研究范围之内
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
if (data) {
// 首先判断disk cache的文件路径是否存在,不存在的话就创建一个
// disk cache的文件路径是存储在_diskCachePath中的
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key url路径
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
// 根据存储的路径(cachePathForKey)和存储的数据(data)将其存放到iOS的文件系统
[_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
// disable iCloud backup
// 如果不使用iCloud进行备份,就使用NSURLIsExcludedFromBackupKey
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
});
}
}
以上我们解决了两个核心的方法,一个是通过key在缓存中查询,一个是将图片保存在缓存中。
有了缓存我们当然要清除缓存,SDWebimage中也提供了清除缓存的方法,清除修改时间最早的file,代码已经奉上。
// 实现了一个简单的缓存清除策略:清除修改时间最早的file
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
// 获取文件的过期时间,SDWebImage中默认是一个星期
// 不过这里虽然称*expirationDate为过期时间,但是实质上并不是这样。
// 其实是这样的,比如在2015/12/12/00:00:00最后一次修改文件,对应的过期时间应该是
// 2015/12/19/00:00:00,不过现在时间是2015/12/27/00:00:00,我先将当前时间减去1个星期,得到
// 2015/12/20/00:00:00,这个时间才是我们函数中的expirationDate。
// 用这个expirationDate和最后一次修改时间modificationDate比较看谁更晚就行。
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
// 用来存储对应文件的一些属性,比如文件所需磁盘空间
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
// 记录当前已经使用的磁盘缓存大小
NSUInteger currentCacheSize = 0;
// Enumerate all of the files in the cache directory. This loop has two purposes:
//
// 1. Removing files that are older than the expiration date.
// 2. Storing file attributes for the size-based cleanup pass.
// 在缓存的目录开始遍历文件. 此次遍历有两个目的:
//
// 1. 移除过期的文件
// 2. 同时存储每个文件的属性(比如该file是否是文件夹、该file所需磁盘大小,修改时间)
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// Skip directories.
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
// 移除过期文件
// 这里判断过期的方式:对比文件的最后一次修改日期和expirationDate谁更晚,如果expirationDate更晚,就认为该文件已经过期,具体解释见上面
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
// 计算当前已经使用的cache大小,
// 并将对应file的属性存到cacheFiles中
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 如果我们当前cache的大小已经超过了允许配置的缓存大小,那就删除已经缓存的文件。
// 删除策略就是,首先删除修改时间更早的缓存文件
// If our remaining disk cache exceeds a configured maximum size, perform a second
// size-based cleanup pass. We delete the oldest files first.
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
// 直接将当前cache大小降到允许最大的cache大小的一半
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
// Sort the remaining cache files by their last modification time (oldest first).
// 根据文件修改时间来给所有缓存文件排序,按照修改时间越早越在前的规则排序
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// Delete files until we fall below our desired cache size.
// 每次删除file后,就计算此时的cache的大小
// 如果此时的cache大小已经降到期望的大小了,就停止删除文件了
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();
});
}
});
}
当app进入后台后的操作
- (void)backgroundCleanDisk {
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
// Clean up any unfinished task business by marking where you
// stopped or ending the task outright.
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
// Start the long-running task and return immediately.
[self cleanDiskWithCompletionBlock:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
}
缓存类到此就结束了,中间有些没有介绍到的方法,应该有基础的同学都能看懂,其实代码看懂不难,问题是当我们看到一个框架后里面很多方法,各种自定义的类,我们就会不知所措,所以经过分块来学习知道了核心代码的意义,其他的方法也会迎刃而解。
遗留问题总结:
1、UIImageView+WebCache核心代码中开始停止所有的operation;
2、SDWebImageCombinedOperation类的原因好处以及出现的subOperation的原因 ;
3、SDWebImageDownloader下载类的实现 ;
还有这三个问题,下一篇我们解决最最核心的SDWebImageDownloader类,也就是下载图片的方法,看看SDWebimage是如何处理这些下载器的。