YYCache Swift化
在平时的开发中,总是会用到各种缓存的,也常使用各种三方的库,在这些三方的缓存库中,首屈一指的就数国内大神@ibireme
造的轮子YYCache
,这是一个非常优秀的缓存库,性能高、线程安全和代码质量高。
具体的设计思路和源码分析,网络上有大量优秀的文章,这里不赘述,这里提供几篇参考文档:
https://blog.ibireme.com/category/tec/ios-tec/
http://www.cocoachina.com/articles/20980
https://juejin.im/post/5a657a946fb9a01cb64ee761#heading-17
但YYCache
是一个Objective-C
的轮子,本着学习的心态,参照优秀的代码尝试实现了一个Swift
的缓存库。
YYCache
代码结构
首先我们来看一下YYCache
的代码结构:
YYCache
中的类如图中所示,结构也非常清晰,职责也非常明确:
-
YYCache
是由YYMemoryCache
与YYDiskCache
两部分组成的,其中 YYMemoryCache 作为高速内存缓存,而 YYDiskCache 则作为低速磁盘缓存; -
YYMemoryCache
负责处理容量小,相对高速的内存缓存;线程安全,支持自动和手动清理缓存等功能; -
_YYLinkedMap
是YYMemoryCache
使用的双向链表类; -
_YYLinkedMapNode
是_YYLinkedMap
使用的节点类; -
YYDiskCache
负责处理容量大,相对低速的磁盘缓存;线程安全,支持异步操作,自动和手动清理缓存等功能; -
YYKVStorage
是YYDiskCache
的底层实现类,用于管理磁盘缓存 -
YYKVStorageItem
内置在YYKVStorage
中,是YYKVStorage
内部用于封装某个缓存的类。
Swift
化
本文的代码结构和职责拆分均完全参考YYCache
的思路,只是语言使用使用Swfit
,以及在具体实现上使用个人认为更优的方式。
Tips:将内存缓存和磁盘缓存同时使用,发挥出最高性能的实现方式暂时没有确定,所以并没有设计类似YYCache
将YYMemoryCache
与YYDiskCache
两部分组合使用的类。组合使用会放到后续的优化中。
协议的定义
首先定义了两个协议:
- 一个是缓存核心能力的协议
XRCacheProtocol
/// cache protocol
public protocol XRCacheProtocol {
associatedtype Element
// 获取缓存
func get(_ key: String) -> Element?
// 设置缓存
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: 默认实现
public extension XRCacheProtocol {
func containsObjectForKey(_ key: String) -> Bool {
if let _ = get(key) { return true }
return false
}
}
- 一个是内存修剪的协议
XRCacheTrimProtocol
/// cache trim protocol
public protocol XRCacheTrimProtocol {
// 按数量清理
func trimToCount(_ count: UInt, completion: (() -> Void)?)
// 按消耗清理
func trimToCost(_ cost: UInt, completion: (() -> Void)?)
// 按时间清理
func trimToAge(_ age: Double, completion: (() -> Void)?)
}
内存缓存
YYMemoryCache
是一个线程安全及实现了LRU
淘汰算法的高效缓存,我们这里同样以此为目标。
同样的,我们不直接操作缓存对应,使用双向链表和字典来间接操作缓存对象,这么做的好处是同时发挥双向链表的增删改的效率优势和字典的读取效率优势,这也是为什么YYMemoryCache
会非常高效。
链表节点
首先定义链表的节点:
/// 内部节点
fileprivate class _XRLinkedNode<E> where E: Equatable {
/// 缓存key
var _key: String?
/// key对应值
var _value: E?
/// 上一个节点
weak var _prev: _XRLinkedNode<E>?
/// 下一个节点
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
}
}
在对节点的处理过程中,会使用==
的比较,所以这里让_XRLinkedNode
实现Equatable
的协议,方便后续的操作:
// MARK: - Equatable
extension _XRLinkedNode: Equatable {
static func == (lhs: _XRLinkedNode<E>, rhs: _XRLinkedNode<E>) -> Bool {
return (lhs._key == rhs._key && lhs._value == rhs._value)
}
}
双向链表
然后是双线链表的实现:
/// 双向链表
fileprivate class _XRLinkedList<E> where E: Equatable {
/// 存放节点 dict
fileprivate var _dic: [String: _XRLinkedNode<E>] = [:]
/// 总开销
fileprivate var _totalCost: UInt = 0
/// 节点总数
fileprivate var _totalCount: UInt = 0
/// 是否在主线程释放,默认为false
fileprivate var _releaseOnMainThread: Bool = false
/// 是否在子线程释放,默认为true
fileprivate var _releaseAsynchronously: Bool = true
/// 首个节点
private var _head: _XRLinkedNode<E>?
/// 最后节点
fileprivate var _tail: _XRLinkedNode<E>?
}
// MARK: - public method
fileprivate extension _XRLinkedList {
/// 添加节点到头部
/// - Parameter node: 节点
func insertNodeAtHead(_ node: _XRLinkedNode<E>) {
guard let k = node._key else { return }
_dic[k] = node
if let _ = _head { // 存在头部节点
node._next = _head
_head?._prev = node
_head = node
} else {
_head = node
_tail = node
}
_totalCost += node._cost
_totalCount += 1
}
/// 将节点移动到头部
/// - Parameter node: 节点
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
}
/// 移除节点
/// - Parameter node: 节点
func removeNode(_ node: _XRLinkedNode<E>) {
guard let k = node._key else { return }
_dic.removeValue(forKey: k)
_totalCost -= node._cost
_totalCount -= 1
// 存在下一个节点
if let _ = node._next {
node._next?._prev = node._prev
}
// 存在上一个节点
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
}
}
/// 移除尾节点
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 { // 只有一个节点
_head = nil
_tail = nil
} else {
_tail = tail._prev
_tail?._next = nil
}
return tail
}
/// 移除所有节点
func removeAll() {
_totalCost = 0
_totalCount = 0
_head = nil
_tail = nil
// 存在节点时
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 { // 主线程释放,且当前处于主线程
DispatchQueue.main.async {
temp.removeAll()
}
} else {
temp.removeAll()
}
}
}
}
双向链表的实现,有两个点说明一下:
-
1.这是针对缓存业务定制的双向链表,链表的操作思路是一样的,并不是完整的链表
如果对
Swift
版本的完整链表有兴趣,可以参考Swift链表。 -
2.异步释放的技巧
let queue = _releaseOnMainThread ? DispatchQueue.main : DispatchQueue.global(qos: .background) queue.async { temp.removeAll() }
这个技巧
ibireme
在他的另一篇文章 iOS 保持界面流畅的技巧 中有提及:Note: 对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。
内存缓存
内存缓存,主体定义与YYCache
一致:
/// 内存缓存
public final class XRMemoryCache<Element: Equatable> {
/// 缓存数量
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
}
/// 是否在主线程释放,默认为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
}
}
/// 是否在子线程释放,默认为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?
/// 最大缓存数量
var countLimit: UInt = UInt.max
/// 最大消耗
var costLimit: UInt = UInt.max
/// 最大到期时间
var ageLimit: Double = Double.greatestFiniteMagnitude
/// 自动调整检查时间间隔,默认5.0
var autoTrimInterval: Double = 5.0
/// 接收到内存警告时,是否移除所有缓存,默认true
var shouldRemoveAllObjectsOnMemoryWarning: Bool = true
/// 切换到后台,是否移除所有缓存,默认true
var shouldRemoveAllObjectsWhenEnteringBackground: Bool = true
/// 接收到内存警告回调
var didReceiveMemoryWarningBlock: ((_ cache: XRMemoryCache) -> ())?
/// 切换到后台回调
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) }
}
}
然后就是缓存功能协议的实现:
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()
// 缓存节点存在时,更新时间和消耗,并将节点提到头部
if let node = currentNode {
_lru._totalCost -= node._cost
_lru._totalCost += cost
node._cost = cost
node._time = now
_lru.bringNodeToHead(node)
} else { // 不存在时,则new一个节点放到头部
let node = _XRLinkedNode(key: key, value: value, prev: nil, next: nil, cost: cost, time: now)
_lru.insertNodeAtHead(node)
}
// 总消耗大于限制的阈值时,做内存修剪
if _lru._totalCost > costLimit {
_queue.async {
self.trimToCost(self.costLimit)
}
}
// 总数量大于限制的阈值时,做内存修剪
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: - 提供便利的下标方法
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)
}
}
}
}
然后就是缓存裁剪协议的实现:
// 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),贡献出了高线程的优先来避免优先级反转的问题,但是我翻了下 libdispatch 的源码倒是没发现相关逻辑,也可能是我忽略了什么。在我的一些测试中,OSSpinLock 和 dispatch_semaphore 都不会产生特别明显的死锁,所以我也无法确定用 dispatch_semaphore 代替 OSSpinLock 是否正确。能够肯定的是,用 pthread_mutex 是安全的。
2.LRU
算法的实现
什么是LRU
的话,就自行百度了哈。
双向链表中有头结点和尾节点:
- 头结点 = 链表中用户最近一次使用(访问)的缓存对象节点,MRU;
- 尾节点 = 链表中用户已经很久没有再次使用(访问)的缓存对象节点,LRU。
如何让头结点和尾节点指向我们想指向的缓存对象节点?参考大神的思路实现如下:
-
当访问一个已有的缓存时,要把这个缓存节点移动到链表头部,原位置两侧的缓存要接上,并且原链表头部的缓存节点要变成现在链表的第二个缓存节点;
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() // 缓存节点存在时,更新时间和消耗,并将节点提到头部 if let node = currentNode { _lru._totalCost -= node._cost _lru._totalCost += cost node._cost = cost node._time = now _lru.bringNodeToHead(node) } else { // 不存在时,则new一个节点放到头部 let node = _XRLinkedNode(key: key, value: value, prev: nil, next: nil, cost: cost, time: now) _lru.insertNodeAtHead(node) } // 总消耗大于限制的阈值时,做内存修剪 if _lru._totalCost > costLimit { _queue.async { self.trimToCost(self.costLimit) } } // 总数量大于限制的阈值时,做内存修剪 if _lru._totalCount > countLimit { let node = _lru.removeTailNode() _holdAndreleaseNode(node) } pthread_mutex_unlock(&_lock) }
-
在资源不足时,从双线链表的尾节点(LRU)开始清理缓存,释放资源,这里只拿消耗(cost)举例,数量(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 { // 尝试加锁,如果加成功,则执行后面逻辑 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 { // 在当前队列中排队等待并释放 _ = holder.count } } }
磁盘缓存
YYDiskCache
是一个线程安全的磁盘缓存,用于存储由SQLite
和文件系统支持的键值对(类似于NSURLCache
的磁盘缓存)。
- 使用
LRU(least-recently-used)
来裁剪缓存; - 支持按 cost,count 和 age 进行控制;
- 可以被配置为当没有可用的磁盘空间时自动驱逐缓存对象;
- 可以自动抉择每个缓存对象的存储类型
(sqlite/file)
以便提供更好的性能表现
主体思路还是按大神的设计,Swift
下的数据库,我选择使用了微信团队开源的WCDB
,相对于sqlite
,具备如下优势:
-
易用,WCDB支持一句代码即可将数据取出并组合为object;
-
通过WINQ,开发者无须为了拼接SQL的字符串而写一大坨胶水代码;
-
高效,WCDB通过框架层和sqlcipher源码优化,使其更高效的表现;
-
ORM(Object Relational Mapping):在WCDB内,ORM(Object Relational Mapping)是指:
将一个ObjC的类,映射到数据库的表和索引;
将类的property,映射到数据库表的字段; -
多线程高并发:WCDB支持多线程读与读、读与写并发执行,写与写串行执行。
Tips:自动抉择每个缓存对象的存储类型(sqlite/file)
以便提供更好的性能表现,这点暂时没有实现,目前数据库没有存储缓存对象,该店后续优化。
然后将磁盘存储类拆了两个,一个负责磁盘存储,一个负责数据库存储。
整体的设计思路与内存缓存类似,这里直接就上代码了:
XRStorageItem
—— 数据库存储最小单元
/// 磁盘 缓存 数据模型(用来做淘汰算法)
public class XRStorageItem: TableCodable {
/// 键
var key: String?
/// 文件大小,单位为 byte
var size: Int = 0
/// 修改的时间戳
var modTime: Double = 0
/// 最后访问的时间戳
var accessTime: Double = 0
/// 缓存二进制数据(仅作为数据存储,不存DB)
var value: Data?
/// WCDB协议 实现
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为主键,非空,默认值为""
return [key: ColumnConstraintBinding(isPrimary: true, isNotNull: true, defaultTo: "")]
}
}
}
该类用于数据库的存储,所以遵循了WCDB
的协议,关于WCDB
可以到WCDB获取源码和文档。
XRStorage
—— 数据库存储管理类
/// 存储类
public class XRStorage {
/// 数据库
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: 构造函数
/// 构造函数
/// - 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()
/// 数据库
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;"语句的对应方法
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;"语句的对应方法
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;"语句的对应方法
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?
/// 最大缓存数量
var countLimit: UInt = UInt.max
/// 最大消耗
var costLimit: UInt = UInt.max
/// 最大到期时间
var ageLimit: Double = Double.greatestFiniteMagnitude
/// 缓存应保留的最小可用磁盘空间
var freeDiskSpaceLimit: UInt = 0
/// 自动调整内存时间间隔,60s,也就是1分钟
/// 高速缓存具有内部计时器,以检查高速缓存是否达到其限制,如果达到限制,则开始逐出对象。
var autoTrimInterval: Double = 60
/// 总缓存数
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 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。
- 相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。
结语
再次膜拜大神的设计。
此次的Swfit
中,还存在一些遗留问题,后续优化:
- 将内存缓存和磁盘缓存同时使用,发挥出最高性能的实现方式暂时没有确定,设计一个类似
YYCache
将YYMemoryCache
与YYDiskCache
两部分组合使用的类,或者用其他的方式来实现; - 磁盘缓存还有些不足,待优化。
- 自动抉择每个缓存对象的存储类型
(db/file)
以便提供更好的性能表现,后续支持数据库缓存较小的缓存对象;
参考文档
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
https://www.jianshu.com/p/2c3f304f7efd