iOS緩存設(shè)計(YYCache思路)

iOS緩存設(shè)計(YYCache思路)

前言:
前段時間業(yè)務(wù)有緩存需求,于是結(jié)合YYCache和業(yè)務(wù)需求扒磁,做了緩存層(內(nèi)存&磁盤)+ 網(wǎng)絡(luò)層的方案嘗試
由于YYCache 采用了內(nèi)存緩存和磁盤緩存組合方式袱箱,性能優(yōu)良遏乔,這里拿它的原理來說下如何設(shè)計一套緩存的思路,并結(jié)合網(wǎng)絡(luò)整理一套完整流程

目錄

  • 初步認識緩存
  • 如何優(yōu)化緩存(YYCache設(shè)計思想)
  • 網(wǎng)絡(luò)和緩存同步流程

一发笔、初步認識緩存

1. 什么是緩存盟萨?

我們做一個緩存前,先了解它是什么了讨,緩存是本地數(shù)據(jù)存儲捻激,存儲方式主要包含兩種:磁盤儲存和內(nèi)存存儲

1.1 磁盤存儲

磁盤緩存制轰,磁盤也就是硬盤緩存,磁盤是程序的存儲空間胞谭,磁盤緩存容量大速度慢垃杖,磁盤是永久存儲東西的,iOS為不同數(shù)據(jù)管理對存儲路徑做了規(guī)范如下:
1丈屹、每一個應(yīng)用程序都會擁有一個應(yīng)用程序沙盒调俘。
2、應(yīng)用程序沙盒就是一個文件系統(tǒng)目錄旺垒。
沙盒根目錄結(jié)構(gòu):Documents彩库、Library、temp先蒋。

磁盤存儲方式主要有文件管理和數(shù)據(jù)庫侧巨,其特性:


image.png

1.2 內(nèi)存存儲

內(nèi)存緩存,內(nèi)存緩存是指當前程序運行空間鞭达,內(nèi)存緩存速度快容量小司忱,它是供cpu直接讀取,比如我們打開一個程序畴蹭,他是運行在內(nèi)存中的坦仍,關(guān)閉程序后內(nèi)存又會釋放。
iOS內(nèi)存分為5個區(qū):棧區(qū)叨襟,堆區(qū)繁扎,全局區(qū),常量區(qū)糊闽,代碼區(qū)

棧區(qū)stack:這一塊區(qū)域系統(tǒng)會自己管理梳玫,我們不用干預(yù),主要存一些局部變量右犹,以及函數(shù)跳轉(zhuǎn)時的現(xiàn)場保護提澎。因此大量的局部變量,深遞歸,函數(shù)循環(huán)調(diào)用都可能導(dǎo)致內(nèi)存耗盡而運行崩潰念链。
堆區(qū)heap:與棧區(qū)相對盼忌,這一塊一般由我們自己管理,比如alloc掂墓,free的操作谦纱,存儲一些自己創(chuàng)建的對象。
全局區(qū)(靜態(tài)區(qū)static):全局變量和靜態(tài)變量都存儲在這里君编,已經(jīng)初始化的和沒有初始化的會分開存儲在相鄰的區(qū)域跨嘉,程序結(jié)束后系統(tǒng)會釋放
常量區(qū):存儲常量字符串和const常量
代碼區(qū):存儲代碼

在程序中聲明的容器(數(shù)組 、字典)都可看做內(nèi)存中存儲吃嘿,特性如下:


image.png

2. 緩存做什么祠乃?

我們使用場景比如:離線加載窘游,預(yù)加載,本地通訊錄...等跳纳,對非網(wǎng)絡(luò)數(shù)據(jù)忍饰,使用本地數(shù)據(jù)管理的一種,具體使用場景有很多

3. 怎么做緩存寺庄?

簡單緩存可以僅使用磁盤存儲艾蓝,iOS主要提供四種磁盤存儲方式:

  • NSKeyedArchiver: 采用歸檔的形式來保存數(shù)據(jù), 該數(shù)據(jù)對象需要遵守NSCoding協(xié)議, 并且該對象對應(yīng)的類必須提供encodeWithCoder:和initWithCoder:方法.
//自定義Person實現(xiàn)歸檔解檔
//.h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject<NSCoding>
@property(nonatomic,copy) NSString * name;

