这里会做一些源码的解析,也会对使用较多的类或方法进行简单的介绍!
目前为止sdwebimage已经更新到4.0.0 测试的第二个版本,我们拿到源码来看看。
只要修改源码的同志们记得吃药,大的架构永远不会变。
一. SDImageCache (sdwebimage缓存类)
我们先来看看SDImageCache.h文件中的内容
#import <Foundation/Foundation.h>
#import "SDWebImageCompat.h"
@class SDImageCacheConfig;
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
};
typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);
typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);
typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);
/**
* SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed
* asynchronous so it doesn’t add unnecessary latency to the UI.
* SDImageCache类用于操控内存缓存和可选磁盘缓存。 磁盘高速缓存写入操作是异步执行的,因此不会对UI增加不必要的延迟
*/
@interface SDImageCache : NSObject
#pragma mark - Properties
/**
* Cache Config object - storing all kind of settings
* 默认:解压缩,但是会消耗很大内存,如果程序崩溃,设为NO
* 默认:禁用iCloud备份
* 默认:最长缓存时间 单位:秒 一周
* 默认:内存缓存
*/
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
/**
* The maximum "total cost" of the in-memory image cache. The cost function is the number of pixels held in memory.
* 内存中图像缓存的最大“总成本”。 成本函数是存储器中保存的像素数
*/
@property (assign, nonatomic) NSUInteger maxMemoryCost;
/**
* The maximum number of objects the cache should hold.
* 缓存应该保留的对象的最大数量
*/
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
#pragma mark - Singleton and initialization
/**
* Returns global shared cache instance
*
* @return SDImageCache global instance
*/
//单例
+ (nonnull instancetype)sharedImageCache;
/**
* Init a new cache store with a specific namespace
*
* @param ns The namespace to use for this cache store
*/
//同下
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;
/**
* Init a new cache store with a specific namespace and directory
*
* @param ns The namespace to use for this cache store
* @param directory Directory to cache disk images in
*/
//初始化,执行此方法会根据ns及directory生成缓存在磁盘中的路径diskCachePath
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;
#pragma mark - Cache paths
//同上,生成diskCachePath(两者又是不同的,具体,看.m文件区别)
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace;
/**
* Add a read-only cache path to search for images pre-cached by SDImageCache
* Useful if you want to bundle pre-loaded images with your app
*
* @param path The path to use for this read-only cache path
*/
- (void)addReadOnlyCachePath:(nonnull NSString *)path;
#pragma mark - Store Ops
/**
* Asynchronously store an image into memory and disk cache at the given key.
*
* @param image The image to store
* @param key The unique image cache key, usually it's image absolute URL
* @param completionBlock A block executed after the operation is finished
*/
//根据key缓存图片(通常情况下key是image absolute URL)
- (void)storeImage:(nullable UIImage *)image
forKey:(nullable NSString *)key
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
/**
* Asynchronously store an image into memory and disk cache at the given key.
*
* @param image The image to store
* @param key The unique image cache key, usually it's image absolute URL
* @param toDisk Store the image to disk cache if YES
* @param completionBlock A block executed after the operation is finished
*/
//同上,指示是否写入磁盘
- (void)storeImage:(nullable UIImage *)image
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
/**
* Asynchronously store an image into memory and disk cache at the given key.
*
* @param image The image to store
* @param imageData The image data as returned by the server, this representation will be used for disk storage
* instead of converting the given image object into a storable/compressed image format in order
* to save quality and CPU
* @param key The unique image cache key, usually it's image absolute URL
* @param toDisk Store the image to disk cache if YES
* @param completionBlock A block executed after the operation is finished
*/
//同上 imagedata服务器返回的未解压的数据
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
/**
* Synchronously store image NSData into disk cache at the given key.
*
* @warning This method is synchronous, make sure to call it from the ioQueue
*
* @param imageData The image data to store
* @param key The unique image cache key, usually it's image absolute URL
*/
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;
#pragma mark - Query and Retrieve Ops
/**
* Async check if image exists in disk cache already (does not load the image)
*
* @param key the key describing the url
* @param completionBlock the block to be executed when the check is done.
* @note the completion block will be always executed on the main queue
*/
//在磁盘中查找是否有key对应的image对象,block中是一bool值
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;
/**
* Operation that queries the cache asynchronously and call the completion when done.
*
* @param key The unique key used to store the wanted image
* @param doneBlock The completion block. Will not get called if the operation is cancelled
*
* @return a NSOperation instance containing the cache op
*/
//先在memory中查找,找不到去disk查找(sdwebimageManager的回调方法)
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;
/**
* Query the memory cache synchronously.
*
* @param key The unique key used to store the image
*/
//查找内存中key对应的图片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key;
/**
* Query the disk cache synchronously.
*
* @param key The unique key used to store the image
*/
//查找磁盘中key对应的图片
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key;
/**
* Query the cache (memory and or disk) synchronously after checking the memory cache.
*
* @param key The unique key used to store the image
*/
//这个注释什么意思?after checking the memory cache?
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;
#pragma mark - Remove Ops
/**
* Remove the image from memory and disk cache asynchronously
*
* @param key The unique image cache key
* @param completion A block that should be executed after the image has been removed (optional)
*/
//内存、磁盘都删
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;
/**
* Remove the image from memory and optionally disk cache asynchronously
*
* @param key The unique image cache key
* @param fromDisk Also remove cache entry from disk if YES
* @param completion A block that should be executed after the image has been removed (optional)
*/
//可选磁盘中的文件是否删除
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;
#pragma mark - Cache clean Ops
/**
* Clear all memory cached images
*/
//清空内存
- (void)clearMemory;
/**
* Async clear all disk cached images. Non-blocking method - returns immediately.
* @param completion A block that should be executed after cache expiration completes (optional)
*/
//清理磁盘中的所有文件
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;
/**
* Async remove all expired cached image from disk. Non-blocking method - returns immediately.
* @param completionBlock A block that should be executed after cache expiration completes (optional)
*/
//清除过期文件
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;
#pragma mark - Cache Info
/**
* Get the size used by the disk cache
*/
//获取缓存文件总大小
- (NSUInteger)getSize;
/**
* Get the number of images in the disk cache
*/
//获取缓存文件数量
- (NSUInteger)getDiskCount;
/**
* Asynchronously calculate the disk cache's size.
*/
//计算缓存总大小
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock;
#pragma mark - Cache Paths
/**
* Get the cache path for a certain key (needs the cache path root folder)
*
* @param key the key (can be obtained from url using cacheKeyForURL)
* @param path the cache path root folder
*
* @return the cache path
*/
//key经过md5之后生成一个字符串拼接在path后面生成完整路径
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path;
/**
* Get the default cache path for a certain key
*
* @param key the key (can be obtained from url using cacheKeyForURL)
*
* @return the default cache path
*/
//同上
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key;
@end
下面,我们来看看SDImageCache.m中的方法实现
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
//image存在,但是其对应的NSData不存在,则重新生成data
if (!data && image) {
//判断图片类型,然后将图片转成data(详见NSData+ImageContentType和UIImage+MultiFormat文件)
SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
data = [image sd_imageDataAsFormat:imageFormatFromData];
}
//存储到磁盘
[self storeImageDataToDisk:data forKey:key];
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
if (!imageData || !key) {
return;
}
[self checkIfQueueIsIOQueue];
/*
* 获取到需要存储的data后,使用fileManager进行存储
* 首先判断disk cache的文件路径是否存在,不存在的话就创建一个
* disk cache的文件路径是存储在_diskCachePath中的
*/
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
/*
* 根据image的key(一般情况下理解为image的url)组合成最终的文件路径
* 上面那个生成的文件路径只是一个文件目录,就跟/cache/images/img1.png和cache/images/的区别一样
*/
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
/*
* 这个url可不是网络端的url,而是file在系统路径下的url
* 比如/foo/bar/baz --------> file:///foo/bar/baz
*/
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
//根据存储的路径(cachePathForKey)和存储的数据(data)将其存放到iOS的文件系统
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
// disable iCloud backup(icould不备份)
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
/*
* 这两个变量主要是为了下面生成NSDirectoryEnumerator准备的
* 一个是记录遍历的文件目录,一个是记录遍历需要预先获取文件的哪些属性
*/
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// This enumerator prefetches useful properties for our cache files.
/*
* 递归地遍历diskCachePath这个文件夹中的所有目录,此处不是直接使用diskCachePath,而是使用其生成的NSURL
* 此处使用includingPropertiesForKeys:resourceKeys,这样每个file的resourceKeys对应的属性也会在遍历时预先获取到
* NSDirectoryEnumerationSkipsHiddenFiles表示不遍历隐藏文件
*/
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
/*
* 获取文件的过期时间,SDWebImage中默认是一个星期
* 不过这里虽然称*expirationDate为过期时间,但是实质上并不是这样
* 其实是这样的,比如在2016/12/12/00:00:00最后一次修改文件,对应的过期时间应该是
* 2016/12/19/00:00:00,不过现在时间是2016/12/27/00:00:00,我先将当前时间减去1个星期,得到
* 2016/12/20/00:00:00,这个时间才是我们函数中的expirationDate
* 用这个expirationDate和最后一次修改时间modificationDate比较看谁更晚就行
*/
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
//用来存储对应文件的一些属性,比如文件所需磁盘空间
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
//记录当前已经使用的磁盘缓存大小
NSUInteger currentCacheSize = 0;
// 在缓存的目录开始遍历文件. 此次遍历有两个目的:
// 1. 移除过期的文件
// 2. 同时存储每个文件的属性(比如该file是否是文件夹、该file所需磁盘大小,修改时间)
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// 当前扫描的是目录,就跳过
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 移除过期文件(这里判断过期的方式:对比文件的最后一次修改日期和expirationDate谁更晚,如果expirationDate更晚,就认为该文件已经过期,具体解释见上面)
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// 计算当前已经使用的cache大小,并将对应file的属性存到cacheFiles中
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
for (NSURL *fileURL in urlsToDelete) {
// 根据需要移除文件的url来移除对应file
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 如果我们当前cache的大小已经超过了允许配置的缓存大小,那就删除已经缓存的文件
// 删除策略就是,首先删除修改时间更早的缓存文件
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
// 直接将当前cache大小降到允许最大的cache大小的一般
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
// 根据文件修改时间来给所有缓存文件排序,按照修改时间越早越在前的规则排序
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// 每次删除file后,就计算此时的cache的大小.
//如果此时的cache大小已经降到期望的大小了,就停止删除文件了
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
// 获取该文件对应的属性
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
// 根据resourceValues获取该文件所需磁盘空间大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
// 计算当前cache大小
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
// 如果有completionBlock,就在主线程中调用
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
const char *str = key.UTF8String;
if (str == NULL) {
str = "";
}
/*
* 使用了MD5进行加密处理
* 开辟一个16字节(128位:md5加密出来就是128bit)的空间
*/
unsigned char r[CC_MD5_DIGEST_LENGTH];
/*
* 官方封装好的加密方法
* 把str字符串转换成了32位的16进制数列(这个过程不可逆转) 存储到了r这个空间中
*/
CC_MD5(str, (CC_LONG)strlen(str), r);
// 最终生成的文件名就是 "md5码"+".文件类型"
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;
}