YYCache Swift化

在平時的開發(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).png

YYCache中的類如圖中所示梨撞,結(jié)構(gòu)也非常清晰雹洗,職責(zé)也非常明確:

  • YYCache是由YYMemoryCacheYYDiskCache兩部分組成的,其中 YYMemoryCache 作為高速內(nèi)存緩存卧波,而 YYDiskCache 則作為低速磁盤緩存时肿;
  • YYMemoryCache負(fù)責(zé)處理容量小,相對高速的內(nèi)存緩存港粱;線程安全螃成,支持自動和手動清理緩存等功能;
  • _YYLinkedMapYYMemoryCache使用的雙向鏈表類查坪;
  • _YYLinkedMapNode_YYLinkedMap使用的節(jié)點類寸宏;
  • YYDiskCache負(fù)責(zé)處理容量大,相對低速的磁盤緩存偿曙;線程安全氮凝,支持異步操作,自動和手動清理緩存等功能望忆;
  • YYKVStorageYYDiskCache的底層實現(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è)計類似YYCacheYYMemoryCacheYYDiskCache兩部分組合使用的類蕾羊。組合使用會放到后續(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è)計一個類似YYCacheYYMemoryCacheYYDiskCache兩部分組合使用的類,或者用其他的方式來實現(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市暇唾,隨后出現(xiàn)的幾起案子促脉,更是在濱河造成了極大的恐慌,老刑警劉巖策州,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瘸味,死亡現(xiàn)場離奇詭異,居然都是意外死亡够挂,警方通過查閱死者的電腦和手機(jī)旁仿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來孽糖,“玉大人枯冈,你說我怎么就攤上這事“煳颍” “怎么了尘奏?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長病蛉。 經(jīng)常有香客問我炫加,道長瑰煎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任琢感,我火速辦了婚禮丢间,結(jié)果婚禮上探熔,老公的妹妹穿的比我還像新娘驹针。我一直安慰自己,他們只是感情好诀艰,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布柬甥。 她就那樣靜靜地躺著,像睡著了一般其垄。 火紅的嫁衣襯著肌膚如雪苛蒲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天绿满,我揣著相機(jī)與錄音臂外,去河邊找鬼。 笑死喇颁,一個胖子當(dāng)著我的面吹牛漏健,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播橘霎,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蔫浆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了姐叁?” 一聲冷哼從身側(cè)響起瓦盛,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎外潜,沒想到半個月后原环,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡处窥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年嘱吗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碧库。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡柜与,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嵌灰,到底是詐尸還是另有隱情弄匕,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布沽瞭,位于F島的核電站迁匠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜城丧,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一延曙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧亡哄,春花似錦枝缔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至截型,卻和暖如春趴荸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背宦焦。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工发钝, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人波闹。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓酝豪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舔痪。 傳聞我的和親對象是個殘疾皇子寓调,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容