@end

//.m文件
#import "Person.h"
@implementation Person
//歸檔要實現(xiàn)的協(xié)議方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:_name forKey:@"name"];
}
//解檔要實現(xiàn)的協(xié)議方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        _name = [aDecoder decodeObjectForKey:@"name"];
    }
    return self;
}
@end

使用歸檔解檔

  // 將數(shù)據(jù)存儲在path路徑下歸檔文件
  [NSKeyedArchiver archiveRootObject:p toFile:path];
  // 根據(jù)path路徑查找解檔文件
  Person *p = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

缺點:歸檔的形式來保存數(shù)據(jù),只能一次性歸檔保存以及一次性解壓。所以只能針對小量數(shù)據(jù),如果想改動數(shù)據(jù)的某一小部分,需要解壓整個數(shù)據(jù)或者歸檔整個數(shù)據(jù)斗塘。

  • NSUserDefaults: 用來保存應(yīng)用程序設(shè)置和屬性赢织、用戶保存的數(shù)據(jù)。用戶再次打開程序或開機后這些數(shù)據(jù)仍然存在馍盟。
    NSUserDefaults可以存儲的數(shù)據(jù)類型包括:NSData于置、NSString、NSNumber贞岭、NSDate八毯、NSArray、 NSDictionary瞄桨。
// 以鍵值方式存儲
  [[NSUserDefaults standardUserDefaults] setObject:@"value" forKey:@"key"];
// 以鍵值方式讀取
  [[NSUserDefaults standardUserDefaults] objectForKey:@"key"];
  • Write寫入方式:永久保存在磁盤中话速。具體方法為:
  //將NSData類型對象data寫入文件,文件名為FileName
  [data writeToFile:FileName atomically:YES];
  //從FileName中讀取出數(shù)據(jù)
  NSData  *data=[NSData dataWithContentsOfFile:FileName options:0 error:NULL];
  • SQLite:采用SQLite數(shù)據(jù)庫來存儲數(shù)據(jù)。SQLite作為?一中小型數(shù)據(jù)庫,應(yīng)用ios中跟其他三種保存方式相比,相對復(fù)雜一些
  //打開數(shù)據(jù)庫
  if (sqlite3_open([databaseFilePath UTF8String],   &database)==SQLITE_OK) {
      NSLog(@"sqlite dadabase is opened."); 
  } else { return;}//打開不成功就返回

  //在打開了數(shù)據(jù)庫的前提下,如果數(shù)據(jù)庫沒有表,那就開始建表了哦!
  char *error;
  const char *createSql="create table(id integer primary key autoincrement, name text)"; if (sqlite3_exec(database, createSql, NULL, NULL, &error)==SQLITE_OK) {
      NSLog(@"create table is ok."); 
  } else {
      sqlite3_free(error);//每次使用完畢清空error字符串,提供給下?一次使用 
  }

  // 建表完成之后, 插入記錄
  const char *insertSql="insert into a person (name) values(‘gg’)";
  if (sqlite3_exec(database, insertSql, NULL, NULL, &error)==SQLITE_OK) {
      NSLog(@"insert operation is ok."); 
  } else {
      sqlite3_free(error);//每次使用完畢清空error字符串,提供給下一次使用 
  }

上面提到的磁盤存儲特性芯侥,具備空間大泊交、可持久、但是讀取慢柱查,面對大量數(shù)據(jù)頻繁讀取時更加明顯廓俭,以往測試中磁盤讀取比內(nèi)存讀取保守測量低于幾十倍,那我們怎么解決磁盤讀取慢的缺點呢? 又如何利用內(nèi)存的優(yōu)勢呢唉工?

二研乒、 如何優(yōu)化緩存(YYCache設(shè)計思想)

YYCache背景知識:
源碼中由兩個主要類構(gòu)成

Snip20190107_2.png
  • YYMemoryCache (內(nèi)存緩存)
    操作YYLinkedMap中數(shù)據(jù), 為實現(xiàn)內(nèi)存優(yōu)化酵紫,采用雙向鏈表數(shù)據(jù)結(jié)構(gòu)實現(xiàn) LRU算法告嘲,YYLinkedMapItem 為每個子節(jié)點
  • YYDiskCache (磁盤緩存)
    不會直接操作緩存對象(sqlite/file),而是通過 YYKVStorage 來間接的操作緩存對象奖地。

