在平時的開發(fā)中,總是會用到各種緩存的共虑,也常使用各種三方的庫愧怜,在這些三方的緩存庫中,首屈一指的就數(shù)國內(nèi)大神@ibireme
造的輪子YYCache
妈拌,這是一個非常優(yōu)秀的緩存庫拥坛,性能高、線程安全和代碼質(zhì)量高尘分。
具體的設(shè)計思路和源碼分析猜惋,網(wǎng)絡(luò)上有大量優(yōu)秀的文章,這里不贅述培愁,這里提供幾篇參考文檔:
https://blog.ibireme.com/category/tec/ios-tec/
http://www.cocoachina.com/articles/20980
https://juejin.im/post/5a657a946fb9a01cb64ee761#heading-17
但YYCache
是一個Objective-C
的輪子著摔,本著學(xué)習(xí)的心態(tài),參照優(yōu)秀的代碼嘗試實現(xiàn)了一個Swift
的緩存庫竭钝。
YYCache
代碼結(jié)構(gòu)
首先我們來看一下YYCache
的代碼結(jié)構(gòu):
YYCache
中的類如圖中所示梨撞,結(jié)構(gòu)也非常清晰雹洗,職責(zé)也非常明確:
-
YYCache
是由YYMemoryCache
與YYDiskCache
兩部分組成的,其中 YYMemoryCache 作為高速內(nèi)存緩存卧波,而 YYDiskCache 則作為低速磁盤緩存时肿; -
YYMemoryCache
負(fù)責(zé)處理容量小,相對高速的內(nèi)存緩存港粱;線程安全螃成,支持自動和手動清理緩存等功能; -
_YYLinkedMap
是YYMemoryCache
使用的雙向鏈表類查坪; -
_YYLinkedMapNode
是_YYLinkedMap
使用的節(jié)點類寸宏; -
YYDiskCache
負(fù)責(zé)處理容量大,相對低速的磁盤緩存偿曙;線程安全氮凝,支持異步操作,自動和手動清理緩存等功能望忆; -
YYKVStorage
是YYDiskCache
的底層實現(xiàn)類罩阵,用于管理磁盤緩存 -
YYKVStorageItem
內(nèi)置在YYKVStorage
中,是YYKVStorage
內(nèi)部用于封裝某個緩存的類启摄。
Swift
化
本文的代碼結(jié)構(gòu)和職責(zé)拆分均完全參考YYCache
的思路稿壁,只是語言使用使用Swfit
,以及在具體實現(xiàn)上使用個人認(rèn)為更優(yōu)的方式歉备。
Tips:將內(nèi)存緩存和磁盤緩存同時使用傅是,發(fā)揮出最高性能的實現(xiàn)方式暫時沒有確定,所以并沒有設(shè)計類似YYCache
將YYMemoryCache
與YYDiskCache
兩部分組合使用的類蕾羊。組合使用會放到后續(xù)的優(yōu)化中喧笔。
協(xié)議的定義
首先定義了兩個協(xié)議:
- 一個是緩存核心能力的協(xié)議
XRCacheProtocol
/// cache protocol
public protocol XRCacheProtocol {
associatedtype Element
// 獲取緩存
func get(_ key: String) -> Element?
// 設(shè)置緩存
func set(_ key: String, value: Element, cost: UInt, completion: (() -> Void)?)
// 移除單個緩存
func remove(_ key: String, completion: (() -> Void)?)
// 移除所有緩存
func removeAll(_ completion: (() -> Void)?)
// 是否包含緩存
func containsObjectForKey(_ key: String) -> Bool
}
// MARK: 默認(rèn)實現(xiàn)
public extension XRCacheProtocol {
func containsObjectForKey(_ key: String) -> Bool {
if let _ = get(key) { return true }
return false
}
}
- 一個是內(nèi)存修剪的協(xié)議
XRCacheTrimProtocol
/// cache trim protocol
public protocol XRCacheTrimProtocol {
// 按數(shù)量清理
func trimToCount(_ count: UInt, completion: (() -> Void)?)
// 按消耗清理
func trimToCost(_ cost: UInt, completion: (() -> Void)?)
// 按時間清理
func trimToAge(_ age: Double, completion: (() -> Void)?)
}
內(nèi)存緩存
YYMemoryCache
是一個線程安全及實現(xiàn)了LRU
淘汰算法的高效緩存,我們這里同樣以此為目標(biāo)肚豺。
同樣的溃斋,我們不直接操作緩存對應(yīng)界拦,使用雙向鏈表和字典來間接操作緩存對象吸申,這么做的好處是同時發(fā)揮雙向鏈表的增刪改的效率優(yōu)勢和字典的讀取效率優(yōu)勢,這也是為什么YYMemoryCache
會非常高效享甸。
鏈表節(jié)點
首先定義鏈表的節(jié)點:
/// 內(nèi)部節(jié)點
fileprivate class _XRLinkedNode<E> where E: Equatable {
/// 緩存key
var _key: String?
/// key對應(yīng)值
var _value: E?
/// 上一個節(jié)點
weak var _prev: _XRLinkedNode<E>?
/// 下一個節(jié)點
var _next: _XRLinkedNode<E>?
/// 緩存開銷
var _cost: UInt = 0
/// 訪問時間戳
var _time: Double = 0
init(key: String?, value: E?, prev: _XRLinkedNode<E>?, next: _XRLinkedNode<E>?, cost: UInt, time: Double) {
self._key = key
self._value = value
self._prev = prev
self._next = next
self._cost = cost
self._time = time
}
}
在對節(jié)點的處理過程中截碴,會使用==
的比較,所以這里讓_XRLinkedNode
實現(xiàn)Equatable
的協(xié)議蛉威,方便后續(xù)的操作:
// MARK: - Equatable
extension _XRLinkedNode: Equatable {
static func == (lhs: _XRLinkedNode<E>, rhs: _XRLinkedNode<E>) -> Bool {
return (lhs._key == rhs._key && lhs._value == rhs._value)
}
}
雙向鏈表
然后是雙線鏈表的實現(xiàn):
/// 雙向鏈表
fileprivate class _XRLinkedList<E> where E: Equatable {
/// 存放節(jié)點 dict
fileprivate var _dic: [String: _XRLinkedNode<E>] = [:]
/// 總開銷
fileprivate var _totalCost: UInt = 0
/// 節(jié)點總數(shù)
fileprivate var _totalCount: UInt = 0
/// 是否在主線程釋放日丹,默認(rèn)為false
fileprivate var _releaseOnMainThread: Bool = false
/// 是否在子線程釋放,默認(rèn)為true
fileprivate var _releaseAsynchronously: Bool = true
/// 首個節(jié)點
private var _head: _XRLinkedNode<E>?
/// 最后節(jié)點
fileprivate var _tail: _XRLinkedNode<E>?
}
// MARK: - public method
fileprivate extension _XRLinkedList {
/// 添加節(jié)點到頭部
/// - Parameter node: 節(jié)點
func insertNodeAtHead(_ node: _XRLinkedNode<E>) {
guard let k = node._key else { return }
_dic[k] = node
if let _ = _head { // 存在頭部節(jié)點
node._next = _head
_head?._prev = node
_head = node
} else {
_head = node
_tail = node
}
_totalCost += node._cost
_totalCount += 1
}
/// 將節(jié)點移動到頭部
/// - Parameter node: 節(jié)點
func bringNodeToHead(_ node: _XRLinkedNode<E>) {
// node 就是 head
if _head == node { return }
if _tail == node { // node 就是 tail
_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é)點
/// - Parameter node: 節(jié)點
func removeNode(_ node: _XRLinkedNode<E>) {
guard let k = node._key else { return }
_dic.removeValue(forKey: k)
_totalCost -= node._cost
_totalCount -= 1
// 存在下一個節(jié)點
if let _ = node._next {
node._next?._prev = node._prev
}
// 存在上一個節(jié)點
if let _ = node._prev {
node._prev?._next = node._next
}
// node 為 head
if _head == node {
_head = node._next
}
// node 為 tail
if _tail == node {
_tail = node._prev
}
}
/// 移除尾節(jié)點
func removeTailNode() -> _XRLinkedNode<E>? {
guard let tail = _tail, let k = _tail?._key else { return nil }
_dic.removeValue(forKey: k)
_totalCost -= tail._cost
_totalCount -= 1
if _head == tail { // 只有一個節(jié)點
_head = nil
_tail = nil
} else {
_tail = tail._prev
_tail?._next = nil
}
return tail
}
/// 移除所有節(jié)點
func removeAll() {
_totalCost = 0
_totalCount = 0
_head = nil
_tail = nil
// 存在節(jié)點時
if _dic.count > 0 {
var temp = _dic
_dic = [:]
if _releaseAsynchronously { // 子線程釋放
let queue = _releaseOnMainThread ? DispatchQueue.main : DispatchQueue.global(qos: .background)
queue.async {
temp.removeAll()
}
} else if _releaseOnMainThread,
pthread_main_np() == 0 { // 主線程釋放蚯嫌,且當(dāng)前處于主線程
DispatchQueue.main.async {
temp.removeAll()
}
} else {
temp.removeAll()
}
}
}
}
雙向鏈表的實現(xiàn)哲虾,有兩個點說明一下:
-
1.這是針對緩存業(yè)務(wù)定制的雙向鏈表丙躏,鏈表的操作思路是一樣的,并不是完整的鏈表
如果對
Swift
版本的完整鏈表有興趣束凑,可以參考Swift鏈表晒旅。 -
2.異步釋放的技巧
let queue = _releaseOnMainThread ? DispatchQueue.main : DispatchQueue.global(qos: .background) queue.async { temp.removeAll() }
這個技巧
ibireme
在他的另一篇文章 iOS 保持界面流暢的技巧 中有提及:Note: 對象的銷毀雖然消耗資源不多,但累積起來也是不容忽視的汪诉。通常當(dāng)容器類持有大量對象時废恋,其銷毀時的資源消耗就非常明顯。同樣的扒寄,如果對象可以放到后臺線程去釋放鱼鼓,那就挪到后臺線程去。這里有個小 Tip:把對象捕獲到 block 中该编,然后扔到后臺隊列去隨便發(fā)送個消息以避免編譯器警告迄本,就可以讓對象在后臺線程銷毀了。
內(nèi)存緩存
內(nèi)存緩存课竣,主體定義與YYCache
一致:
/// 內(nèi)存緩存
public final class XRMemoryCache<Element: Equatable> {
/// 緩存數(shù)量
var totalCount: UInt {
pthread_mutex_lock(&_lock)
let count = _lru._totalCount
pthread_mutex_unlock(&_lock)
return count
}
/// 緩存消耗
var totalCost: UInt {
pthread_mutex_lock(&_lock)
let totalCost = _lru._totalCost
pthread_mutex_unlock(&_lock)
return totalCost
}
/// 是否在主線程釋放岸梨,默認(rèn)為false
var releaseOnMainThread: Bool {
set {
pthread_mutex_lock(&_lock)
_lru._releaseOnMainThread = newValue
pthread_mutex_unlock(&_lock)
}
get {
pthread_mutex_lock(&_lock)
let value = _lru._releaseOnMainThread
pthread_mutex_unlock(&_lock)
return value
}
}
/// 是否在子線程釋放,默認(rèn)為true
var releaseAsynchronously: Bool {
set {
pthread_mutex_lock(&_lock)
_lru._releaseAsynchronously = newValue
pthread_mutex_unlock(&_lock)
}
get {
pthread_mutex_lock(&_lock)
let value = _lru._releaseAsynchronously
pthread_mutex_unlock(&_lock)
return value
}
}
/// cache 名
var name: String?
/// 最大緩存數(shù)量
var countLimit: UInt = UInt.max
/// 最大消耗
var costLimit: UInt = UInt.max
/// 最大到期時間
var ageLimit: Double = Double.greatestFiniteMagnitude
/// 自動調(diào)整檢查時間間隔稠氮,默認(rèn)5.0
var autoTrimInterval: Double = 5.0
/// 接收到內(nèi)存警告時曹阔,是否移除所有緩存,默認(rèn)true
var shouldRemoveAllObjectsOnMemoryWarning: Bool = true
/// 切換到后臺隔披,是否移除所有緩存赃份,默認(rèn)true
var shouldRemoveAllObjectsWhenEnteringBackground: Bool = true
/// 接收到內(nèi)存警告回調(diào)
var didReceiveMemoryWarningBlock: ((_ cache: XRMemoryCache) -> ())?
/// 切換到后臺回調(diào)
var didEnterBackgroundBlock: ((_ cache: XRMemoryCache) -> ())?
/// 互斥鎖
private var _lock: pthread_mutex_t = pthread_mutex_t()
/// lru淘汰算法鏈表
private var _lru = _XRLinkedList<Element>()
/// 隊列
private var _queue = DispatchQueue(label: "com.xr.cache.memory")
init() {
pthread_mutex_init(&_lock, nil)
NotificationCenter.default.addObserver(self, selector: #selector(_appDidReceiveMemoryWarningNotification), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(_appDidEnterBackgroundNotification), name: UIApplication.didEnterBackgroundNotification, object: nil)
_trimRecursively()
}
deinit {
NotificationCenter.default.removeObserver(self, name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
_lru.removeAll()
pthread_mutex_destroy(&_lock)
}
// MARK: observe method
@objc func _appDidReceiveMemoryWarningNotification() {
didReceiveMemoryWarningBlock?(self)
if shouldRemoveAllObjectsOnMemoryWarning { removeAll(nil) }
}
@objc func _appDidEnterBackgroundNotification() {
didEnterBackgroundBlock?(self)
if shouldRemoveAllObjectsWhenEnteringBackground { removeAll(nil) }
}
}
然后就是緩存功能協(xié)議的實現(xiàn):
extension XRMemoryCache: XRCacheProtocol {
public func get(_ key: String) -> Element? {
pthread_mutex_lock(&_lock)
let currentNode = _lru._dic[key]
if let node = currentNode {
node._time = CACurrentMediaTime()
_lru.bringNodeToHead(node)
}
pthread_mutex_unlock(&_lock)
return currentNode?._value
}
public func set(_ key: String, value: Element, cost: UInt = 0, completion: (() -> Void)? = nil) {
pthread_mutex_lock(&_lock)
let currentNode = _lru._dic[key]
let now = CACurrentMediaTime()
// 緩存節(jié)點存在時,更新時間和消耗奢米,并將節(jié)點提到頭部
if let node = currentNode {
_lru._totalCost -= node._cost
_lru._totalCost += cost
node._cost = cost
node._time = now
_lru.bringNodeToHead(node)
} else { // 不存在時抓韩,則new一個節(jié)點放到頭部
let node = _XRLinkedNode(key: key, value: value, prev: nil, next: nil, cost: cost, time: now)
_lru.insertNodeAtHead(node)
}
// 總消耗大于限制的閾值時,做內(nèi)存修剪
if _lru._totalCost > costLimit {
_queue.async {
self.trimToCost(self.costLimit)
}
}
// 總數(shù)量大于限制的閾值時鬓长,做內(nèi)存修剪
if _lru._totalCount > countLimit {
let node = _lru.removeTailNode()
_holdAndreleaseNode(node)
}
pthread_mutex_unlock(&_lock)
}
public func remove(_ key: String, completion: (() -> Void)? = nil) {
pthread_mutex_lock(&_lock)
let currentNode = _lru._dic[key]
if let node = currentNode {
_lru.removeNode(node)
_holdAndreleaseNode(node)
}
pthread_mutex_unlock(&_lock)
}
public func removeAll(_ completion: (() -> Void)? = nil) {
pthread_mutex_lock(&_lock)
_lru.removeAll()
pthread_mutex_unlock(&_lock)
}
// MARK: - 提供便利的下標(biāo)方法
public subscript(key: String) -> Element? {
get {
self[key, 0]
}
set(newValue) {
self[key, 0] = newValue
}
}
public subscript(key: String, cost: UInt) -> Element? {
get {
get(key)
}
set(newValue) {
if let newValue = newValue {
set(key, value: newValue, cost: cost)
} else {
remove(key)
}
}
}
}
然后就是緩存裁剪協(xié)議的實現(xiàn):
// MARK: - XRCacheTrimProtocol
extension XRMemoryCache: XRCacheTrimProtocol {
public func trimToCount(_ count: UInt, completion: (() -> Void)? = nil) {
if count == 0 { removeAll(nil) ; return }
_trimToCount(count)
}
public func trimToCost(_ cost: UInt, completion: (() -> Void)? = nil) {
_trimToCost(cost)
}
public func trimToAge(_ age: Double, completion: (() -> Void)? = nil) {
_trimToAge(age)
}
}
這里劃兩個重點:
1.如何保證線程安全
ibireme
選擇使用pthread_mutex
線程鎖來確保YYMemoryCache
的線程安全谒拴,我們這里也使用一樣的思路:
/// 互斥鎖
private var _lock: pthread_mutex_t = pthread_mutex_t()
/// lru淘汰算法鏈表,間接操作緩存
private var _lru = _XRLinkedList<Element>()
/// 隊列
private var _queue = DispatchQueue(label: "com.xr.cache.memory")
ibireme
在他的博客中說明了使用pthread_mutex
線程鎖的原因:
ibireme: 蘋果員工說 libobjc 里 spinlock 是用了一些私有方法 (mach_thread_switch)涉波,貢獻(xiàn)出了高線程的優(yōu)先來避免優(yōu)先級反轉(zhuǎn)的問題英上,但是我翻了下 libdispatch 的源碼倒是沒發(fā)現(xiàn)相關(guān)邏輯,也可能是我忽略了什么啤覆。在我的一些測試中苍日,OSSpinLock 和 dispatch_semaphore 都不會產(chǎn)生特別明顯的死鎖,所以我也無法確定用 dispatch_semaphore 代替 OSSpinLock 是否正確窗声。能夠肯定的是相恃,用 pthread_mutex 是安全的。
2.LRU
算法的實現(xiàn)
什么是LRU
的話笨觅,就自行百度了哈拦耐。
雙向鏈表中有頭結(jié)點和尾節(jié)點:
- 頭結(jié)點 = 鏈表中用戶最近一次使用(訪問)的緩存對象節(jié)點耕腾,MRU;
- 尾節(jié)點 = 鏈表中用戶已經(jīng)很久沒有再次使用(訪問)的緩存對象節(jié)點杀糯,LRU幽邓。
如何讓頭結(jié)點和尾節(jié)點指向我們想指向的緩存對象節(jié)點?參考大神的思路實現(xiàn)如下:
-
當(dāng)訪問一個已有的緩存時火脉,要把這個緩存節(jié)點移動到鏈表頭部牵舵,原位置兩側(cè)的緩存要接上,并且原鏈表頭部的緩存節(jié)點要變成現(xiàn)在鏈表的第二個緩存節(jié)點倦挂;
public func get(_ key: String) -> Element? { pthread_mutex_lock(&_lock) let currentNode = _lru._dic[key] if let node = currentNode { node._time = CACurrentMediaTime() // 更新緩存節(jié)點時間畸颅,并將其移動至雙向鏈表頭結(jié)點 _lru.bringNodeToHead(node) } pthread_mutex_unlock(&_lock) return currentNode?._value }
-
當(dāng)寫入一個新的緩存時,要把這個緩存節(jié)點放在鏈表頭部方援,并且并且原鏈表頭部的緩存節(jié)點要變成現(xiàn)在鏈表的第二個緩存節(jié)點没炒;
public func set(_ key: String, value: Element, cost: UInt = 0, completion: (() -> Void)? = nil) { pthread_mutex_lock(&_lock) let currentNode = _lru._dic[key] let now = CACurrentMediaTime() // 緩存節(jié)點存在時,更新時間和消耗犯戏,并將節(jié)點提到頭部 if let node = currentNode { _lru._totalCost -= node._cost _lru._totalCost += cost node._cost = cost node._time = now _lru.bringNodeToHead(node) } else { // 不存在時送火,則new一個節(jié)點放到頭部 let node = _XRLinkedNode(key: key, value: value, prev: nil, next: nil, cost: cost, time: now) _lru.insertNodeAtHead(node) } // 總消耗大于限制的閾值時,做內(nèi)存修剪 if _lru._totalCost > costLimit { _queue.async { self.trimToCost(self.costLimit) } } // 總數(shù)量大于限制的閾值時先匪,做內(nèi)存修剪 if _lru._totalCount > countLimit { let node = _lru.removeTailNode() _holdAndreleaseNode(node) } pthread_mutex_unlock(&_lock) }
-
在資源不足時种吸,從雙線鏈表的尾節(jié)點(LRU)開始清理緩存,釋放資源呀非,這里只拿消耗(cost)舉例坚俗,數(shù)量(count)和時間(age)類似。
func _trimToCost(_ costLimit: UInt) { var finish = false pthread_mutex_lock(&_lock) if costLimit == 0 { // 消耗最大值為0時岸裙,移除全部 _lru.removeAll() finish = true } else if _lru._totalCost <= costLimit { // 總消耗小于閾值時猖败,不做任何處理 finish = true } pthread_mutex_unlock(&_lock) if finish { return } // 集中釋放的容器 var holder: [_XRLinkedNode<Element>] = [] while !finish { // 嘗試加鎖,如果加成功降允,則執(zhí)行后面邏輯 if pthread_mutex_trylock(&_lock) == 0 { if _lru._totalCost > costLimit { // 需要修剪時 let tailNode = _lru.removeTailNode() if let node = tailNode { holder.append(node) } } else { finish = true } } else { // 加鎖失敗的話恩闻,等待 10 ms usleep(10 * 1000) // 10 ms } pthread_mutex_unlock(&_lock); } // holder不為空 if holder.isEmpty { let queue = _lru._releaseOnMainThread ? DispatchQueue.main : DispatchQueue.global(qos: .background) queue.async { // 在當(dāng)前隊列中排隊等待并釋放 _ = holder.count } } }
磁盤緩存
YYDiskCache
是一個線程安全的磁盤緩存,用于存儲由SQLite
和文件系統(tǒng)支持的鍵值對(類似于NSURLCache
的磁盤緩存)剧董。
- 使用
LRU(least-recently-used)
來裁剪緩存幢尚; - 支持按 cost,count 和 age 進(jìn)行控制送滞;
- 可以被配置為當(dāng)沒有可用的磁盤空間時自動驅(qū)逐緩存對象侠草;
- 可以自動抉擇每個緩存對象的存儲類型
(sqlite/file)
以便提供更好的性能表現(xiàn)
主體思路還是按大神的設(shè)計,Swift
下的數(shù)據(jù)庫犁嗅,我選擇使用了微信團(tuán)隊開源的WCDB
,相對于sqlite
晤碘,具備如下優(yōu)勢:
易用褂微,WCDB支持一句代碼即可將數(shù)據(jù)取出并組合為object功蜓;
通過WINQ,開發(fā)者無須為了拼接SQL的字符串而寫一大坨膠水代碼宠蚂;
高效式撼,WCDB通過框架層和sqlcipher源碼優(yōu)化,使其更高效的表現(xiàn)求厕;
-
ORM(Object Relational Mapping):在WCDB內(nèi)著隆,ORM(Object Relational Mapping)是指:
將一個ObjC的類,映射到數(shù)據(jù)庫的表和索引呀癣;
將類的property美浦,映射到數(shù)據(jù)庫表的字段; 多線程高并發(fā):WCDB支持多線程讀與讀项栏、讀與寫并發(fā)執(zhí)行浦辨,寫與寫串行執(zhí)行。
Tips:自動抉擇每個緩存對象的存儲類型(sqlite/file)
以便提供更好的性能表現(xiàn)沼沈,這點暫時沒有實現(xiàn)流酬,目前數(shù)據(jù)庫沒有存儲緩存對象,該店后續(xù)優(yōu)化列另。
然后將磁盤存儲類拆了兩個芽腾,一個負(fù)責(zé)磁盤存儲,一個負(fù)責(zé)數(shù)據(jù)庫存儲页衙。
整體的設(shè)計思路與內(nèi)存緩存類似晦嵌,這里直接就上代碼了:
XRStorageItem
—— 數(shù)據(jù)庫存儲最小單元
/// 磁盤 緩存 數(shù)據(jù)模型(用來做淘汰算法)
public class XRStorageItem: TableCodable {
/// 鍵
var key: String?
/// 文件大小,單位為 byte
var size: Int = 0
/// 修改的時間戳
var modTime: Double = 0
/// 最后訪問的時間戳
var accessTime: Double = 0
/// 緩存二進(jìn)制數(shù)據(jù)(僅作為數(shù)據(jù)存儲拷姿,不存DB)
var value: Data?
/// WCDB協(xié)議 實現(xiàn)
public enum CodingKeys: String, CodingTableKey {
public typealias Root = XRStorageItem
public static let objectRelationalMapping = TableBinding(CodingKeys.self)
case key
case size
case modTime
case accessTime
public static var columnConstraintBindings: [CodingKeys: ColumnConstraintBinding]? {
// 配置key為主鍵惭载,非空,默認(rèn)值為""
return [key: ColumnConstraintBinding(isPrimary: true, isNotNull: true, defaultTo: "")]
}
}
}
該類用于數(shù)據(jù)庫的存儲响巢,所以遵循了WCDB
的協(xié)議描滔,關(guān)于WCDB
可以到WCDB獲取源碼和文檔。
XRStorage
—— 數(shù)據(jù)庫存儲管理類
/// 存儲類
public class XRStorage {
/// 數(shù)據(jù)庫
private var database: Database!
/// 路徑最大長度
private static let kPathLengthMax = PATH_MAX - 64
private lazy var diskCachePath: String = {
let diskCachePath = (XRFileManagerStorage.basePath as NSString).appendingPathComponent("data")
try! FileManager.default.createDirectory(atPath: diskCachePath, withIntermediateDirectories: true, attributes: nil)
return diskCachePath
}()
private lazy var dbUrl: URL? = {
let dbUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).last?.appendingPathComponent("com.xr.disk.cache").appendingPathComponent("db").appendingPathComponent("xrcache.db")
return dbUrl
}()
/// 文件存儲
private lazy var fileStorage = XRFileManagerStorage(path: self.diskCachePath)
// MARK: 構(gòu)造函數(shù)
/// 構(gòu)造函數(shù)
/// - Parameter path: 路徑
init() {
if let url = dbUrl {
database = Database(withFileURL: url)
} else {
var path = (XRFileManagerStorage.basePath as NSString).appendingPathComponent("db")
path = (path as NSString).appendingPathComponent("xrcache.db")
database = Database(withPath: path)
}
dbInitialize()
}
}
// MARK: - public method
public extension XRStorage {
/// 保存item
func saveItem(_ item: XRStorageItem) -> Bool {
guard
let key = item.key,
let value = item.value
else { return false }
return saveItem(key, value: value)
}
/// 保存item
@discardableResult
func saveItem(_ key: String, value: Data) -> Bool {
guard
!key.isEmpty,
!value.isEmpty
else { return false }
if !fileStorage.setData(value, key: key) {
return false
}
if !_dbSave(key, size: value.count) {
fileStorage.removeData(with: key)
return false
}
return true
}
@discardableResult
func removeItem(_ key: String) -> Bool {
guard !key.isEmpty else { return false }
fileStorage.removeData(with: key)
return _dbDeleteItemWithKey(key)
}
func removeItem(_ keys: [String]) -> Bool {
keys.forEach { fileStorage.removeData(with: $0) }
return _dbDeleteItemWithKeys(keys)
}
func removeItemsLargerThanSize(_ size: Int) -> Bool {
if size == Int.max { return false }
if size <= 0 { return removeAllItems() }
if let keys = _dbGetKeysWithSizeLargerThan(size) {
keys.forEach { self.fileStorage.removeData(with: $0) }
}
return _dbDeleteItemsWithSizeLargerThan(size)
}
@discardableResult
func removeItemsEarlierThanTime(_ time: Double) -> Bool {
if time <= 0 { return true }
if time == Double(Int.max) { return removeAllItems() }
if let keys = _dbGetKeysWithTimeEarlierThan(time) {
keys.forEach { self.fileStorage.removeData(with: $0) }
}
return _dbDeleteItemsWithTimeEarlierThan(time)
}
func removeItemsToFitSize(_ maxSize: Int) -> Bool {
if maxSize == Int.max { return true }
if maxSize <= 0 { return removeAllItems() }
if var total = _dbGetTotalItemSize() {
if total < 0 { return false }
if total <= maxSize { return true }
var items: [XRStorageItem] = []
var isSuc: Bool = false
repeat {
if let itemAry = _dbGetItemSizeInfoOrderByTimeAscWithLimit(16) {
items = itemAry
items.forEach {
if total > maxSize {
if let key = $0.key {
self.fileStorage.removeData(with: key)
isSuc = self._dbDeleteItemWithKey(key)
}
total -= $0.size
} else {
return
}
if !isSuc { return }
}
}
} while (total > maxSize && items.count > 0 && isSuc)
return isSuc
}
return false
}
@discardableResult
func removeItemsToFitCount(_ maxCount: Int) -> Bool {
if maxCount == Int.max { return false }
if maxCount <= 0 { return removeAllItems() }
if var total = _dbGetTotalItemCount() {
if total < 0 { return false }
if total <= maxCount { return true }
var items: [XRStorageItem] = []
var isSuc: Bool = false
repeat {
if let itemAry = _dbGetItemSizeInfoOrderByTimeAscWithLimit(16) {
items = itemAry
items.forEach {
if total > maxCount {
if let key = $0.key {
self.fileStorage.removeData(with: key)
isSuc = self._dbDeleteItemWithKey(key)
}
total -= 1
} else {
return
}
if !isSuc { return }
}
}
} while (total > maxCount && items.count > 0 && isSuc)
return isSuc
}
return false
}
@discardableResult
func removeAllItems() -> Bool {
fileStorage.removeAllData()
/// 數(shù)據(jù)庫
if !dbInitialize() { return false }
return true
}
func getItem(_ key: String) -> XRStorageItem? {
guard !key.isEmpty else { return nil }
if let item = _dbGetItem(key) {
_dbUpdateAccessTimeWithKey(key)
if let value = fileStorage.fetchData(key) {
item.value = value
return item
} else {
_dbDeleteItemWithKey(key)
}
}
return nil
}
func getItemWithoutValue(_ key: String) -> XRStorageItem? {
guard !key.isEmpty else { return nil }
return _dbGetItem(key)
}
func getItemValue(_ key: String) -> Data? {
guard !key.isEmpty else { return nil }
if let value = fileStorage.fetchData(key) {
return value
} else {
_dbDeleteItemWithKey(key)
}
return nil
}
func getItem(_ keys: [String]) -> [XRStorageItem]? {
guard !keys.isEmpty else { return nil }
if let items = _dbGetItems(keys),
!items.isEmpty {
_dbUpdateAccessTimeWithKeys(keys)
items.forEach {
if let key = $0.key {
if let value = self.fileStorage.fetchData(key) {
$0.value = value
} else {
_dbDeleteItemWithKey(key)
}
}
}
}
return nil
}
func getItemWithoutValue(_ keys: [String]) -> [XRStorageItem]? {
guard !keys.isEmpty else { return nil }
return _dbGetItems(keys)
}
func getItemValue(_ keys: [String]) -> [String: Data]? {
guard !keys.isEmpty else { return nil }
var kvs: [String: Data] = [:]
keys.forEach {
if let value = self.fileStorage.fetchData($0) {
kvs[$0] = value
}
}
return !kvs.isEmpty ? kvs : nil
}
func itemExists(_ key: String) -> Bool {
guard !key.isEmpty else { return false }
if let cnt = _dbGetItemCountWithKey(key) {
return cnt > 0
}
return false
}
func getItemsCount() -> Int? {
return _dbGetTotalItemCount()
}
func getItemsSize() -> Int? {
return _dbGetTotalItemSize()
}
}
// MARK: - private method
private extension XRStorage {
@discardableResult
func dbInitialize() -> Bool {
do {
try database.create(table: "XRStorageItemTable", of: XRStorageItem.self)
return true
} catch {}
XRCacheLog.error(message: "~~~ 建表失敗 !!!")
return false
}
}
// MARK: - db operate
private extension XRStorage {
func _dbSave(_ key: String, size: Int) -> Bool {
let item = XRStorageItem()
item.key = key
item.size = size
let currentTime = CFAbsoluteTimeGetCurrent()
item.modTime = currentTime
item.accessTime = currentTime
if let _ = try? database.insert(objects: item, intoTable: "XRStorageItemTable") {
return true
}
return false
}
@discardableResult
func _dbUpdateAccessTimeWithKey(_ key: String) -> Bool {
let item = XRStorageItem()
item.accessTime = CFAbsoluteTimeGetCurrent()
if let _ = try? database.update(table: "XRStorageItemTable",
on: XRStorageItem.Properties.accessTime,
with: item,
where: XRStorageItem.Properties.key == key) {
return true
}
return false
}
@discardableResult
func _dbUpdateAccessTimeWithKeys(_ keys: [String]) -> Bool {
let item = XRStorageItem()
item.accessTime = CFAbsoluteTimeGetCurrent()
if let _ = try? database.update(table: "XRStorageItemTable",
on: XRStorageItem.Properties.accessTime,
with: item,
where: XRStorageItem.Properties.key.in(keys)) {
return true
}
return false
}
@discardableResult
func _dbDeleteItemWithKey(_ key: String) -> Bool {
if let _ = try? database.delete(fromTable: "XRStorageItemTable",
where: XRStorageItem.Properties.key == key) {
return true
}
return false
}
func _dbDeleteItemWithKeys(_ keys: [String]) -> Bool {
if let _ = try? database.delete(fromTable: "XRStorageItemTable",
where: XRStorageItem.Properties.key.in(keys)) {
return true
}
return false
}
func _dbDeleteItemsWithSizeLargerThan(_ size: Int) -> Bool {
if let _ = try? database.delete(fromTable: "XRStorageItemTable",
where: XRStorageItem.Properties.size > size) {
return true
}
return false
}
func _dbDeleteItemsWithTimeEarlierThan(_ time: Double) -> Bool {
if let _ = try? database.delete(fromTable: "XRStorageItemTable",
where: XRStorageItem.Properties.accessTime < time) {
return true
}
return false
}
func _dbGetItem(_ key: String) -> XRStorageItem? {
if let item: XRStorageItem = try? database.getObject(fromTable: "XRStorageItemTable", where: XRStorageItem.Properties.key == key) {
return item
}
return nil
}
func _dbGetItems(_ keys: [String]) -> [XRStorageItem]? {
if let items: [XRStorageItem] = try? database.getObjects(fromTable: "XRStorageItemTable", where: XRStorageItem.Properties.key.in(keys)) {
return items
}
return nil
}
func _dbGetKeysWithSizeLargerThan(_ size: Int) -> [String]? {
if let items: [XRStorageItem] = try? database.getObjects(fromTable: "XRStorageItemTable", where: XRStorageItem.Properties.size > size) {
return items.reduce([String]()) {
var keys = $0
if let key = $1.key {
keys.append(key)
}
return keys
}
}
return nil
}
func _dbGetKeysWithTimeEarlierThan(_ time: Double) -> [String]? {
if let items: [XRStorageItem] = try? database.getObjects(fromTable: "XRStorageItemTable", where: XRStorageItem.Properties.accessTime < time) {
return items.reduce([String]()) {
var keys = $0
if let key = $1.key {
keys.append(key)
}
return keys
}
}
return nil
}
func _dbGetItemSizeInfoOrderByTimeAscWithLimit(_ count: Int) -> [XRStorageItem]? {
if let items: [XRStorageItem] = try? database.getObjects(fromTable: "XRStorageItemTable", orderBy: [XRStorageItem.Properties.accessTime], limit: count) {
return items
}
return nil
}
func _dbGetItemCountWithKey(_ key: String) -> Int? {
// FIXME: - 暫時沒找到"select count(key) from manifest where key = ?1;"語句的對應(yīng)方法
if let items: [XRStorageItem] = try? database.getObjects(fromTable: "XRStorageItemTable", where: XRStorageItem.Properties.key == key) {
return items.count
}
return nil
}
func _dbGetTotalItemSize() -> Int? {
// FIXME: - 暫時沒找到"select sum(size) from manifest;"語句的對應(yīng)方法
if let items: [XRStorageItem] = try? database.getObjects(fromTable: "XRStorageItemTable") {
return items.reduce(0) { $0 + $1.size }
}
return nil
}
func _dbGetTotalItemCount() -> Int? {
// FIXME: - 暫時沒找到"select count(*) from manifest;"語句的對應(yīng)方法
if let items: [XRStorageItem] = try? database.getObjects(fromTable: "XRStorageItemTable") {
return items.count
}
return nil
}
}
XRFileManagerStorage
—— 文件存儲管理類
open class XRFileManagerStorage {
/// domain
public static let kDomain = "com.xr.disk.cache"
/// base 路徑(不允許修改)
public private(set) static var basePath: String = {
let cachesPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]
let pathComponent = XRFileManagerStorage.kDomain
let basePath = (cachesPath as NSString).appendingPathComponent(pathComponent)
return basePath
}()
/// 異步隊列
private var trashQueue = DispatchQueue(label: "com.xr.cache.disk.trash")
public let path: String
public init(path: String) {
self.path = path
}
}
// MARK: - public method
public extension XRFileManagerStorage {
func path(forKey key: String) -> String {
let filename = key.MD5Filename()
let keyPath = (self.path as NSString).appendingPathComponent(filename)
return keyPath
}
func setData( _ data: Data, key: String) -> Bool {
return setDataSync(data, key: key)
}
@discardableResult
func fetchData(_ key: String) -> Data? {
let path = self.path(forKey: key)
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: Data.ReadingOptions()) {
return data
}
return nil
}
func removeData(with key: String) {
let path = self.path(forKey: key)
self.removeFile(atPath: path)
}
func removeAllData(_ completion: (() -> ())? = nil) {
let fileManager = FileManager.default
let cachePath = self.path
trashQueue.async(execute: {
do {
let contents = try fileManager.contentsOfDirectory(atPath: cachePath)
for pathComponent in contents {
let path = (cachePath as NSString).appendingPathComponent(pathComponent)
do {
try fileManager.removeItem(atPath: path)
} catch {
XRCacheLog.error(message: "Failed to remove path \(path)", error: error)
}
}
} catch {
XRCacheLog.error(message: "Failed to list directory", error: error)
}
if let completion = completion {
DispatchQueue.main.async {
completion()
}
}
})
}
}
fileprivate extension XRFileManagerStorage {
func removeFile(atPath path: String) {
do {
try FileManager.default.removeItem(atPath: path)
} catch {
XRCacheLog.error(message: "Failed to remove file", error: error)
}
}
@discardableResult
func setDataSync(_ data: Data, key: String) -> Bool {
let path = self.path(forKey: key)
do {
try data.write(to: URL(fileURLWithPath: path), options: Data.WritingOptions.atomicWrite)
return true
} catch {
XRCacheLog.error(message: "Failed to write key \(key)", error: error)
}
return false
}
func isNoSuchFileError(_ error : Error?) -> Bool {
if let error = error {
return NSCocoaErrorDomain == (error as NSError).domain && (error as NSError).code == NSFileReadNoSuchFileError
}
return false
}
}
XRDiskCache
—— 磁盤緩存類
/// 磁盤緩存
public final class XRDiskCache {
/// cache 名
var name: String?
/// 最大緩存數(shù)量
var countLimit: UInt = UInt.max
/// 最大消耗
var costLimit: UInt = UInt.max
/// 最大到期時間
var ageLimit: Double = Double.greatestFiniteMagnitude
/// 緩存應(yīng)保留的最小可用磁盤空間
var freeDiskSpaceLimit: UInt = 0
/// 自動調(diào)整內(nèi)存時間間隔,60s,也就是1分鐘
/// 高速緩存具有內(nèi)部計時器肋层,以檢查高速緩存是否達(dá)到其限制蒸辆,如果達(dá)到限制,則開始逐出對象疯汁。
var autoTrimInterval: Double = 60
/// 總緩存數(shù)
var totalCount: Int {
lock.wait()
let count = kv.getItemsCount()
lock.signal()
return count ?? 0
}
/// 總消耗
var totalCost: Int {
lock.wait()
let cost = kv.getItemsSize()
lock.signal()
return cost ?? 0
}
/// 緩存路徑
private(set) var path: String!
/// storage
private lazy var kv: XRStorage = XRStorage()
/// 信號量
private var lock: DispatchSemaphore = DispatchSemaphore(value: 1)
/// 子線程
private var queue: DispatchQueue = DispatchQueue(label: "com.xr.cache.disk")
private init() {}
init(_ path: String) {
self.path = path
_trimRecursively()
}
// MARK: - public method
extension XRDiskCache {
func containsObjectForKey(_ key: String) -> Bool {
guard !key.isEmpty else { return false }
lock.wait()
let contains = kv.itemExists(key)
lock.signal()
return contains
}
func objectFor(_ key: String) -> Data? {
guard !key.isEmpty else { return nil }
lock.wait()
let item = kv.getItem(key)
lock.signal()
return item?.value
}
func setObject(_ object: Data, key: String, callback: (() -> ())? = nil) {
guard !key.isEmpty else { return }
guard !object.isEmpty else {
removeObject(key)
return
}
lock.wait()
kv.saveItem(key, value: object)
lock.signal()
}
func removeObject(_ key: String, callback: (() -> ())? = nil) {
guard !key.isEmpty else { return }
lock.wait()
kv.removeItem(key)
lock.signal()
}
func removeAllObjects(_ callback: (() -> ())? = nil) {
lock.wait()
kv.removeAllItems()
lock.signal()
}
}
// MARK: - XRCacheTrimProtocol
extension XRDiskCache: XRCacheTrimProtocol {
public func trimToCount(_ count: UInt, completion: (() -> Void)? = nil) {
if countLimit >= Int.max { return }
kv.removeItemsToFitCount(Int(count))
}
public func trimToCost(_ cost: UInt, completion: (() -> Void)? = nil) {
if countLimit >= Int.max { return }
}
public func trimToAge(_ age: Double, completion: (() -> Void)? = nil) {
if ageLimit <= 0 {
kv.removeAllItems()
return
}
let timestamp = time(nil)
if Double(timestamp) <= ageLimit { return }
let age = Double(timestamp) - ageLimit
if age >= Double(Int.max) { return }
kv.removeItemsEarlierThanTime(age)
}
}
// MARK: - private method
extension XRDiskCache {
/// 遞歸清理
func _trimRecursively() {
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + autoTrimInterval) { [weak self] in
self?._trimInBackground()
self?._trimRecursively()
}
}
func _trimInBackground() {
queue.async { [weak self] in
guard let `self` = self else { return }
self.lock.wait()
self.trimToCost(self.costLimit)
self.trimToCount(self.countLimit)
self.trimToAge(self.ageLimit)
self._trimToFreeDiskSpace()
self.lock.signal()
}
}
}
在磁盤緩存中,為保證線程安全,我們使用的鎖是DispatchSemaphore
陪腌,在作者的博客中,我們找到了答案:
- dispatch_semaphore 是信號量,但當(dāng)信號總量設(shè)為 1 時也可以當(dāng)作鎖來诗鸭。在沒有等待情況出現(xiàn)時染簇,它的性能比 pthread_mutex 還要高,但一旦有等待情況出現(xiàn)時强岸,性能就會下降許多锻弓。
- 相對于 OSSpinLock 來說,它的優(yōu)勢在于等待時不會消耗 CPU 資源蝌箍。對磁盤緩存來說青灼,它比較合適。
結(jié)語
再次膜拜大神的設(shè)計妓盲。
此次的Swfit
中杂拨,還存在一些遺留問題,后續(xù)優(yōu)化:
- 將內(nèi)存緩存和磁盤緩存同時使用本橙,發(fā)揮出最高性能的實現(xiàn)方式暫時沒有確定扳躬,設(shè)計一個類似
YYCache
將YYMemoryCache
與YYDiskCache
兩部分組合使用的類,或者用其他的方式來實現(xiàn)甚亭; - 磁盤緩存還有些不足贷币,待優(yōu)化。
- 自動抉擇每個緩存對象的存儲類型
(db/file)
以便提供更好的性能表現(xiàn)亏狰,后續(xù)支持?jǐn)?shù)據(jù)庫緩存較小的緩存對象役纹;
參考文檔
https://blog.ibireme.com/category/tec/ios-tec/
http://www.cocoachina.com/articles/20980
https://juejin.im/post/5a657a946fb9a01cb64ee761#heading-17
https://github.com/Tencent/wcdb
https://github.com/Haneke/HanekeSwift
http://www.reibang.com/p/2c3f304f7efd