SDWebImage主要使用SDImageCache來緩存圖片,實(shí)現(xiàn)了內(nèi)存存取和磁盤存取還有一系列的處理然爆。下面分析它的源碼只损。本文分析的版本為4.4.3。首先來看一下它對(duì)開發(fā)者暴露的接口拖云。
屬性
首先我們來看一下它的屬性
#pragma mark - Properties
//配置
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
//最大內(nèi)存大小
@property (assign, nonatomic) NSUInteger maxMemoryCost;
//最大內(nèi)存數(shù)量
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
這3個(gè)屬性都是可配置的屬性,其中maxMemoryCost和maxMemoryCountLimit用于配置其內(nèi)部的NSCache应又,config則負(fù)責(zé)大部分的配置,下面是它內(nèi)部的屬性宙项。
@interface SDImageCacheConfig : NSObject
//是否解壓縮圖片,默認(rèn)為YES
@property (assign, nonatomic) BOOL shouldDecompressImages;
//是否禁用iCloud備份株扛,默認(rèn)為YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;
//是否緩存一份到內(nèi)存中尤筐,默認(rèn)為YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
//是否額外存一份弱引用的緩存,默認(rèn)為YES
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;
//從磁盤讀取圖片的配置項(xiàng)洞就,默認(rèn)是NSDataReadingMappedIfSafe盆繁,也就是使用文件映射內(nèi)存的方式,是不消耗內(nèi)存的
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;
//寫文件的配置項(xiàng)旬蟋,默認(rèn)是NSDataWritingAtomic油昂,也就是會(huì)覆蓋原有的文件
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
//圖片在磁盤的最大時(shí)間,默認(rèn)是一周
@property (assign, nonatomic) NSInteger maxCacheAge;
//圖片在磁盤的最大大小,默認(rèn)是0冕碟,即沒有限制
@property (assign, nonatomic) NSUInteger maxCacheSize;
//清除磁盤緩存是基于什么清除稠腊,默認(rèn)是SDImageCacheConfigExpireTypeModificationDate,即基于圖片修改時(shí)間
@property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType;
@end
可以看出SDImageCacheConfig中大多數(shù)配置都是跟磁盤相關(guān)的鸣哀。
初始化方法
//單例
+ (nonnull instancetype)sharedImageCache;
//新建一個(gè)存儲(chǔ)類架忌,如果是用init方法創(chuàng)建,默認(rèn)傳入的是default
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;
//全能初始化方法我衬,比起上一個(gè)方法叹放,額外指定了存儲(chǔ)目錄,默認(rèn)目錄是在Cache/default的文件夾下
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;
其全能初始化方法的實(shí)現(xiàn)如下:
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory {
if ((self = [super init])) {
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
//創(chuàng)建專門讀寫磁盤的隊(duì)列挠羔,注意是并發(fā)
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
//初始化配置config
_config = [[SDImageCacheConfig alloc] init];
//初始化內(nèi)存空間
_memCache = [[SDMemoryCache alloc] initWithConfig:_config];
_memCache.name = fullNamespace;
//初始化存儲(chǔ)目錄
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
dispatch_sync(_ioQueue, ^{
self.fileManager = [NSFileManager new];
});
//注冊(cè)通知井仰,大意就是在程序進(jìn)后臺(tái)和退出的時(shí)候,清理一下磁盤
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
return self;
}
其中破加,makeDiskCachePath也是個(gè)暴露的方法:
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return [paths[0] stringByAppendingPathComponent:fullNamespace];
}
顯然文件的目錄基本都在Cache目錄下俱恶。
內(nèi)存存儲(chǔ)設(shè)計(jì)
這個(gè)類是使用SDMemoryCache來進(jìn)行存儲(chǔ),它繼承自NSCache范舀,而在這個(gè)類中合是,又持有了一個(gè)weakCache屬性弱引用著內(nèi)存,它實(shí)際上是在config中的shouldUseWeakMemoryCache置為YES才有效的锭环。
@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType>
@end
@interface SDMemoryCache <KeyType, ObjectType> ()
//配置
@property (nonatomic, strong, nonnull) SDImageCacheConfig *config;
//弱引用緩存
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache;
//信號(hào)量的鎖
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock;
- (instancetype)initWithConfig:(nonnull SDImageCacheConfig *)config;
@end
首先里面的NSMapTable相當(dāng)于一個(gè)字典,他的相關(guān)知識(shí)可以參看這篇文章聪全,總的來說,它可以設(shè)置鍵和值是賦值方式辅辩,當(dāng)設(shè)置鍵的賦值方式為Copy,值的賦值方式為Strong的時(shí)候难礼,它就相當(dāng)于NSMutableDictionary。
它自身也是一個(gè)NSCache玫锋,但與父類不一樣的是蛾茉,它多了一個(gè)收到內(nèi)存警告,刪除父類所有對(duì)象的功能撩鹿。
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
- (instancetype)initWithConfig:(SDImageCacheConfig *)config {
self = [super init];
if (self) {
//weakCache是一個(gè)鍵是強(qiáng)引用谦炬,值是弱引用的MapTable
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
self.weakCacheLock = dispatch_semaphore_create(1);
self.config = config;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveMemoryWarning:)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
return self;
}
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
//移除父類的對(duì)象
[super removeAllObjects];
}
可以看出,在收到內(nèi)存警告的時(shí)候三痰,僅僅清除了父類的對(duì)象吧寺,并沒有清除weakCache的對(duì)象窜管,因?yàn)槭侨跻妙愋蜕⒔伲膊挥檬謩?dòng)清除。
接下來就是一些操作內(nèi)存的時(shí)候?qū)eakCache的一些同步操作方法:
//setObject的相關(guān)方法都會(huì)調(diào)到這里來幕帆,因此只需重寫這個(gè)方法
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
[super setObject:obj forKey:key cost:g];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
if (key && obj) {
LOCK(self.weakCacheLock);
[self.weakCache setObject:obj forKey:key];
UNLOCK(self.weakCacheLock);
}
}
- (id)objectForKey:(id)key {
//先看看自身有沒有這個(gè)值
id obj = [super objectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return obj;
}
if (key && !obj) {
//從緩存找获搏,找到的話重新設(shè)置回去
LOCK(self.weakCacheLock);
obj = [self.weakCache objectForKey:key];
UNLOCK(self.weakCacheLock);
if (obj) {
NSUInteger cost = 0;
if ([obj isKindOfClass:[UIImage class]]) {
cost = SDCacheCostForImage(obj);
}
[super setObject:obj forKey:key cost:cost];
}
}
return obj;
}
//移除的時(shí)候,weakCache也需要同步移除
- (void)removeObjectForKey:(id)key {
[super removeObjectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
if (key) {
LOCK(self.weakCacheLock);
[self.weakCache removeObjectForKey:key];
UNLOCK(self.weakCacheLock);
}
}
- (void)removeAllObjects {
[super removeAllObjects];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
LOCK(self.weakCacheLock);
[self.weakCache removeAllObjects];
UNLOCK(self.weakCacheLock);
}
這一塊代碼容易讀懂,主要思想是在NSCache因?yàn)槟承┰蚯宄臅r(shí)候在內(nèi)存中仍然維持著一份弱引用常熙,只要這些弱引用的對(duì)象仍然被其他對(duì)象(比如UIImageView)所持有纬乍,那仍然會(huì)在該類中找到。
雖然這里引入了SDImageCacheConfig,但是實(shí)際上只使用了它的shouldUseWeakMemoryCache屬性裸卫,雖然代碼看上去并沒有直接設(shè)置shouldUseWeakMemoryCache這個(gè)成員屬性來得好仿贬,但是以后擴(kuò)展起來會(huì)容易一些。
存儲(chǔ)圖片方法
這個(gè)文件暴露了很多存圖片的API墓贿,大部分都會(huì)調(diào)到下面的方法中來:
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
//沒有圖片和存儲(chǔ)鍵直接返回
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
//內(nèi)存存一份
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
dispatch_async(self.ioQueue, ^{
//這里的data比較大茧泪,存到磁盤后需要及時(shí)釋放掉,不能讓其繼續(xù)占用內(nèi)存
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
//如果data為nil聋袋,則轉(zhuǎn)換為data存儲(chǔ)
SDImageFormat format;
if (SDCGImageRefContainsAlpha(image.CGImage)) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
}
//磁盤存一份队伟,這個(gè)方法會(huì)阻塞線程
[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;
}
if (![self.fileManager fileExistsAtPath:_diskCachePath]) {
[self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
//返回MD5后的字符串,這一步也是耗時(shí)的
NSString *cachePathForKey = [self defaultCachePathForKey:key];
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
//寫文件
[imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
if (self.config.shouldDisableiCloud) {
//不讓該文件被iCloud備份
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
所有關(guān)于磁盤的耗時(shí)操作都放在ioQueue里操作幽勒,這樣保證了主線程的正常運(yùn)行嗜侮。
獲取圖片方法
讀取圖片的方法有很多,其中先從最常用的方法講起:
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key {
//先從緩存讀
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
return image;
}
//緩存找不到啥容,就從磁盤找
image = [self imageFromDiskCacheForKey:key];
return image;
}
其中锈颗,imageFromMemoryCacheForKey這個(gè)方法比較簡單,無非就是從memCache中讀取而已咪惠,這里就不貼代碼了宜猜。
imageFromDiskCacheForKey這個(gè)方法的實(shí)現(xiàn)如下:
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.config.shouldCacheImagesInMemory) {
//重新將圖片放進(jìn)內(nèi)存
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
return diskImage;
}
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
//先讀取出數(shù)據(jù)
NSData *data = [self diskImageDataForKey:key];
//再將數(shù)據(jù)轉(zhuǎn)成圖片
return [self diskImageForKey:key data:data];
}
從磁盤中讀取圖片主要分成2個(gè)步驟,一是從磁盤中讀取出數(shù)據(jù)硝逢,二是將數(shù)據(jù)轉(zhuǎn)化為圖片姨拥。
- (nullable NSData *)diskImageDataForKey:(nullable NSString *)key {
if (!key) {
return nil;
}
__block NSData *imageData = nil;
//在ioQueue,阻塞當(dāng)前線程
dispatch_sync(self.ioQueue, ^{
imageData = [self diskImageDataBySearchingAllPathsForKey:key];
});
return imageData;
}
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
//MD5字符串
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil];
if (data) {
return data;
}
//現(xiàn)在默認(rèn)路徑上找
data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
if (data) {
return data;
}
//如果默認(rèn)路徑?jīng)]找到,則在其他路徑找渠鸽,這些路徑可由開發(fā)者配置
NSArray<NSString *> *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
if (imageData) {
return imageData;
}
imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
if (imageData) {
return imageData;
}
}
return nil;
}
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data {
return [self diskImageForKey:key data:data options:0];
}
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options {
if (data) {
//圖片解碼
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
//這里主要是進(jìn)行圖片放大叫乌、動(dòng)圖的操作
image = [self scaledImageForKey:key image:image];
if (self.config.shouldDecompressImages) {
BOOL shouldScaleDown = options & SDImageCacheScaleDownLargeImages;
//解壓圖片
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
}
return image;
} else {
return nil;
}
}
除了同步獲取圖片的方法,該類還提供了異步獲取圖片的方法,其原理基本是一樣的,這里僅貼出接口:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock徽缚;
他返回了一個(gè)NSOperation憨奸,開發(fā)者可以通過這個(gè)Operation中斷查找。
刪除圖片方法
刪除圖片的實(shí)現(xiàn)如下:
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion {
[self removeImageForKey:key fromDisk:YES withCompletion:completion];
}
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
if (key == nil) {
return;
}
//從緩存刪除
if (self.config.shouldCacheImagesInMemory) {
[self.memCache removeObjectForKey:key];
}
if (fromDisk) {
//在ioQueue中從磁盤中刪除
dispatch_async(self.ioQueue, ^{
[self.fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
} else if (completion){
completion();
}
}
可以看出凿试,刪除圖片的操作還是比較簡單的排宰。
此外,還有清除內(nèi)存那婉、清除磁盤的方法板甘,代碼也比較簡單,這里就不貼出來了详炬。
刪除磁盤舊圖片功能實(shí)現(xiàn)
SDImageCache還可以定期刪除磁盤中的圖片盐类,其實(shí)現(xiàn)方式是在程序進(jìn)入后臺(tái)或者程序結(jié)束時(shí),調(diào)用下面這個(gè)方法:
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
//選擇是根據(jù)修改時(shí)間還是根據(jù)創(chuàng)建時(shí)間清除老文件
NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
switch (self.config.diskCacheExpireType) {
case SDImageCacheConfigExpireTypeAccessDate:
cacheContentDateKey = NSURLContentAccessDateKey;
break;
case SDImageCacheConfigExpireTypeModificationDate:
cacheContentDateKey = NSURLContentModificationDateKey;
break;
default:
break;
}
//清除文件,只需要知道文件是否是文件夾在跳、時(shí)間以及占用大小3個(gè)信息
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
//獲取文件的迭代
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
//得到過期的時(shí)間
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
//排除錯(cuò)誤的文件以及文件夾
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
//如果文件過期枪萄,則添加到待刪除的數(shù)組中
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
//如果沒有過期,則計(jì)算其占用大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
for (NSURL *fileURL in urlsToDelete) {
[self.fileManager removeItemAtURL:fileURL error:nil];
}
//如果這個(gè)時(shí)候總大小仍比配置的大小要大猫妙,則按照時(shí)間刪除文件瓷翻,知道文件總大小小于配置大小的一半
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) { return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
}];
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
總結(jié)
總的來說,緩存的邏輯主要復(fù)雜在磁盤的讀寫上割坠,所有的磁盤操作都放在io線程上讀取逻悠。此外,在內(nèi)存上使用NSCache+NSMapTable而不是NSDictionary存儲(chǔ)圖片韭脊,也值得我們借鑒童谒。