容量管理:

  • ageLimit :時間周期限制,比如每天或每星期開始清理
  • costLimit: 容量限制赋焕,比如超出10M后開始清理內(nèi)存
  • countLimit : 數(shù)量限制参歹, 比如超出1000個數(shù)據(jù)就清理

這里借用YYCache設(shè)計, 來講述緩存優(yōu)化

1. 磁盤+內(nèi)存組合優(yōu)化

利用內(nèi)存和磁盤特性,融合各自優(yōu)點隆判,整合如下:

image.png
  • APP會優(yōu)先請求內(nèi)存緩沖中的資源
  • 如果內(nèi)存緩沖中有犬庇,則直接返回資源文件僧界, 如果沒有的話,則會請求資源文件臭挽,這時資源文件默認資源為本地磁盤存儲捂襟,需要操作文件系統(tǒng)或數(shù)據(jù)庫來獲取。
  • 獲取到的資源文件欢峰,先緩存到內(nèi)存緩存葬荷,方便以后不再重復(fù)獲取,節(jié)省時間纽帖。
    然后就是從緩存中取到數(shù)據(jù)然后給app使用宠漩。
    這樣就充分結(jié)合兩者特性,利用內(nèi)存讀取快特性減少讀取數(shù)據(jù)時間懊直,

YYCache 源碼解析

- (id<NSCoding>)objectForKey:(NSString *)key {
    // 1.如果內(nèi)存緩存中存在則返回數(shù)據(jù)
    id<NSCoding> object = [_memoryCache objectForKey:key];
    if (!object) {
        // 2.若不存在則查取磁盤緩存數(shù)據(jù)
        object = [_diskCache objectForKey:key];
        if (object) {
            // 3.并將數(shù)據(jù)保存到內(nèi)存中
            [_memoryCache setObject:object forKey:key];
        }
    }
    return object;
}

2. 內(nèi)存優(yōu)化-- 提高內(nèi)存命中率

但是我們想在基礎(chǔ)上再做優(yōu)化扒吁,比如想讓經(jīng)常訪問的數(shù)據(jù)保留在內(nèi)存中,提高內(nèi)存的命中率室囊,減少磁盤的讀取雕崩,那怎么做處理呢? -- LRU算法


LRU算法.png

LRU算法:我們可以將鏈表看成一串數(shù)據(jù)鏈融撞,每個數(shù)據(jù)是這個串上的一個節(jié)點晨逝,經(jīng)常訪問的數(shù)據(jù)移動到頭部,等數(shù)據(jù)超出容量后從鏈表后面的一些節(jié)點銷毀懦铺,這樣經(jīng)常訪問數(shù)據(jù)在頭部位置捉貌,還保留在內(nèi)存中。

鏈表實現(xiàn)結(jié)構(gòu)圖:

_LinkedMap.jpg

YYCache 源碼解析

/**
 A node in linked map.
 Typically, you should not use this class directly.
 */
@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}
@end
@implementation _YYLinkedMapNode
@end
/**
 A linked map used by YYMemoryCache.
 It's not thread-safe and does not validate the parameters.
 Typically, you should not use this class directly.
 */
@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; // do not set object directly
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head; // MRU, do not change it directly
    _YYLinkedMapNode *_tail; // LRU, do not change it directly
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}

/// Insert a node at head and update the total cost.
/// Node and node.key should not be nil.
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

/// Bring a inner node to header.
/// Node should already inside the dic.
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

/// Remove a inner node and update the total cost.
/// Node should already inside the dic.
- (void)removeNode:(_YYLinkedMapNode *)node;

/// Remove tail node if exist.
- (_YYLinkedMapNode *)removeTailNode;

/// Remove all node in background queue.
- (void)removeAll;

@end

_YYLinkedMapNode *_prev 為該節(jié)點的頭指針冬念,指向前一個節(jié)點
_YYLinkedMapNode *_next為該節(jié)點的尾指針趁窃,指向下一個節(jié)點
頭指針和尾指針將一個個子節(jié)點串連起來,形成雙向鏈表

來看下bringNodeToHead:的源碼實現(xiàn)急前,它是實現(xiàn)LRU算法主要方法醒陆,移動node子結(jié)點到鏈頭。

