YYMemoryCache
YYMemoryCache
用于對內(nèi)存緩存進(jìn)行管理,與SDWebImage
對于內(nèi)存緩存管理策略的區(qū)別是,SDWebImage
對于內(nèi)存緩存的管理是基于系統(tǒng)的NSCache
類,而YYMemoryCache
是基于作者自定義的雙向鏈表,并基于鏈表自定義了一套淘汰算法來對內(nèi)存使用進(jìn)行性能優(yōu)化。
_YYLinkedMap和_YYLinkedMapNode
既然有自定義鏈表,必然也有自定義的鏈表節(jié)點,關(guān)于鏈表節(jié)點聲明如下:
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@end
@implementation _YYLinkedMapNode
@end
既然_YYLinkedMap
是雙向鏈表,那么結(jié)點_YYLinkedMapNode
的數(shù)據(jù)結(jié)構(gòu)中必然會有兩個指針,_prev
指向鏈接當(dāng)前結(jié)點的上一個結(jié)點,_next
指向當(dāng)前結(jié)點鏈接的下一個結(jié)點。
_key
可以理解為它是當(dāng)前緩存數(shù)據(jù)的標(biāo)識符,比方對于UIImage
對象來說,它的key
就是URL
,那么_value
就是這個UIImage
對象。
_cost
表示當(dāng)前緩存數(shù)據(jù)的內(nèi)存占用情況,打個比方,在YYImageCahce
中對于UIImage
對象的內(nèi)存計算是通過獲取圖片的高度和行數(shù)再將這兩個數(shù)據(jù)進(jìn)行相乘的結(jié)果,代碼如下:
- (NSUInteger)imageCost:(UIImage *)image {
CGImageRef cgImage = image.CGImage;
if (!cgImage) return 1;
CGFloat height = CGImageGetHeight(cgImage);
size_t bytesPerRow = CGImageGetBytesPerRow(cgImage);
NSUInteger cost = bytesPerRow * height;
if (cost == 0) cost = 1;
return cost;
}
而在SDWebImage
的SDImageCache
中,對于UIImage
的內(nèi)存計算是UIImage
的width
和height
以及scale
相乘的結(jié)果:
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
return image.size.height * image.size.width * image.scale * image.scale;
}
最后結(jié)點還有一個成員變量_time
,表示當(dāng)前結(jié)點的生命周期。
關(guān)于自定義鏈表:
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // do not set object directly
NSUInteger _totalCost;
NSUInteger _totalCount;
_YYLinkedMapNode *_head; // MRU, do not change it directly
_YYLinkedMapNode *_tail; // LRU, do not change it directly
BOOL _releaseOnMainThread;
BOOL _releaseAsynchronously;
}
成員變量_dic
用于存放_YYLinkedMapNode
結(jié)點數(shù)據(jù),key
就是結(jié)點的key
值,value
就是結(jié)點本身。另外_dic
的類型是Core Foundation
層的CFMutableDictionaryRef
。
關(guān)于_totalCost
和_totalCount
這兩個變量,意思和NSCache
中的totalCostLimit
和countLimit
差不多,_totalCost
表示圖片的內(nèi)存總占用,_totalCount
表示總的個數(shù),這兩個變量用于后續(xù)實現(xiàn)淘汰算法。
關(guān)于_head
和_tail
變量,這兩個變量的類型都是_YYLinkedMapNode
,后面的注釋已經(jīng)解釋了這兩個變量的作用,
_head
是最近使用較多的結(jié)點(MRU),_tail
最近使用最少的結(jié)點(LRU)。
關(guān)于_releaseOnMainThread
和_releaseAsynchronously
變量,如果_releaseOnMainThread = YES
,就會在主線程釋放_dic
變量,如果_releaseAsynchronously = YES
,就會獲取一個專門用來做release
操作的異步隊列來釋放_dic
。
_YYLinkedMap
也提供了一些操作結(jié)點的方法:
1.向表頭插入一個結(jié)點
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
2.將鏈表內(nèi)某個結(jié)點移至表頭
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
3.刪除某個結(jié)點
- (void)removeNode:(_YYLinkedMapNode *)node;
4.刪除使用最少的結(jié)點
- (_YYLinkedMapNode *)removeTailNode;
5.刪除所有結(jié)點
- (void)removeAll;
YYMemoryCache
@implementation YYMemoryCache {
pthread_mutex_t _lock;
_YYLinkedMap *_lru;
dispatch_queue_t _queue;
}
YYMemoryCache
初始化做了這些事:
1、初始化互斥鎖_lock
2、初始化_lru
3、初始化串行隊列_queue
4、對_countLimit、_costLimit、_ageLimit、_autoTrimInterval
設(shè)置默認(rèn)值。
5、對_shouldRemoveAllObjectsOnMemoryWarning
和_shouldRemoveAllObjectsWhenEnteringBackground
設(shè)置默認(rèn)值為YES
,接收到內(nèi)存警告或程序進(jìn)入后臺都會清空內(nèi)存。
YYMemoryCache
提供了一些訪問方法:
#pragma mark - Access Methods
- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
- (void)removeObjectForKey:(id)key;
- (void)removeAllObjects;
#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
Access Methods
下的幾個方法都是操作鏈表_lru
及其結(jié)點。
trim
下的3個方法就是淘汰算法的實現(xiàn),也是對雙向鏈表的操作代碼,淘汰的緯度有3個,包括內(nèi)存管理容器所存儲結(jié)點的數(shù)量、結(jié)點的開銷、結(jié)點的使用頻率等。
這里有個tip
,同時也是作者分享的,就是讓block
捕獲一個局部變量,然后扔到后臺隊列去隨便發(fā)送個消息以避免編譯器警告,這樣就可以讓對象在后臺線程銷毀:
NSMutableArray *holder = [NSMutableArray new];
if (holder.count) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[holder count]; // release in queue
});
}
作者還提供了一個看起來簡單點的實現(xiàn)方式:
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
另外,在YYMemoryCache
初始化方法當(dāng)中,會調(diào)用_trimRecursively
方法,該方法每隔5秒中就會調(diào)用自己一次,另外,在這個方法中還會調(diào)用調(diào)用Trim
下的3個方法對LRU
進(jìn)行清理,以節(jié)省內(nèi)存。
以上就是YYMemoryCache
的實現(xiàn)。
YYKVStorage
在了解YYDiskCache
的實現(xiàn)原理前需要先了解一下YYKVStorage
。
作者是這樣介紹YYKVStorage
的:
YYKVStorage
是一個基于sqlite
和文件系統(tǒng)的鍵值存儲。但作者不建議我們直接使用此類(ps:這個類被封裝到了YYDiskCache里,可以通過YYDiskCache間接使用此類),這個類只有一個初始化方法,即initWithPath:type:
,初始化后,講根據(jù)path
創(chuàng)建一個目錄來保存鍵值數(shù)據(jù),初始化后,如果沒有得到當(dāng)前類的實例對象,就表示你不應(yīng)該對改目錄進(jìn)行讀寫操作。最后,作者還寫了個警告,告訴我們這個類的實例對象并不是線程安全的,你需要確保同一時間只有一條線程訪問實例對象,如果你確實需要在多線程中處理大量數(shù)據(jù),可以把數(shù)據(jù)拆分到多個實例對象當(dāng)中去。
YYKVStorage
提供了一些public
方法用于操作sqlite
和文件系統(tǒng),這些方法覆蓋了增刪改查這4個操作。
基于sqlite
的鍵值存儲
YYKVStorage
實現(xiàn)了對sqlite
的封裝,包括數(shù)據(jù)庫初始化、打開數(shù)據(jù)庫、關(guān)閉數(shù)據(jù)庫、執(zhí)行sql
等操作。這個類中絕大多數(shù)代碼也是對這些操作的實現(xiàn)代碼。
為了加強(qiáng)數(shù)據(jù)庫檢索時的性能,在建表的同時又為表建立了索引。
表名叫manifest
,表內(nèi)有幾個字段:
key:主鍵,增刪改查操作都圍繞這個主鍵來完成
filename:文件名稱
size:文件大小
inline_data:存儲的二進(jìn)制數(shù)據(jù)
modification_time:文件修改時間
last_access_time:文件最后訪問時間
extended_data:文件擴(kuò)建時間
1. 保存
方法:saveItemWithKey:value:filename:extendedData:
數(shù)據(jù)保存的目標(biāo)地點分為兩種,一種是直接放在沙盒指定目錄下,另外一種是存儲在數(shù)據(jù)庫(雖然db
也是放在沙盒里的),具體采用哪種目標(biāo)地點會根據(jù)filename
做判斷,如果filename
存在即表明數(shù)據(jù)是直接存儲在沙盒某個目錄下的文件里,如果filename
不存在就會走和數(shù)據(jù)庫相關(guān)的流程。
如果是寫入文件:
- 會根據(jù)
filename
生成完整的存儲路徑,再把value
寫入到目標(biāo)文件中。- 如果寫入成功不再執(zhí)行后續(xù)代碼。
- 如果寫入失敗,則嘗試把數(shù)據(jù)寫入到數(shù)據(jù)庫中。
- 如果寫入成功不再執(zhí)行后續(xù)代碼;
- 如果數(shù)據(jù)庫也寫入失敗了,就會刪除前面生成的存儲路徑下的文件,避免產(chǎn)生垃圾文件。
如果是寫入數(shù)據(jù)庫:
- 首先會判斷當(dāng)前的
storage
對象是不是用來做數(shù)據(jù)庫緩存操作的實例對象。- 如果是,根據(jù)方法傳入的參數(shù)重新向數(shù)據(jù)庫中插入數(shù)據(jù)。
- 如果不是,根據(jù)
key
到數(shù)據(jù)庫里查詢對應(yīng)的filename
,根據(jù)filename
刪除相應(yīng)路徑下的文件,最后根據(jù)方法傳入的參數(shù)重新向數(shù)據(jù)庫中插入數(shù)據(jù)。
2. 查詢
方式一:直接獲取二進(jìn)制數(shù)據(jù):
(1)從文件中查詢:
1.根據(jù)key
從數(shù)據(jù)庫查找到對應(yīng)的filename
2.根據(jù)步驟1
中查詢到的filename
生成完成的文件存儲路徑并讀取文件數(shù)據(jù),更新數(shù)據(jù)中該條數(shù)據(jù)的訪問時間。
3.如果步驟2
中沒讀取到數(shù)據(jù),則把這條數(shù)據(jù)從數(shù)據(jù)庫中刪除。
(2)從數(shù)據(jù)庫中查詢:
1.直接根據(jù)key
到數(shù)據(jù)庫中查詢inline_data
,這個inline_data
對應(yīng)著YYKVStorageItem
中的value
屬性。
2.更新數(shù)據(jù)中該條數(shù)據(jù)的訪問時間
(3)混合查詢(文件&數(shù)據(jù)庫):
1.根據(jù)key
找到filename
2.filename
存在讀取文件數(shù)據(jù),如果不存在數(shù)據(jù)則刪除文件。
3.filename
不存在則去數(shù)據(jù)庫中通過key
獲取inline_data
。
4.更新數(shù)據(jù)中該條數(shù)據(jù)的訪問時間
以上3個查詢操作獲取的均是單純的二進(jìn)制數(shù)據(jù),這些二進(jìn)制數(shù)據(jù)可能是由NSString
、UIImage
等對象轉(zhuǎn)換而來,調(diào)用者可根據(jù)需要自己轉(zhuǎn)換回原來的數(shù)據(jù)類型。
方式二:把獲取到的數(shù)據(jù)封裝成YYKVStorageItem
:
1.根據(jù)key
到數(shù)據(jù)庫中查找數(shù)據(jù),根據(jù)數(shù)據(jù)生成YYKVStorageItem
實例對象。
2.如果1
中獲取的對象存在則同時更新當(dāng)前數(shù)據(jù)的last_access_time
(最后訪問時間)。
3.通過拿到的filename
生成文件路徑,讀取該文件,獲取文件內(nèi)存儲的二進(jìn)制數(shù)據(jù)。
4.如果二進(jìn)制數(shù)據(jù)不存在,就把該數(shù)據(jù)從數(shù)據(jù)庫中刪除,最后返回這個YYKVStorageItem
實例對象。
YYDiskCache
知道YYKVStorage
做什么之后,再來看YYDiskCache
就簡單了。
作者這樣介紹YYDiskCache
:
YYDiskCache是一個線程安全的緩存,用于存儲SQLite支持的鍵值對和文件系統(tǒng)(類似于NSURLCache的磁盤緩存)。
YYDiskCache具有以下功能:
1.它使用LRU(最近最少使用)來刪除對象。
2.它可以通過成本,計數(shù)和年齡來控制。
3.它可以配置為在沒有可用磁盤空間時自動驅(qū)逐對象。
4.它可以自動決定每個對象的存儲類型(sqlite / file)。
總結(jié)一下就是:
YYDiskCache
封裝了YYKVStorage
,在YYDiskCache
中對于disk
的緩存操作實際上都是通過YYKVStorage
完成的,除此之外,YYDiskCache
又自定義了淘汰規(guī)則,刪除那些最近時間段內(nèi)不常用的對象。
YYCache
YYCache
封裝了YYMemoryCache
和YYDiskCache
。
YYCache
初始化需要一個NSString
類型的name
或path
,它會根據(jù)這兩個值生成一個路徑,根據(jù)這個路徑初始化出YYDiskCache
。
所以,接下來的事情就好辦了。
如果是存儲操作,YYCache
首先會通過YYMemoryCache
放進(jìn)內(nèi)存緩存,然后通過YYDiskCache
放進(jìn)磁盤緩存。
如果是查詢操作,YYCache
首先會通過YYMemoryCache
先到內(nèi)存緩存中取,如果內(nèi)存緩存中沒有,再通過YYDiskCache
到磁盤緩存中取。
如果是刪除操作,YYCache
首先會通過YYMemoryCache
刪除內(nèi)存緩存的數(shù)據(jù),然后通過YYDiskCache
刪除磁盤緩存的數(shù)據(jù)。
總結(jié)
YYCache
自定義了內(nèi)存緩存和磁盤緩存類,并實現(xiàn)了各自的淘汰算法,在時間和空間上對數(shù)據(jù)緩存操作都進(jìn)行了優(yōu)化。