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结构.png

YYCache中的类如图中所示,结构也非常清晰,职责也非常明确:

  • YYCache是由YYMemoryCacheYYDiskCache两部分组成的,其中 YYMemoryCache 作为高速内存缓存,而 YYDiskCache 则作为低速磁盘缓存;
  • YYMemoryCache负责处理容量小,相对高速的内存缓存;线程安全,支持自动和手动清理缓存等功能;
  • _YYLinkedMapYYMemoryCache使用的双向链表类;
  • _YYLinkedMapNode_YYLinkedMap使用的节点类;
  • YYDiskCache负责处理容量大,相对低速的磁盘缓存;线程安全,支持异步操作,自动和手动清理缓存等功能;
  • YYKVStorageYYDiskCache的底层实现类,用于管理磁盘缓存
  • YYKVStorageItem内置在YYKVStorage中,是YYKVStorage内部用于封装某个缓存的类。

Swift

本文的代码结构和职责拆分均完全参考YYCache的思路,只是语言使用使用Swfit,以及在具体实现上使用个人认为更优的方式。

Tips:将内存缓存和磁盘缓存同时使用,发挥出最高性能的实现方式暂时没有确定,所以并没有设计类似YYCacheYYMemoryCacheYYDiskCache两部分组合使用的类。组合使用会放到后续的优化中。

协议的定义

首先定义了两个协议:

  • 一个是缓存核心能力的协议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中,还存在一些遗留问题,后续优化:

  • 将内存缓存和磁盘缓存同时使用,发挥出最高性能的实现方式暂时没有确定,设计一个类似YYCacheYYMemoryCacheYYDiskCache两部分组合使用的类,或者用其他的方式来实现;
  • 磁盘缓存还有些不足,待优化。
  • 自动抉择每个缓存对象的存储类型(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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容