(詳細已注釋在代碼中)

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    if (_head == node) return; // 如果當前節(jié)點是鏈頭裆针,則不需要移動
    
    // 鏈表中存了兩個指向鏈頭(_head)和鏈尾(_tail)的指針刨摩,便于鏈表訪問
    if (_tail == node) {
        _tail = node->_prev; // 若當前節(jié)點為鏈尾,則更新鏈尾指針
        _tail->_next = nil; // 鏈尾的尾節(jié)點這里設(shè)置為nil
    } else {
        // 比如:A B C 鏈表, 將 B拿走世吨,將A C重新聯(lián)系起來
        node->_next->_prev = node->_prev; // 將node的下一個節(jié)點的頭指針指向node的上一個節(jié)點澡刹,
        node->_prev->_next = node->_next; // 將node的上一個節(jié)點的尾指針指向node的下一個節(jié)點
    }
    node->_next = _head; // 將當前node節(jié)點的尾指針指向之前的鏈頭,因為此時node為最新的第一個節(jié)點
    node->_prev = nil; // 鏈頭的頭節(jié)點這里設(shè)置為nil
    _head->_prev = node; // 之前的_head將為第二個節(jié)點
    _head = node; // 當前node成為新的_head
}

其他方法就不挨個舉例了耘婚,具體可翻看源碼罢浇,這些代碼結(jié)構(gòu)清晰,類和函數(shù)遵循單一職責,接口高內(nèi)聚嚷闭,低耦合攒岛,是個不錯的學(xué)習(xí)示例!

3. 磁盤優(yōu)化 - 數(shù)據(jù)分類存儲

YYDiskCache 是一個線程安全的磁盤緩存胞锰,基于 sqlite 和 file 來做的磁盤緩存灾锯,我們的緩存對象可以自由的選擇存儲類型,
下面簡單對比一下:

  • sqlite: 對于小數(shù)據(jù)(例如 NSNumber)的存取效率明顯高于 file嗅榕。
  • file: 對于較大數(shù)據(jù)(例如高質(zhì)量圖片)的存取效率優(yōu)于 sqlite顺饮。

所以 YYDiskCache 使用兩者配合,靈活的存儲以提高性能誊册。

另外:
YYDiskCache 具有以下功能:

  • 它使用 LRU(least-recently-used) 來刪除對象领突。
  • 支持按 cost,count 和 age 進行控制案怯。
  • 它可以被配置為當沒有可用的磁盤空間時自動驅(qū)逐緩存對象君旦。
  • 它可以自動抉擇每個緩存對象的存儲類型(sqlite/file)以便提供更好的性能表現(xiàn)。

YYCache源碼解析

// YYKVStorageItem 是 YYKVStorage 中用來存儲鍵值對和元數(shù)據(jù)的類
// 通常情況下嘲碱,我們不應(yīng)該直接使用這個類
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key;                ///< key
@property (nonatomic, strong) NSData *value;                ///< value
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
@property (nonatomic) int size;                             ///< value's size in bytes
@property (nonatomic) int modTime;                          ///< modification unix timestamp
@property (nonatomic) int accessTime;                       ///< last access unix timestamp
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end
 
 
/**
 YYKVStorage 是基于 sqlite 和文件系統(tǒng)的鍵值存儲金砍。
 通常情況下,我們不應(yīng)該直接使用這個類麦锯。
  
 @warning 
  這個類的實例是 *非* 線程安全的恕稠,你需要確保
  只有一個線程可以同時訪問該實例。如果你真的
  需要在多線程中處理大量的數(shù)據(jù)扶欣,應(yīng)該分割數(shù)據(jù)
  到多個 KVStorage 實例(分片)鹅巍。
 */
@interface YYKVStorage : NSObject
 
#pragma mark - Attribute
@property (nonatomic, readonly) NSString *path;        /// storage 路徑
@property (nonatomic, readonly) YYKVStorageType type;  /// storage 類型
@property (nonatomic) BOOL errorLogsEnabled;           /// 是否開啟錯誤日志
 
#pragma mark - Initializer
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;
 
#pragma mark - Save Items
- (BOOL)saveItem:(YYKVStorageItem *)item;
...
 
