深入源碼理解YYCache 谣沸、SDWebImage、AFNetworking笋颤、NSCache 緩存方式與對比
轉(zhuǎn)載請注明出處 http://www.reibang.com/p/18d9fe85266d
在之前的一篇文章iOS緩存 NSCache詳解及SDWebImage緩存策略源碼分析中詳細(xì)講解了NSCache
的用法以及SDWebImage
內(nèi)存和磁盤緩存的源碼分析乳附,本篇文章將簡要講解AFNetworking
緩存類和YYCache
并作出對比。
由于之前的一篇文章已經(jīng)詳細(xì)講解了NSCache
和SDWebImage
緩存策略伴澄,本篇文章不再贅述赋除,會(huì)簡要介紹一下AFNetworking
和YYCache
的源碼。
AFNetworking圖片緩存AFAutoPurgingImageCache
AFNetworking
也提供了同SDWebImage
一樣的下載圖片的功能非凌,也提供了緩存這些圖片的功能贤重,但它只提供了內(nèi)存緩存,沒有提供磁盤緩存功能清焕。
看一下頭文件:
//緩存協(xié)議并蝗,如果用戶需要實(shí)現(xiàn)自定義的
@protocol AFImageCache <NSObject>
//添加圖片并傳遞一個(gè)唯一id,一般使用圖片的URL
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;
//刪除id為identifier的圖片
- (BOOL)removeImageWithIdentifier:(NSString *)identifier;
//刪除所有緩存圖片
- (BOOL)removeAllImages;
//根據(jù)id獲取圖片
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;
@end
//AFImageRequestCache協(xié)議秸妥,繼承AFImageCache協(xié)議
@protocol AFImageRequestCache <AFImageCache>
//添加圖片滚停,傳入下載圖片的request和額外的id
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
//刪除圖片,傳入下載圖片的request和額外的id
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
//根據(jù)下載圖片的request和額外的id獲取圖片
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
@end
//AFNetworking提供的緩存類粥惧,
@interface AFAutoPurgingImageCache : NSObject <AFImageRequestCache>
//單位是字節(jié)键畴,能夠支持最大緩存大小
@property (nonatomic, assign) UInt64 memoryCapacity;
//建議的當(dāng)內(nèi)存緩存要釋放的時(shí)候需要釋放到多大的大小
@property (nonatomic, assign) UInt64 preferredMemoryUsageAfterPurge;
//當(dāng)前內(nèi)存緩存占用的字節(jié)大小
@property (nonatomic, assign, readonly) UInt64 memoryUsage;
//構(gòu)造函數(shù)
- (instancetype)init;
//構(gòu)造函數(shù)
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity;
@end
從頭文件可以看出,AFNetworking
提供的AFAutoPurgingImageCache
接口不多突雪,而且都很簡單起惕,只實(shí)現(xiàn)了內(nèi)存緩存的功能。
看一下實(shí)現(xiàn)文件:
//緩存對象包裝類
@interface AFCachedImage : NSObject
//緩存的圖片
@property (nonatomic, strong) UIImage *image;
//id
@property (nonatomic, strong) NSString *identifier;
//圖片字節(jié)大小
@property (nonatomic, assign) UInt64 totalBytes;
//淘汰算法是LRU所以需要記錄上次訪問時(shí)間
@property (nonatomic, strong) NSDate *lastAccessDate;
//沒用到的屬性
@property (nonatomic, assign) UInt64 currentMemoryUsage;
@end
@implementation AFCachedImage
//初始化構(gòu)函數(shù)
-(instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier {
if (self = [self init]) {
self.image = image;
self.identifier = identifier;
//計(jì)算圖片的字節(jié)大小咏删,每個(gè)像素占4字節(jié)32位
CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
CGFloat bytesPerPixel = 4.0;
CGFloat bytesPerSize = imageSize.width * imageSize.height;
self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerSize;
self.lastAccessDate = [NSDate date];
}
return self;
}
//通過緩存對象獲取圖片時(shí)要更新上次訪問時(shí)間為當(dāng)前時(shí)間
- (UIImage*)accessImage {
//直接使用NSDate
self.lastAccessDate = [NSDate date];
return self.image;
}
- (NSString *)description {
NSString *descriptionString = [NSString stringWithFormat:@"Idenfitier: %@ lastAccessDate: %@ ", self.identifier, self.lastAccessDate];
return descriptionString;
}
@end
//AFNetworking緩存類的豬腳
@interface AFAutoPurgingImageCache ()
//可變字典用于存儲所有的緩存對象AFCachedImage對象惹想,key為字符串類型
@property (nonatomic, strong) NSMutableDictionary <NSString* , AFCachedImage*> *cachedImages;
//當(dāng)前緩存對象內(nèi)存占用大小
@property (nonatomic, assign) UInt64 currentMemoryUsage;
//用于線程安全防止產(chǎn)生競爭條件,沒有用鎖
@property (nonatomic, strong) dispatch_queue_t synchronizationQueue;
@end
@implementation AFAutoPurgingImageCache
//構(gòu)造函數(shù)督函,默認(rèn)內(nèi)存占用100M嘀粱,每次清除緩存到60M
- (instancetype)init {
return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}
//構(gòu)造函數(shù)
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity {
if (self = [super init]) {
self.memoryCapacity = memoryCapacity;
self.preferredMemoryUsageAfterPurge = preferredMemoryCapacity;
self.cachedImages = [[NSMutableDictionary alloc] init];
//創(chuàng)建一個(gè)并行隊(duì)列,但后面使用時(shí)都是在同步情況或barrier情況下辰狡,隊(duì)列中的任務(wù)還是以串行執(zhí)行
//可以防止產(chǎn)生競爭條件锋叨,保證線程安全
NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
//添加通知,監(jiān)聽收到系統(tǒng)的內(nèi)存警告后刪除所有緩存對象
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(removeAllImages)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
return self;
}
//析構(gòu)函數(shù)宛篇,刪除通知的監(jiān)聽
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
//memoryUsage的getter
- (UInt64)memoryUsage {
__block UInt64 result = 0;
dispatch_sync(self.synchronizationQueue, ^{
result = self.currentMemoryUsage;
});
return result;
}
//添加圖片到緩存中
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
//使用dispatch_barrier_async不阻塞當(dāng)前線程娃磺,不阻塞向隊(duì)列中添加任務(wù)
//但隊(duì)列中其他任務(wù)要執(zhí)行就必須等待前一個(gè)任務(wù)結(jié)束,不管是不是并發(fā)隊(duì)列
dispatch_barrier_async(self.synchronizationQueue, ^{
//創(chuàng)建AFCachedImage對象
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
//判斷對應(yīng)id是否已經(jīng)保存在緩存字典中了
AFCachedImage *previousCachedImage = self.cachedImages[identifier];
//如果已經(jīng)保存了減去占用的內(nèi)存大小
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}
//更新字典叫倍,更新占用內(nèi)存大小
self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});
//同上偷卧,該block必須等待前一個(gè)block執(zhí)行完成才可以執(zhí)行
dispatch_barrier_async(self.synchronizationQueue, ^{
//判斷當(dāng)前占用內(nèi)存大小是否超過了設(shè)置的內(nèi)存緩存總大小
if (self.currentMemoryUsage > self.memoryCapacity) {
//計(jì)算需要釋放多少空間
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
//把緩存字典中的所有緩存對象取出
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
//設(shè)置排序描述器豺瘤,按照上次訪問時(shí)間排序
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
//排序取出的所有緩存對象
[sortedImages sortUsingDescriptors:@[sortDescriptor]];
UInt64 bytesPurged = 0;
//遍歷,釋放緩存對象涯冠,滿足要求后break
for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
//更新當(dāng)前占用緩存大小
self.currentMemoryUsage -= bytesPurged;
}
});
}
//刪除圖片
- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
__block BOOL removed = NO;
//同步方法
dispatch_barrier_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
if (cachedImage != nil) {
[self.cachedImages removeObjectForKey:identifier];
self.currentMemoryUsage -= cachedImage.totalBytes;
removed = YES;
}
});
return removed;
}
//刪除所有圖片
- (BOOL)removeAllImages {
__block BOOL removed = NO;
//同步方法
dispatch_barrier_sync(self.synchronizationQueue, ^{
if (self.cachedImages.count > 0) {
[self.cachedImages removeAllObjects];
self.currentMemoryUsage = 0;
removed = YES;
}
});
return removed;
}
//根據(jù)id獲取圖片
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
__block UIImage *image = nil;
dispatch_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
//更新訪問時(shí)間
image = [cachedImage accessImage];
});
return image;
}
//AFImageRequestCache協(xié)議的方法炉奴,通過request構(gòu)造一個(gè)key然后調(diào)用前面的方法
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
[self addImage:image withIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
//同上
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
return [self removeImageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
//同上
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
return [self imageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
//通過request和額外的id構(gòu)造一個(gè)key
- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
//取圖片的URI然后追加額外的id構(gòu)造key
NSString *key = request.URL.absoluteString;
if (additionalIdentifier != nil) {
key = [key stringByAppendingString:additionalIdentifier];
}
return key;
}
@end
AFAutoPurgingImageCache
的實(shí)現(xiàn)很簡單,邏輯也都很簡單蛇更,不再贅述了瞻赶。它的淘汰算法采用的是LRU
,從源碼中其實(shí)也可以看出缺點(diǎn)挺多的派任,比如上次訪問時(shí)間使用NSDate
類砸逊,使用UNIX時(shí)間戳
應(yīng)該更好,不僅內(nèi)存占用小排序也更快吧掌逛。淘汰緩存時(shí)需要從緩存字典中取出所有的緩存對象然后根據(jù)NSDate
排序师逸,如果有大量緩存圖片,這里似乎就是一個(gè)性能瓶頸豆混,但它的優(yōu)點(diǎn)就是實(shí)現(xiàn)簡單明了篓像,對于性能要求不高的程序選擇這個(gè)也沒有太多影響。
YYCache的內(nèi)存緩存和磁盤緩存
本節(jié)文章將講解YYCache
的內(nèi)存緩存YYMemoryCache
和磁盤緩存YYDiskCache
皿伺,但源碼較多员辩,而且本文的篇幅有限,所以不再和之前的文章一樣貼所有的源碼來講解鸵鸥,這里只會(huì)貼一些比較重要的代碼進(jìn)行講解奠滑,需要深入研究的讀者還是要自己看一下完整的源碼。
YYMemoryCache講解
貼一下頭文件的接口代碼:
@interface YYMemoryCache : NSObject
//內(nèi)存
@property (nullable, copy) NSString *name;
//當(dāng)前緩存對象的個(gè)數(shù)
@property (readonly) NSUInteger totalCount;
//當(dāng)前緩存對象的總cost數(shù)
@property (readonly) NSUInteger totalCost;
//同NSCache妒穴,支持緩存多少個(gè)對象
@property NSUInteger countLimit;
//同NSCache宋税,每個(gè)緩存對象可以設(shè)置一個(gè)cost,這個(gè)值就標(biāo)識支持緩存的最大cost總量
@property NSUInteger costLimit;
//緩存的過期時(shí)間讼油,單位是秒
@property NSTimeInterval ageLimit;
//自動(dòng)清理緩存的時(shí)間間隔杰赛,默認(rèn)是5秒
@property NSTimeInterval autoTrimInterval;
//是否在程序進(jìn)入后臺后刪除所有的緩存對象
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
//收到系統(tǒng)內(nèi)存警告后執(zhí)行的回調(diào)塊
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);
//程序進(jìn)入后臺后執(zhí)行的回調(diào)塊
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);
//是否在主線程釋放對象,如果有UIView這樣的UI對象需要在主線程中釋放
@property BOOL releaseOnMainThread;
//是否異步釋放汁讼,默認(rèn)是YES
@property BOOL releaseAsynchronously;
//key的緩存對象是否存在
- (BOOL)containsObjectForKey:(id)key;
//根據(jù)key獲取緩存對象
- (nullable id)objectForKey:(id)key;
//設(shè)置key的緩存對象
- (void)setObject:(nullable id)object forKey:(id)key;
//設(shè)置key的緩存對象并設(shè)置其cost值
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
//刪除key的緩存對象
- (void)removeObjectForKey:(id)key;
//刪除所有緩存對象
- (void)removeAllObjects;
//清除緩存到緩存對象只有count個(gè)
- (void)trimToCount:(NSUInteger)count;
//清除緩存到cost上限為cost
- (void)trimToCost:(NSUInteger)cost;
//清除超過age的過期緩存對象
- (void)trimToAge:(NSTimeInterval)age;
@end
YYMemeoryCache
提供的接口和NSCache
類似淆攻,提供緩存對象個(gè)數(shù)限制、緩存對象占用cost限制和緩存對象過期限制嘿架,實(shí)現(xiàn)緩存對象的清理。YYMemeoryCache
與AFAutoPurgingImageCache
不同啸箫,他使用鏈表來管理緩存對象耸彪,同樣也使用LRU淘汰算法
來清除緩存對象。
首先看一下鏈表節(jié)點(diǎn)定義:
//這是一個(gè)雙向鏈表忘苛,保存prev和next指針
@interface _YYLinkedMapNode : NSObject {
@package
//__unsafe_unretained標(biāo)識不保留其他節(jié)點(diǎn)對象蝉娜,提高效率
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
//記錄key唱较、value、cost和訪問時(shí)間召川,使用unix時(shí)間戳
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@end
@implementation _YYLinkedMapNode
@end
上面的代碼就是雙向鏈表的節(jié)點(diǎn)南缓,其實(shí)和AFAutoPurgingImageCache
封裝的緩存對象一樣,不過YYMemeoryCache
支持任意類型的緩存對象而不限于是圖片荧呐,YYMemeoryCache
性能和效率非常高汉形,通過上面的定義也可以看出,能夠提高效率的地方一定會(huì)用最好的方式實(shí)現(xiàn)倍阐,由于是YYMemeoryCache
管理緩存對象的節(jié)點(diǎn)概疆,所以能夠保證在生命周期內(nèi)不會(huì)被釋放,所以使用__unsafe_unretained
修飾峰搪,不會(huì)產(chǎn)生任何問題岔冀。
再看一下鏈表的定義:
@interface _YYLinkedMap : NSObject {
@package
//使用Core Foundation的CFMutableDictionaryRef可變字典保存上面的節(jié)點(diǎn)對象
CFMutableDictionaryRef _dic; // do not set object directly
//記錄總cost
NSUInteger _totalCost;
//記錄總count
NSUInteger _totalCount;
//記錄雙向鏈表的head指針
_YYLinkedMapNode *_head; // MRU, do not change it directly
//雙向鏈表的tail指針
_YYLinkedMapNode *_tail; // LRU, do not change it directly
//是否在主線程中釋放緩存對象
BOOL _releaseOnMainThread;
//是否異步釋放緩存對象
BOOL _releaseAsynchronously;
}
//雙向鏈表在表頭插入節(jié)點(diǎn)
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
//將節(jié)點(diǎn)移到表頭,LRU算法概耻,訪問一個(gè)緩存對象后就將這個(gè)對象移動(dòng)到表頭
//不需要向AFNetworking一樣記錄NSDate然后更新排序
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
//刪除一個(gè)節(jié)點(diǎn)
- (void)removeNode:(_YYLinkedMapNode *)node;
//刪除尾節(jié)點(diǎn)
- (_YYLinkedMapNode *)removeTailNode;
//刪除所有的節(jié)點(diǎn)
- (void)removeAll;
@end
上面是鏈表的定義使套,它使用Core Foundation
的CFMutableDictionaryRef
來保存節(jié)點(diǎn)對象,并維護(hù)一個(gè)雙向鏈表鞠柄,記錄了一些需要使用的參數(shù)侦高。也就是說YYMemeoryCache
會(huì)通過構(gòu)造一個(gè)_YYLinkedMapNode
對象來封裝需要緩存的對象,然后使用_YYLinkedMap
對象來維護(hù)和管理這個(gè)雙向鏈表春锋,由于篇幅問題不列舉所有的實(shí)現(xiàn)方法了矫膨,這些方法都是常規(guī)的鏈表操作,讀者可自行閱讀期奔,舉一個(gè)源碼:
//將一個(gè)節(jié)點(diǎn)移動(dòng)到表頭侧馅,由于使用LRU淘汰算法
//所以當(dāng)我們訪問一個(gè)緩存對象時(shí)就會(huì)調(diào)用這個(gè)方法將封裝緩存對象的節(jié)點(diǎn)移動(dòng)到表頭
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
//判斷節(jié)點(diǎn)是頭節(jié)點(diǎn)、尾節(jié)點(diǎn)或普通節(jié)點(diǎn)呐萌,然后修改指針指向
if (_head == node) return;
if (_tail == node) {
_tail = node->_prev;
_tail->_next = nil;
} else {
node->_next->_prev = node->_prev;
node->_prev->_next = node->_next;
}
node->_next = _head;
node->_prev = nil;
_head->_prev = node;
_head = node;
}
其他方法就不再贅述了馁痴,講到這里再結(jié)合之前AFNetworking
的緩存源碼,相信大家也可以自己寫出添加緩存肺孤、刪除緩存的代碼了罗晕。接下來看一下YYMemeoryCache
如何添加緩存對象的:
//添加緩存對象
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
//key為空直接返回
if (!key) return;
//要緩存的對象object不存在就刪除對應(yīng)key的已經(jīng)緩存的對象
if (!object) {
[self removeObjectForKey:key];
return;
}
//使用pthread_mutext互斥鎖,防止產(chǎn)生競爭條件保障線程安全
pthread_mutex_lock(&_lock);
//首先從存儲緩存對象的字典中獲取緩存節(jié)點(diǎn)
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
//獲取一個(gè)時(shí)間
NSTimeInterval now = CACurrentMediaTime();
//如果從緩存字典中找到了節(jié)點(diǎn)
if (node) {
//更新節(jié)點(diǎn)的信息
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
node->_cost = cost;
node->_time = now;
node->_value = object;
//將節(jié)點(diǎn)移動(dòng)至表頭
[_lru bringNodeToHead:node];
} else {
//如果節(jié)點(diǎn)不存在赠堵,就構(gòu)造一個(gè)
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
//將新創(chuàng)建的節(jié)點(diǎn)插在表頭
[_lru insertNodeAtHead:node];
}
//如果鏈表的總cost值大于限制值就清理
if (_lru->_totalCost > _costLimit) {
//異步在串行隊(duì)列中執(zhí)行清除操作
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
/*
如果鏈表緩存節(jié)點(diǎn)總數(shù)大于限制值就清理一個(gè)節(jié)點(diǎn)
由于每次添加緩存對象的時(shí)候都會(huì)判斷是否超出總數(shù)小渊,所以只需要?jiǎng)h除一個(gè)節(jié)點(diǎn)就能保持總數(shù)小于等于限制值
*/
if (_lru->_totalCount > _countLimit) {
//獲取要?jiǎng)h除的尾節(jié)點(diǎn)
_YYLinkedMapNode *node = [_lru removeTailNode];
//根據(jù)配置值獲取隊(duì)列
if (_lru->_releaseAsynchronously) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
/*
這里是一個(gè)小技巧,block捕獲并持有node
由于是異步方法茫叭,當(dāng)前代碼塊會(huì)立刻結(jié)束酬屉,所以局部變量node會(huì)被釋放,緩存節(jié)點(diǎn)的引用計(jì)數(shù)只有當(dāng)前block有
此時(shí)block執(zhí)行完成以后就會(huì)由這個(gè)線程負(fù)責(zé)釋放節(jié)點(diǎn)的內(nèi)存,就實(shí)現(xiàn)了其他線程異步釋放內(nèi)存的操作
*/
[node class]; //hold and release in queue
});
} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
[node class]; //hold and release in queue
});
}
}
//釋放鎖
pthread_mutex_unlock(&_lock);
}
YYMemoryCache
對于線程安全沒有使用GCD串行隊(duì)列來實(shí)現(xiàn)呐萨,在第一個(gè)版本中使用的是性能最好的自旋鎖杀饵,由于自旋鎖存在一些bug,所以后來的版本改成了互斥鎖谬擦,pthread_mutext
上鎖和釋放鎖的效率也很高切距,具體可以參考YYCache
作者寫的文章不再安全的 OSSpinLock,使用GCD隊(duì)列
來實(shí)現(xiàn)線程安全防止產(chǎn)生競爭條件很簡單惨远,但是效率沒有互斥鎖高谜悟,所以作者選擇了使用互斥鎖。
上面的代碼是向YYMemeoryCache
中添加緩存對象锨络,代碼也很簡單赌躺,就是簡單的鏈表操作,比較值得注意的就是在其他線程異步釋放對象羡儿,有可能我們會(huì)覺得釋放對象并不需要消耗太多的資源礼患,但是累積起來也會(huì)產(chǎn)生一定的消耗了。
在看一下根據(jù)key
獲取緩存對象的方法:
- (id)objectForKey:(id)key {
if (!key) return nil;
//加互斥鎖
pthread_mutex_lock(&_lock);
//從字典中獲取key對應(yīng)的緩存對象節(jié)點(diǎn)
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
//如果存在就更新訪問時(shí)間并將節(jié)點(diǎn)移至表頭
if (node) {
node->_time = CACurrentMediaTime();
[_lru bringNodeToHead:node];
}
pthread_mutex_unlock(&_lock);
return node ? node->_value : nil;
}
獲取緩存對象也很簡單掠归,接下來看一下根據(jù)限制清除緩存的操作:
/*
遞歸函數(shù)缅叠,一種實(shí)現(xiàn)定時(shí)器的技巧
使用GCD 每_autoTrimInterval秒(默認(rèn)5s)執(zhí)行一次_trimInBackground方法
遞歸調(diào)用就可以實(shí)現(xiàn)每_autoTrimInterval秒進(jìn)行一次緩存對象的清除操作
*/
- (void)_trimRecursively {
__weak typeof(self) _self = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
__strong typeof(_self) self = _self;
if (!self) return;
[self _trimInBackground];
[self _trimRecursively];
});
}
//調(diào)用三種清除策略
- (void)_trimInBackground {
dispatch_async(_queue, ^{
[self _trimToCost:self->_costLimit];
[self _trimToCount:self->_countLimit];
[self _trimToAge:self->_ageLimit];
});
}
//只看一個(gè)栗子
- (void)_trimToCost:(NSUInteger)costLimit {
BOOL finish = NO;
//加鎖,并判斷是否已經(jīng)滿足限制的要求
pthread_mutex_lock(&_lock);
if (costLimit == 0) {
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCost <= costLimit) {
finish = YES;
}
//釋放所
pthread_mutex_unlock(&_lock);
//如果已經(jīng)滿足要求就直接返回虏冻,不需要做清除操作
if (finish) return;
//定義一個(gè)數(shù)組用于存儲要?jiǎng)h除的緩存節(jié)點(diǎn)對象
NSMutableArray *holder = [NSMutableArray new];
//循環(huán)
while (!finish) {
//trylock會(huì)嘗試上鎖肤粱,上鎖成功返回0,如果已經(jīng)被占用返回一個(gè)非零值
if (pthread_mutex_trylock(&_lock) == 0) {
//判斷是否滿足限制要求
if (_lru->_totalCost > costLimit) {
//刪除尾節(jié)點(diǎn)厨相,加入到要?jiǎng)h除的數(shù)組中
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
//如果沒有上鎖成功就睡10ms重試
usleep(10 * 1000); //10 ms
}
}
//如果要?jiǎng)h除的數(shù)組個(gè)數(shù)大于0领曼,就根據(jù)配置異步或主線程中釋放對象
//這里的釋放技巧和之前講解的一致
if (holder.count) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[holder count]; // release in queue
});
}
}
上面的代碼也很簡單,使用GCD
和遞歸函數(shù)實(shí)現(xiàn)了一個(gè)簡單的計(jì)時(shí)器蛮穿,每5ms執(zhí)行一次清理操作庶骄,代碼很簡單不再贅述了。
到現(xiàn)在為止践磅,我們已經(jīng)熟悉了AFNetworking
內(nèi)存緩存单刁、SDWebImage
內(nèi)存緩存和YYCache
的內(nèi)存緩存的實(shí)現(xiàn),SDWebImage
內(nèi)存緩存其實(shí)就直接使用了NSCache
府适。
基于內(nèi)存的緩存可以使用NSCache
和NSMutableDictionary
來實(shí)現(xiàn)羔飞,但使用NSCache
其清除緩存的算法不是我們可控的,比如我們想要LRU
淘汰算法檐春,或者FILO
逻淌、FIFO
等各種算法都沒辦法實(shí)現(xiàn),此時(shí)只能自己實(shí)現(xiàn)疟暖,并且NSCache
緩存的讀寫效率并不高恍风,他幫我們做的只有自動(dòng)清理緩存,所以在性能要求不高的情況下使用NSCache
很合適誓篱,其實(shí)現(xiàn)簡單朋贬,已經(jīng)幫我們完成了所有的工作,我們只需要像操作字典一樣操作他窜骄。
如果要實(shí)現(xiàn)自定義的淘汰算法锦募,就需要自定義實(shí)現(xiàn),如AFNetworking
使用NSMutableDictionary
實(shí)現(xiàn)LRU淘汰算法
邻遏,其實(shí)通過對比YYCache
和AFNetworkign
不難發(fā)現(xiàn)糠亩,AFAutoPurgingImageCache
的性能瓶頸有很多,值得提升的地方也有很多准验,YYMemeoryCache
為了追求極致的性能很多地方都是直接使用C函數(shù)赎线,直接使用Core Foundation
的CFMutableDictionaryRef
可變字典,包括釋放對象這樣不怎么消耗資源的操作都盡量放在其他線程執(zhí)行糊饱。所以垂寥,對于高性能需求的場景就需要我們深思實(shí)現(xiàn)方式。
YYDiskCache
YYCache
的磁盤緩存YYDiskCache
的實(shí)現(xiàn)相比就復(fù)雜一些了另锋,作者在經(jīng)過大量調(diào)研和實(shí)驗(yàn)后發(fā)現(xiàn)滞项,SQLite
對于數(shù)據(jù)的寫入性能高于直接寫文件,但是對于讀性能來說需要考慮數(shù)據(jù)的大小夭坪,對于20KB以上的數(shù)據(jù)讀文件的性能要高于讀數(shù)據(jù)庫的性能文判,所以,為了實(shí)現(xiàn)高性能的磁盤緩存室梅,作者結(jié)合了SQLite
和文件系統(tǒng)戏仓,將緩存數(shù)據(jù)的元數(shù)據(jù)保存在數(shù)據(jù)庫中,對于大于20KB的數(shù)據(jù)存入文件系統(tǒng)中亡鼠,讀取時(shí)直接從文件系統(tǒng)中讀取赏殃。到這里,需求基本都說完了拆宛,如果讓讀者自行實(shí)現(xiàn)相信你也能寫出一個(gè)磁盤緩存嗓奢。
因?yàn)榻Y(jié)合了SQLite
和文件系統(tǒng),作者實(shí)現(xiàn)了YYKVStorage
類封裝了數(shù)據(jù)庫和文件的操作浑厚,YYDiskCache
類似于表現(xiàn)層股耽,進(jìn)一步封裝并提供便于使用的接口,YYKVStorage
不是線程安全的钳幅,所以保證線程安全的操作由YYDiskCache
實(shí)現(xiàn)物蝙。
看一下YYKVStorage
的頭文件聲明代碼中比較重要的部分:
//緩存對象model
@interface YYKVStorageItem : NSObject
//唯一id key
@property (nonatomic, strong) NSString *key; ///< key
//value
@property (nonatomic, strong) NSData *value; ///< value
//保存在文件系統(tǒng)中的文件名,如果只保存在數(shù)據(jù)庫中則為nil
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
//value的字節(jié)大小
@property (nonatomic) int size; ///< value's size in bytes
//修改時(shí)間敢艰,unix時(shí)間戳
@property (nonatomic) int modTime; ///< modification unix timestamp
//訪問時(shí)間诬乞,unix時(shí)間戳
@property (nonatomic) int accessTime; ///< last access unix timestamp
//附加數(shù)據(jù),如果沒有為nil
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end
//存儲方式類型,key-value緩存對象的存儲位置
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
//只存在文件系統(tǒng)里
YYKVStorageTypeFile = 0,
//只存在數(shù)據(jù)庫里
YYKVStorageTypeSQLite = 1,
//文件系統(tǒng)和數(shù)據(jù)庫都存
YYKVStorageTypeMixed = 2,
};
上面定義了一個(gè)封裝的緩存對象類YYKVStorageItem
震嫉,還定義了三種緩存對象存儲方式森瘪,可以只使用文件系統(tǒng)、只使用數(shù)據(jù)庫或在兩者中都存儲票堵,用戶可以按需選擇扼睬,這個(gè)存儲類型的設(shè)置方式是在初始化構(gòu)造函數(shù)中指定的。
在看一下文件系統(tǒng)的目錄結(jié)構(gòu)和數(shù)據(jù)庫表結(jié)構(gòu):
File:
/path/
/manifest.sqlite
/manifest.sqlite-shm
/manifest.sqlite-wal
/data/
/e10adc3949ba59abbe56e057f20f883e
/e10adc3949ba59abbe56e057f20f883e
/trash/
/unused_file_or_folder
SQL:
create table if not exists manifest (
key text,
filename text,
size integer,
inline_data blob,
modification_time integer,
last_access_time integer,
extended_data blob,
primary key(key)
);
create index if not exists last_access_time_idx on manifest(last_access_time);
類似于SDWebImage
的磁盤緩存悴势,文件名稱使用MD5散列key獲得窗宇,YYKVStorage
類主要實(shí)現(xiàn)了文件和數(shù)據(jù)庫的增刪改查操作,由于篇幅問題這里就分別舉一個(gè)栗子:
//SQLite插入一條數(shù)據(jù)
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return NO;
int timestamp = (int)time(NULL);
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
sqlite3_bind_int(stmt, 3, (int)value.length);
if (fileName.length == 0) {
sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
} else {
sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
}
sqlite3_bind_int(stmt, 5, timestamp);
sqlite3_bind_int(stmt, 6, timestamp);
sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}
//向文件中寫數(shù)據(jù)
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
if (key.length == 0 || value.length == 0) return NO;
if (_type == YYKVStorageTypeFile && filename.length == 0) {
return NO;
}
if (filename.length) {
//將value數(shù)據(jù)寫入文件系統(tǒng)中
if (![self _fileWriteWithName:filename data:value]) {
return NO;
}
//將數(shù)據(jù)插入到數(shù)據(jù)庫中
if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
[self _fileDeleteWithName:filename];
return NO;
}
return YES;
} else {
if (_type != YYKVStorageTypeSQLite) {
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
//通過key獲取緩存對象
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
if (key.length == 0) return nil;
//首先查找數(shù)據(jù)庫特纤,不獲取
YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
if (item) {
//更新訪問
[self _dbUpdateAccessTimeWithKey:key];
//如果文件路徑存在军俊,就去讀文件系統(tǒng)
if (item.filename) {
item.value = [self _fileReadWithName:item.filename];
if (!item.value) {
[self _dbDeleteItemWithKey:key];
item = nil;
}
}
}
return item;
}
//清除緩存直到滿足大小要求
- (BOOL)removeItemsToFitSize:(int)maxSize {
if (maxSize == INT_MAX) return YES;
if (maxSize <= 0) return [self removeAllItems];
int total = [self _dbGetTotalItemSize];
if (total < 0) return NO;
if (total <= maxSize) return YES;
NSArray *items = nil;
BOOL suc = NO;
do {
int perCount = 16;
//數(shù)據(jù)庫查找并排序相關(guān)信息
items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
//遍歷刪除數(shù)據(jù)庫,如果文件系統(tǒng)中有對應(yīng)數(shù)據(jù)就刪除掉
for (YYKVStorageItem *item in items) {
if (total > maxSize) {
if (item.filename) {
[self _fileDeleteWithName:item.filename];
}
suc = [self _dbDeleteItemWithKey:item.key];
total -= item.size;
} else {
break;
}
if (!suc) break;
}
} while (total > maxSize && items.count > 0 && suc);
if (suc) [self _dbCheckpoint];
return suc;
}
上面的代碼就是操作數(shù)據(jù)庫和文件系統(tǒng)的代碼捧存,不再贅述了粪躬,不過,從寫文件的函數(shù)可以發(fā)現(xiàn)矗蕊,如果選擇保存在文件系統(tǒng)和數(shù)據(jù)庫中短蜕,那么value即會(huì)被寫入文件系統(tǒng)也會(huì)被存儲在操作系統(tǒng)中,關(guān)于YYKVStorage
的代碼不再講解了傻咖,讀者可以自行查閱朋魔。
接下來看幾個(gè)YYDiskCache
中比較重要的方法:
//YYDiskCache初始化構(gòu)造函數(shù)
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
self = [super init];
if (!self) return nil;
YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
if (globalCache) return globalCache;
/*
根據(jù)傳入的threadhold判斷緩存對象存儲類型
如果為0就所有數(shù)據(jù)存入文件中,如果為NSUIntegerMax存入數(shù)據(jù)庫中
其他值就混合存儲
*/
YYKVStorageType type;
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}
//創(chuàng)建一個(gè)YYKVStorage對象
YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
if (!kv) return nil;
_kv = kv;
_path = path;
_lock = dispatch_semaphore_create(1);
_queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
_inlineThreshold = threshold;
_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_freeDiskSpaceLimit = 0;
_autoTrimInterval = 60;
//開啟遞歸清除緩存
[self _trimRecursively];
_YYDiskCacheSetGlobal(self);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
return self;
}
上面的代碼是YYDiskCache
的初始化構(gòu)造函數(shù)卿操,主要是確定存儲類型警检、創(chuàng)建YYKVStorage
對象并創(chuàng)建數(shù)據(jù)庫表和文件目錄,開啟定時(shí)清除緩存操作害淤。
//宏定義扇雕,使用信號量充當(dāng)鎖的作用
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)
//YYDiskCache獲取緩存對象
- (id<NSCoding>)objectForKey:(NSString *)key {
if (!key) return nil;
//加鎖,通過YYKVStorage獲取key對應(yīng)的數(shù)據(jù)
Lock();
YYKVStorageItem *item = [_kv getItemForKey:key];
Unlock();
if (!item.value) return nil;
id object = nil;
//如果有自定義的unarchiveBlock就執(zhí)行反序列化操作
if (_customUnarchiveBlock) {
object = _customUnarchiveBlock(item.value);
} else {
@try {
object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (object && item.extendedData) {
[YYDiskCache setExtendedData:item.extendedData toObject:object];
}
return object;
}
//遞歸定時(shí)清除緩存
- (void)_trimRecursively {
__weak typeof(self) _self = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
__strong typeof(_self) self = _self;
if (!self) return;
[self _trimInBackground];
[self _trimRecursively];
});
}
//調(diào)用YYKVStorage的方法刪除緩存直到滿足要求
- (void)_trimToCost:(NSUInteger)costLimit {
if (costLimit >= INT_MAX) return;
[_kv removeItemsToFitSize:(int)costLimit];
}
上面就是YYDiskCache
如何通過YYKVStorage
存儲緩存對象到數(shù)據(jù)庫和文件系統(tǒng)的大致操作窥摄。其他源碼篇幅問題就不再講述了镶奉,讀者可自行閱讀。
SDWebImage
和YYCache
的磁盤緩存最大的區(qū)別就是應(yīng)用場景崭放,SDWebImage
存儲的都是圖片哨苛,圖片一般都比較大,所以直接采用文件系統(tǒng)能夠保證讀性能币砂,YYCache
作為第三方庫建峭,需要緩存任意類型的對象,所以提供了數(shù)據(jù)庫和文件系統(tǒng)結(jié)合的方式實(shí)現(xiàn)决摧,所以具體選擇什么樣的緩存策略需要考慮具體的應(yīng)用場景亿蒸。
備注
由于作者水平有限凑兰,難免出現(xiàn)紕漏,如有問題還請不吝賜教边锁。