源码地址:https://github.com/rs/SDWebImage
版本:3.7
SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能。它具有以下功能:
提供UIImageView的一个分类,以支持网络图片的加载与缓存管理
一个异步的图片加载器
一个异步的内存+磁盘图片缓存,并具有自动缓存过期处理功能
支持GIF图片
支持WebP图片
后台图片解压缩处理
确保同一个URL的图片不被下载多次
确保虚假的URL不会被反复加载
确保下载及缓存时,主线程不被阻塞
优良的性能
使用GCD和ARC
支持Arm64
在这个SDWebImage源码解析的第一篇,我们将先关注它的异步图片缓存部分。SDWebImage的模块化非常出色,能独立的使用包括异步图片缓存在内的很多模块。
下面就从类的实例开始,对SDImageCache模块源码进行分析,本文源码解析的顺序为自己阅读顺序。
实例
SDImageCache类管理着内存缓存和可选的磁盘缓存,提供了一个方便的单例sharedImageCache。如果你不想使用default缓存空间,而想创建另外的空间,你可以创建你自己的SDImageCache对象来管理缓存。
+ (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;
}
单例sharedImageCache使用default的命名空间,而我们自己也可以通过使用initWithNamespace:或initWithNamespace:diskCacheDirectory:来创建另外的命名空间。
内存缓存类
@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事件注册使用经典的观察者模式,当观察到内存警告、程序被终止、程序进入后台这些事件时,程序将自动调用相应的方法处理。
内存警告
- (void)clearMemory {
[self.memCache removeAllObjects];
}
上面是收到UIApplicationDidReceiveMemoryWarningNotification时,调用的@selector(clearMemory),在方法中调用内存缓存类AutoPurgeCache的方法removeAllObject。
程序被终止
- (void)cleanDisk {
[self cleanDiskWithCompletionBlock:nil];
}
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 使用目录枚举器获取缓存文件的三个重要属性:(1)URL是否为目录;(2)内容最后更新日期;(3)文件总的分配大小。
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
// 计算过期日期,默认为一星期前的缓存文件认为是过期的。
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// 枚举缓存目录的所有文件,此循环有两个目的:
//
// 1. 清除超过过期日期的文件。
// 2. 为以大小为基础的第二轮清除保存文件属性。
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// 跳过目录.
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 记录超过过期日期的文件;
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();
});
}
});
}
当收到UIApplicationWillTerminateNotification时,SDImageCache将会使用ioQueue异步地清理磁盘缓存。
具体清理逻辑:
- 先清除已超过最大缓存时间的缓存文件(最大缓存时间默认为一星期)
- 在第一轮清除的过程中保存文件属性,特别是缓存文件大小
- 在第一轮清除后,如果设置了最大缓存并且保留下来的磁盘缓存文件仍然超过了配置的最大缓存,那么进行第二轮以大小为基础的清除。
- 首先删除最老的文件,直到达到期望的总的缓存大小,即最大缓存的一半。
程序进入后台
- (void)backgroundCleanDisk {
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
// 清理任何未完成的任务作业,标记完全停止或结束任务。
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
// 开始长时间后台运行的任务并且立即return。
[self cleanDiskWithCompletionBlock:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
}
当收到UIApplicationDidEnterBackgroundNotification时,在手机系统后台进行如上面描述的异步磁盘缓存清理。这里利用Objective-C的动态语言特性,得到UIApplication的单例sharedApplication,使用sharedApplication开启后台任务cleanDiskWithCompletionBlock:。
查询图片
- (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;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
//将图片保存到NSCache中,并把图片像素大小作为该对象的cost值
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
return image.size.height * image.size.width * image.scale * image.scale;
}
查询缓存,默认使用方法queryDiskCacheForKey:done:,如果此方法返回nil,则说明缓存中现在还没有这张照片,因此你需要得到并缓存这张图片。缓存key是缓存图片的程序唯一的标识符,一般使用图片的完整URL。
如果不想SDImageCache查询磁盘缓存,你可以调用另一个方法:imageFromMemoryCacheForKey:。
返回值为NSOpration,单独使用SDImageCache没用,但是使用SDWebImageManager就可以对多个任务的优先级、依赖,并且可以取消。
自定义@autoreleasepool,autoreleasepool代码段里面有大量的内存消耗操作,自定义autoreleasepool可以及时地释放掉内存。
//返回缓存完整路径,其中文件名是根据key值生成的MD5值,具体生成方法见后文解析
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
NSString *filename = [self cachedFileNameForKey:key];
return [path stringByAppendingPathComponent:filename];
}
- (NSString *)defaultCachePathForKey:(NSString *)key {
return [self cachePathForKey:key inPath:self.diskCachePath];
}
//从默认路径和只读的bundle路径中搜索图片
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
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;
}
}
return nil;
}
- (UIImage *)diskImageForKey:(NSString *)key {
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
UIImage *image = [UIImage sd_imageWithData:data];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}
上面代码段是从磁盘获取图片的代码。得到图片对应的UIData后,还要经过如下步骤,才能返回对应的图片:
- 根据图片的不同种类,生成对应的UIImage
- 根据key值,调整image的scale值
- 如果设置图片需要解压缩,则还需对UIImage进行解码
MD5计算
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
str = "";
}
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;
}
iOS经典的MD5值计算方法,这段代码大家可以拿去重用。
保存图片
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值就好了。