#pragma mark - Remove Items
- (BOOL)removeItemForKey:(NSString *)key;
...
 
#pragma mark - Get Items
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
...
 
#pragma mark - Get Storage Status
- (BOOL)itemExistsForKey:(NSString *)key;
- (int)getItemsCount;
- (int)getItemsSize;
 
@end

我們只需要看一下 YYKVStorageType 這個枚舉,它決定著 YYKVStorage 的存儲類型料祠。

YYKVStorageType

/**
 存儲類型骆捧,指示“YYKVStorageItem.value”存儲在哪里。
  
 @discussion
  通常髓绽,將數(shù)據(jù)寫入 sqlite 比外部文件更快敛苇,但是
  讀取性能取決于數(shù)據(jù)大小。在測試環(huán)境 iPhone 6s 64G顺呕,
  當數(shù)據(jù)較大(超過 20KB)時從外部文件讀取數(shù)據(jù)比 sqlite 更快枫攀。
 */
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    YYKVStorageTypeFile = 0, // value 以文件的形式存儲于文件系統(tǒng)
    YYKVStorageTypeSQLite = 1, // value 以二進制形式存儲于 sqlite
    YYKVStorageTypeMixed = 2, // value 將根據(jù)你的選擇基于上面兩種形式混合存儲
};

總結(jié):

這里說了YYCache幾個主要設(shè)計優(yōu)化之處,其實細節(jié)上也有很多不錯的處理株茶,比如:

  • 線程安全
    如果說 YYCache 這個類是一個純邏輯層的緩存類(指 YYCache 的接口實現(xiàn)全部是調(diào)用其他類完成)来涨,那么 YYMemoryCache 與 YYDiskCache 還是做了一些事情的(并沒有 YYCache 當甩手掌柜那么輕松),其中最顯而易見的就是 YYMemoryCache 與 YYDiskCache 為 YYCache 保證了線程安全忌卤。
    YYMemoryCache 使用了 pthread_mutex 線程鎖來確保線程安全扫夜,而 YYDiskCache 則選擇了更適合它的 dispatch_semaphore,上文已經(jīng)給出了作者選擇這些鎖的原因驰徊。

  • 性能

YYCache 中對于性能提升的實現(xiàn)細節(jié):

  1. 異步釋放緩存對象
  2. 鎖的選擇
  3. 使用 NSMapTable 單例管理的 YYDiskCache
  4. YYKVStorage 中的 _dbStmtCache
  5. 甚至使用 CoreFoundation 來換取微乎其微的性能提升

3. 網(wǎng)絡(luò)和緩存同步流程

結(jié)合網(wǎng)絡(luò)層和緩存層笤闯,設(shè)計了一套接口緩存方式,比較靈活且速度得到提升; 比如首頁界面可能由多個接口提供數(shù)據(jù)棍厂,沒有采用整塊存儲而是將存儲細分到每個接口中颗味,有API接口控制,基本結(jié)構(gòu)如下:

主要分為:

  • 應(yīng)用層 :顯示數(shù)據(jù)
  • 管理層: 管理網(wǎng)絡(luò)層和緩存層牺弹,為應(yīng)用層提供數(shù)據(jù)支持
  • 網(wǎng)絡(luò)層: 請求網(wǎng)絡(luò)數(shù)據(jù)
  • 緩存層: 緩存數(shù)據(jù)

層級圖:

緩存流程.png
  1. 服務(wù)端每套數(shù)據(jù)對應(yīng)一個version (或時間戳)浦马,若后臺數(shù)據(jù)發(fā)生變更,則version發(fā)生變化张漂,在返回客戶端數(shù)據(jù)時并將version一并返回晶默。
  2. 當客戶端請求網(wǎng)絡(luò)時,將本地上一次數(shù)據(jù)對應(yīng)version上傳航攒。
  3. 服務(wù)端獲取客戶端傳來得version后磺陡,與最新的version進行對比,若version不一致漠畜,則返回最新數(shù)據(jù)币他,若未發(fā)生變化,服務(wù)端不需要返回全部數(shù)據(jù)只需返回304(No Modify) 狀態(tài)值
  4. 客戶端接到服務(wù)端返回數(shù)據(jù)憔狞,若返回全部數(shù)據(jù)非304蝴悉,客戶端則將最新數(shù)據(jù)同步到本地緩存中;客戶端若接到304狀態(tài)值后瘾敢,表示服務(wù)端數(shù)據(jù)和本地數(shù)據(jù)一致拍冠,直接從緩存中獲取顯示
    這也是ETag的大致流程;詳細可以查看 https://baike.baidu.com/item/ETag/4419019?fr=aladdin

源碼示例

- (void)getDataWithPage:(NSNumber *)page pageSize:(NSNumber *)pageSize option:(DataSourceOption)option completion:(void (^)(HomePageListCardModel * _Nullable, NSError * _Nullable))completionBlock {
    NSString *cacheKey = CacheKey(currentUser.userId, PlatIndexRecommendation);// 全局靜態(tài)常量 (userid + apiName)
   // 根據(jù)需求而定是否需要緩存方式簇抵,網(wǎng)絡(luò)方式走304邏輯
    switch (option) {
        case DataSourceCache:
        {
            if ([_cache containsObjectForKey:cacheKey]) {
                completionBlock((HomePageListCardModel *)[self->_cache objectForKey:cacheKey], nil);
            } else {
                completionBlock(nil, LJDError(400, @"緩存中不存在"));
            }
        }
            break;
        case DataSourceNetwork:
        {
            [NetWorkServer requestDataWithPage:page pageSize:pageSize completion:^(id _Nullable responseObject, NSError * _Nullable error) {
                if (responseObject && !error) {
                    HomePageListCardModel *model = [HomePageListCardModel yy_modelWithJSON:responseObject];
                    if (model.errnonumber == 304) { //取緩存數(shù)據(jù)
                        completionBlock((HomePageListCardModel *)[self->_cache objectForKey:cacheKey], nil);
                    } else {
                        completionBlock(model, error);
                        [self->_cache setObject:model forKey:cacheKey]; //保存到緩存中
                    }
                } else {
                    completionBlock(nil, error);
                }
            }];
        }
            break;
             
        default:
            break;
    }
}

這樣做好處:

  • 對于不頻繁更新數(shù)據(jù)的接口庆杜,節(jié)省了大量JSON數(shù)據(jù)轉(zhuǎn)化時間
  • 節(jié)約流量,節(jié)省加載時長
  • 用戶界面顯示加快

總結(jié):項目中并不一定完全這樣做正压,有時候過渡設(shè)計也是一種浪費欣福,多了解其他設(shè)計思路后,針對項目找到適合的才是最好的焦履!

參考文獻:
YYCache: https://github.com/ibireme/YYCache
YYCache 設(shè)計思路 :https://blog.ibireme.com/2015/10/26/yycache/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拓劝,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子嘉裤,更是在濱河造成了極大的恐慌郑临,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件屑宠,死亡現(xiàn)場離奇詭異厢洞,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門躺翻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丧叽,“玉大人,你說我怎么就攤上這事公你∮淮荆” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵陕靠,是天一觀的道長迂尝。 經(jīng)常有香客問我,道長剪芥,這世上最難降的妖魔是什么垄开? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮税肪,結(jié)果婚禮上溉躲,老公的妹妹穿的比我還像新娘。我一直安慰自己寸认,他們只是感情好签财,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著偏塞,像睡著了一般唱蒸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上灸叼,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天神汹,我揣著相機與錄音,去河邊找鬼古今。 笑死屁魏,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的捉腥。 我是一名探鬼主播氓拼,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抵碟!你這毒婦竟也來了桃漾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拟逮,失蹤者是張志新(化名)和其女友劉穎撬统,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體敦迄,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡恋追,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年凭迹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苦囱。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡嗅绸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出沿彭,到底是詐尸還是另有隱情朽砰,我是刑警寧澤尖滚,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布喉刘,位于F島的核電站,受9級特大地震影響漆弄,放射性物質(zhì)發(fā)生泄漏睦裳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一撼唾、第九天 我趴在偏房一處隱蔽的房頂上張望廉邑。 院中可真熱鬧,春花似錦倒谷、人聲如沸蛛蒙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牵祟。三九已至,卻和暖如春抖格,著一層夾襖步出監(jiān)牢的瞬間诺苹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工雹拄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留收奔,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓滓玖,卻偏偏與公主長得像坪哄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子势篡,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355