iOS源码解析—YYCache(YYDiskCache)

概述

上一篇主要讲解了YYMemoryCache的文件结构,分析了YYMemoryCache类的相关方法,本章主要分析硬盘缓存类YYDiskCache。YYDiskCache通过文件和SQLite数据库两种方式存储缓存数据。YYKVStorage核心功能类,实现了文件读写和数据库读写的功能。

YYKVStorage

YYKVStorage定义了读写缓存数据的三种枚举类型,即

typedef NS_ENUM(NSUInteger, YYKVStorageType) {
    //文件读取
    YYKVStorageTypeFile = 0,
    //数据库读写
    YYKVStorageTypeSQLite = 1,
    //根据策略决定使用文件还是数据库读写数据
    YYKVStorageTypeMixed = 2,
};

由于读写数据的方式不同,YYKVStorage分别实现了数据库和文件的读写方式,下面分析主要方法。

初始化

调用initWithPath: type:方法进行初始化,指定了存储方式,创建了缓存文件夹和SQLite数据库用于存放缓存,打开并初始化数据库。下面是部分代码注释:

- (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type {
    ...
    self = [super init];
    _path = path.copy;
    _type = type; //指定存储方式,是数据库还是文件存储
    _dataPath = [path stringByAppendingPathComponent:kDataDirectoryName]; //缓存数据的文件路径
    _trashPath = [path stringByAppendingPathComponent:kTrashDirectoryName]; //存放垃圾缓存数据的文件路径
    _trashQueue = dispatch_queue_create("com.ibireme.cache.disk.trash", DISPATCH_QUEUE_SERIAL);
    _dbPath = [path stringByAppendingPathComponent:kDBFileName]; //数据库路径
    _errorLogsEnabled = YES;
    NSError *error = nil;
    //创建缓存数据的文件夹和垃圾缓存数据的文件夹
    if (![[NSFileManager defaultManager] createDirectoryAtPath:path
                                   withIntermediateDirectories:YES
                                                    attributes:nil
                                                         error:&error] ||
        ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kDataDirectoryName]
                                   withIntermediateDirectories:YES
                                                    attributes:nil
                                                         error:&error] ||
        ![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kTrashDirectoryName]
                                   withIntermediateDirectories:YES
                                                    attributes:nil
                                                         error:&error]) {
        NSLog(@"YYKVStorage init error:%@", error);
        return nil;
    }
    //创建并打开数据库、在数据库中建表
    if (![self _dbOpen] || ![self _dbInitialize]) {
        // db file may broken...
        [self _dbClose];
        [self _reset]; // rebuild
        if (![self _dbOpen] || ![self _dbInitialize]) {
            [self _dbClose];
            NSLog(@"YYKVStorage init error: fail to open sqlite db.");
            return nil;
        }
    }
    [self _fileEmptyTrashInBackground]; // empty the trash if failed at last time
    return self;
}

_dbInitialize方法调用sql语句在数据库中创建一张表,代码如下:

- (BOOL)_dbInitialize {
    NSString *sql = @"pragma journal_mode = wal; pragma synchronous = normal; create table if not exists manifest (key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob, primary key(key)); create index if not exists last_access_time_idx on manifest(last_access_time);";
    return [self _dbExecute:sql];
}

"pragma journal_mode = wal"表示使用WAL模式进行数据库操作,如果不指定,默认DELETE模式,是"journal_mode=DELETE"。使用WAL模式时,改写操作数据库的操作会先写入WAL文件,而暂时不改动数据库文件,当执行checkPoint方法时,WAL文件的内容被批量写入数据库。checkPoint操作会自动执行,也可以改为手动。WAL模式的优点是支持读写并发,性能更高,但是当wal文件很大时,需要调用checkPoint方法清空wal文件中的内容。关于WAL模式,可以参考这篇文章

dataPath和trashPath用于文件的方式读写缓存数据,当dataPath中的部分缓存数据需要被清除时,先将其移至trashPath中,然后统一清空trashPath中的数据,类似回收站的思路。_dbPath是数据库文件,需要创建并初始化,下面是路径:

3-1.png

调用_dbOpen方法创建和打开数据库manifest.sqlite,调用_dbInitialize方法创建数据库中的表。调用_fileEmptyTrashInBackground方法将trash目录中的缓存数据删除。

YYKVStorageItem

YYKVStorageItem封装了每次写入硬盘的数据,代码如下:

@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; //缓存数据的key
@property (nonatomic, strong) NSData *value; //缓存数据的value
@property (nullable, nonatomic, strong) NSString *filename; //缓存文件名(文件缓存时有用)
@property (nonatomic) int size; //数据大小
@property (nonatomic) int modTime; //数据修改时间(用于更新相同key的缓存)
@property (nonatomic) int accessTime; //数据访问时间
@property (nullable, nonatomic, strong) NSData *extendedData; //附加数据
@end

缓存数据是按一条记录的格式存入数据库的,这条SQL记录包含的字段如下:

key(键)、fileName(文件名)、size(大小)、inline_data(value/二进制数据)、modification_time(修改时间)、last_access_time(最后访问时间)、extended_data(附加数据)

描述了这条缓存数据的相关信息,对应YYKVStorageItem对象的各个属性。

写入缓存数据

通过saveItemWithKey: value: filename: extendedData:方法将缓存数据写入硬盘,代码注释如下:

- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
    if (key.length == 0 || value.length == 0) return NO;
    if (_type == YYKVStorageTypeFile && filename.length == 0) {
        return NO;
    }
    //如果有文件名,说明需要写入文件中
    if (filename.length) {
        if (![self _fileWriteWithName:filename data:value]) { //写数据进文件
            return NO;
        }
        //写文件进数据库
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
            [self _fileDeleteWithName:filename]; //写失败,同时删除文件中的数据
            return NO;
        }
        return YES;
    } else {
        if (_type != YYKVStorageTypeSQLite) {
            NSString *filename = [self _dbGetFilenameWithKey:key]; //从文件中删除缓存
            if (filename) {
                [self _fileDeleteWithName:filename];
            }
        }
        //写入数据库
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
    }
}

该方法首先判断fileName即文件名是否为空,如果存在,则调用_fileWriteWithName方法将缓存的数据写入文件系统中,同时将数据写入数据库,需要注意的是,调用_dbSaveWithKey:value:fileName:extendedData:方法会创建一条SQL记录写入表中,

代码注释如下:

- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
    //构建sql语句,将一条记录添加进manifest表
    NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; //准备sql语句,返回stmt指针
    if (!stmt) return NO;
    
    int timestamp = (int)time(NULL);
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); //绑定参数值对应"?1"
    sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL); //绑定参数值对应"?2"
    sqlite3_bind_int(stmt, 3, (int)value.length);
    if (fileName.length == 0) { //如果fileName不存在,绑定参数值value.bytes对应"?4"
        sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
    } else { //如果fileName存在,不绑定,"?4"对应的参数值为null
        sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
    }
    sqlite3_bind_int(stmt, 5, timestamp); //绑定参数值对应"?5"
    sqlite3_bind_int(stmt, 6, timestamp); //绑定参数值对应"?6"
    sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0); //绑定参数值对应"?7"
    
    int result = sqlite3_step(stmt); //开始执行sql语句
    if (result != SQLITE_DONE) {
        if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
        return NO;
    }
    return YES;
}

该方法首先创建sql语句,value括号中的参数"?"表示参数需要通过变量绑定,"?"后面的数字表示绑定变量对应的索引号,如果VALUES (?1, ?1, ?2),则可以用同一个值绑定多个变量。

然后调用_dbPrepareStmt方法构建数据位置指针stmt,标记查询到的数据位置,sqlite3_prepare_v2()方法进行数据库操作的准备工作,第一个参数为成功打开的数据库指针db,第二个参数为要执行的sql语句,第三个参数为stmt指针的地址,这个方法也会返回一个int值,作为标记状态是否成功。

接着调用sqlite3_bind_text()方法将实际值作为变量绑定sql中的"?"参数,序号对应"?"后面对应的数字。不同类型的变量调用不同的方法,例如二进制数据是sqlite3_bind_blob方法。

同时判断如果fileName存在,则生成的sql语句只绑定数据的相关描述,不绑定inline_data,即实际存储的二进制数据,因为该缓存之前已经将二进制数据写进文件。这样做可以防止缓存数据同时写入文件和数据库,造成缓存空间的浪费。如果fileName不存在,则只写入数据库中,这时sql语句绑定inline_data,不绑定fileName。

最后执行sqlite3_step方法执行sql语句,对stmt指针进行移动,并返回一个int值。

删除缓存数据
  1. removeItemForKey:方法

    该方法删除指定key对应的缓存数据,区分type,如果是YYKVStorageTypeSQLite,调用_dbDeleteItemWithKey:从数据库中删除对应key的缓存记录,如下:

    - (BOOL)_dbDeleteItemWithKey:(NSString *)key {
        NSString *sql = @"delete from manifest where key = ?1;"; //sql语句
        sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; //准备stmt
        if (!stmt) return NO;
        sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); //绑定参数
        int result = sqlite3_step(stmt); //执行sql语句
        ...
        return YES;
    }
    

    如果是YYKVStorageTypeFile或者YYKVStorageTypeMixed,说明可能缓存数据之前可能被写入文件中,判断方法是调用_dbGetFilenameWithKey:方法从数据库中查找key对应的SQL记录的fileName字段。该方法的流程和上面的方法差不多,只是sql语句换成了select查询语句。如果查询到fileName,说明数据之前写入过文件中,调用_fileDeleteWithName方法删除数据,同时删除数据库中的记录。否则只从数据库中删除SQL记录。

  2. removeItemForKeys:方法

    该方法和上一个方法类似,删除一组key对应的缓存数据,同样区分type,对于YYKVStorageTypeSQLite,调用_dbDeleteItemWithKeys:方法指定sql语句删除一组记录,如下:

    - (BOOL)_dbDeleteItemWithKeys:(NSArray *)keys {
        if (![self _dbCheck]) return NO;
         //构建sql语句
        NSString *sql =  [NSString stringWithFormat:@"delete from manifest where key in (%@);", [self _dbJoinedKeys:keys]];
        sqlite3_stmt *stmt = NULL;
        int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL);
        ...
        //绑定变量
        [self _dbBindJoinedKeys:keys stmt:stmt fromIndex:1]; 
        result = sqlite3_step(stmt); //执行参数
        sqlite3_finalize(stmt); //对stmt指针进行关闭
        ...
        return YES;
    }
    

    其中_dbJoinedKeys:方法是拼装,?,?,?格式,_dbBindJoinedKeys:stmt:fromIndex:方法绑定变量和参数,如果?后面没有参数,则sqlite3_bind_text方法的第二个参数,索引值依次对应sql后面的"?"。

    如果是YYKVStorageTypeFile或者YYKVStorageTypeMixed,通过_dbGetFilenameWithKeys:方法返回一组fileName,根据每一个fileName删除文件中的缓存数据,同时删除数据库中的记录,否则只从数据库中删除SQL记录。

  3. removeItemsLargerThanSize:方法删除那些size大于指定size的缓存数据。同样是区分type,删除的逻辑也和上面的方法一致。_dbDeleteItemsWithSizeLargerThan方法除了sql语句不同,操作数据库的步骤相同。_dbCheckpoint方法调用sqlite3_wal_checkpoint方法进行checkpoint操作,将数据同步到数据库中。

  4. 其余的remove方法也都是根据一些筛选条件,删除不符合条件的数据,调用不同的sql语句实现这些数据库的操作,不详细分析了。

读取缓存数据
  1. getItemValueForKey:方法

    该方法通过key访问缓存数据value,区分type,如果是YYKVStorageTypeFile,调用_dbGetValueWithKey:方法从数据库中查询key对应的记录中的inline_data。如果是YYKVStorageTypeFile,首先调用_dbGetFilenameWithKey:方法从数据库中查询key对应的记录中的filename,根据filename从文件中删除对应缓存数据。如果是YYKVStorageTypeMixed,同样先获取filename,根据filename是否存在选择用相应的方式访问。代码注释如下:

    - (NSData *)getItemValueForKey:(NSString *)key {
        if (key.length == 0) return nil;
        NSData *value = nil;
        switch (_type) {
            case YYKVStorageTypeFile: {
                NSString *filename = [self _dbGetFilenameWithKey:key]; //从数据库中查找filename
                if (filename) {
                    value = [self _fileReadWithName:filename]; //根据filename读取数据
                    if (!value) {
                        [self _dbDeleteItemWithKey:key]; //如果没有读取到缓存数据,从数据库中删除记录,保持数据同步
                        value = nil;
                    }
                }
            } break;
            case YYKVStorageTypeSQLite: {
                value = [self _dbGetValueWithKey:key]; //直接从数据中取inline_data
            } break;
            case YYKVStorageTypeMixed: {
                NSString *filename = [self _dbGetFilenameWithKey:key]; //从数据库中查找filename
                if (filename) {
                    value = [self _fileReadWithName:filename]; //根据filename读取数据
                    if (!value) {
                        [self _dbDeleteItemWithKey:key]; //保持数据同步
                        value = nil;
                    }
                } else {
                    value = [self _dbGetValueWithKey:key]; //直接从数据中取inline_data
                }
            } break;
        }
        if (value) {
            [self _dbUpdateAccessTimeWithKey:key]; //更新访问时间
        }
        return value;
    }
    

    调用方法用于更新该数据的访问时间,即sql记录中的last_access_time字段。

  2. getItemForKey:方法

    该方法通过key访问数据,返回YYKVStorageItem封装的缓存数据。首先调用_dbGetItemWithKey:excludeInlineData:从数据库中查询,下面是代码注释:

    - (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData {
         //查询sql语句,是否排除inline_data
        NSString *sql = excludeInlineData ? @"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key = ?1;" : @"select key, filename, size, inline_data, modification_time, last_access_time, extended_data from manifest where key = ?1;";
        sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; //准备工作,构建stmt
        if (!stmt) return nil;
        sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); //绑定参数
        
        YYKVStorageItem *item = nil;
        int result = sqlite3_step(stmt); //执行sql语句
        if (result == SQLITE_ROW) {
            item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData]; //取出查询记录中的各个字段,用YYKVStorageItem封装并返回
        } else {
            if (result != SQLITE_DONE) {
                if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
            }
        }
        return item;
    }
    

    sql语句是查询符合key值的记录中的各个字段,例如缓存的key、大小、二进制数据、访问时间等信息, excludeInlineData表示查询数据时,是否要排除inline_data字段,即是否查询二进制数据,执行sql语句后,通过stmt指针和_dbGetItemFromStmt:excludeInlineData:方法取出各个字段,并创建YYKVStorageItem对象,将记录的各个字段赋值给各个属性,代码注释如下:

    - (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData {
        int i = 0;
        char *key = (char *)sqlite3_column_text(stmt, i++); //key
        char *filename = (char *)sqlite3_column_text(stmt, i++); //filename
        int size = sqlite3_column_int(stmt, i++); //数据大小
        const void *inline_data = excludeInlineData ? NULL : sqlite3_column_blob(stmt, i); //二进制数据
        int inline_data_bytes = excludeInlineData ? 0 : sqlite3_column_bytes(stmt, i++); 
        int modification_time = sqlite3_column_int(stmt, i++); //修改时间
        int last_access_time = sqlite3_column_int(stmt, i++); //访问时间
        const void *extended_data = sqlite3_column_blob(stmt, i); //附加数据
        int extended_data_bytes = sqlite3_column_bytes(stmt, i++);
        
         //用YYKVStorageItem对象封装
        YYKVStorageItem *item = [YYKVStorageItem new];
        if (key) item.key = [NSString stringWithUTF8String:key];
        if (filename && *filename != 0) item.filename = [NSString stringWithUTF8String:filename];
        item.size = size;
        if (inline_data_bytes > 0 && inline_data) item.value = [NSData dataWithBytes:inline_data length:inline_data_bytes];
        item.modTime = modification_time;
        item.accessTime = last_access_time;
        if (extended_data_bytes > 0 && extended_data) item.extendedData = [NSData dataWithBytes:extended_data length:extended_data_bytes];
        return item; //返回YYKVStorageItem对象
    }
    

    最后取出YYKVStorageItem对象后,判断filename属性是否存在,如果存在说明缓存的二进制数据写进了文件中,此时返回的YYKVStorageItem对象的value属性是nil,需要调用_fileReadWithName:方法从文件中读取数据,并赋值给YYKVStorageItem的value属性。代码注释如下:

    - (YYKVStorageItem *)getItemForKey:(NSString *)key {
        if (key.length == 0) return nil;
         //从数据库中查询记录,返回YYKVStorageItem对象,封装了缓存数据的信息
        YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
        if (item) {
            [self _dbUpdateAccessTimeWithKey:key]; //更新访问时间
            if (item.filename) { //filename存在,按照item.value从文件中读取
                item.value = [self _fileReadWithName:item.filename];
                ...
            }
        }
        return item;
    }
    
  3. getItemForKeys:方法

    返回一组YYKVStorageItem对象信息,调用_dbGetItemWithKeys:excludeInlineData:方法获取一组YYKVStorageItem对象。访问逻辑和getItemForKey:方法类似,sql语句的查询条件改为多个key匹配。

  4. getItemValueForKeys:方法

    返回一组缓存数据,调用getItemForKeys:方法获取一组YYKVStorageItem对象后,取出其中的value,存入一个临时字典对象后返回。

YYDiskCache

YYDiskCache是上层调用YYKVStorage的类,对外提供了存、删、查、边界控制的方法。内部维护了三个变量,如下:

@implementation YYDiskCache {
    YYKVStorage *_kv;
    dispatch_semaphore_t _lock;
    dispatch_queue_t _queue;
}

_kv用于缓存数据,_lock是信号量变量,用于多线程访问数据时的同步操作。

初始化方法

initWithPath:inlineThreshold:方法用于初始化,下面是代码注释:

- (instancetype)initWithPath:(NSString *)path
             inlineThreshold:(NSUInteger)threshold {
    ...
    YYKVStorageType type;
    if (threshold == 0) {
        type = YYKVStorageTypeFile;
    } else if (threshold == NSUIntegerMax) {
        type = YYKVStorageTypeSQLite;
    } else {
        type = YYKVStorageTypeMixed;
    }
    YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
    if (!kv) return nil;
    _kv = kv;
    _path = path;
    _lock = dispatch_semaphore_create(1);
    _queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
    _inlineThreshold = threshold;
    _countLimit = NSUIntegerMax;
    _costLimit = NSUIntegerMax;
    _ageLimit = DBL_MAX;
    _freeDiskSpaceLimit = 0;
    _autoTrimInterval = 60;
    
    [self _trimRecursively];
    ...
    return self;

根据threshold参数决定缓存的type,默认threshold是20KB,会选择YYKVStorageTypeMixed方式,即根据缓存数据的size进一步决定。然后初始化YYKVStorage对象,信号量、各种limit参数。

写缓存

setObject:forKey:方法存储数据,首先判断type,如果是YYKVStorageTypeSQLite,则直接将数据存入数据库中,filename传nil,如果是YYKVStorageTypeFile或者YYKVStorageTypeMixed,则判断要存储的数据的大小,如果超过threshold(默认20KB),则需要将数据写入文件,并通过key生成filename。YYCache的作者认为当数据代销超过20KB时,写入文件速度更快。代码注释如下:

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    ...  
    value = [NSKeyedArchiver archivedDataWithRootObject:object]; //序列化
    ...
    NSString *filename = nil;
    if (_kv.type != YYKVStorageTypeSQLite) {
        if (value.length > _inlineThreshold) { //value大于阈值,用文件方式存储value
            filename = [self _filenameForKey:key];
        }
    }
    Lock();
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData]; //filename存在,数据库中不写入value,即inline_data字段为空
    Unlock();
}
读缓存

objectForKey:方法调用YYKVStorage对象的getItemForKey:方法读取数据,返回YYKVStorageItem对象,取出value属性,进行反序列化。

删除缓存

removeObjectForKey:方法调用YYKVStorage对象的removeItemForKey:方法删除缓存数据。

边界控制

前一篇文章中,YYMemoryCache实现了内存缓存的LRU算法,YYDiskCache也试了LRU算法,在初始化的时候调用_trimRecursively方法每个一定时间检测一下缓存数据大小是否超过容量。

数据同步

YYMemoryCache使用了互斥锁来实现多线程访问数据的同步性,YYDiskCache使用了信号量来实现,下面是两个宏:

#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)

读写缓存数据的�方法中都调用了宏:

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key
{
    ...
    Lock();
    [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
    Unlock();
}

- (id<NSCoding>)objectForKey:(NSString *)key {
    Lock();
    YYKVStorageItem *item = [_kv getItemForKey:key];
    Unlock();
    ...
}

初始化方法创建信号量,dispatch_semaphore_create(1),值是1。当线程调用写缓存的方法时,调用dispatch_semaphore_wait方法使信号量-1。同时线程B在读缓存时,由于信号量为0,遇到dispatch_semaphore_wait方法时会被阻塞。直到线程A写完数据时,调用dispatch_semaphore_signal方法时,信号量+1,线程B继续执行,读取数据。关于iOS中各种互斥锁性能的对比,可以参考作者的文章

总结

YYCache库的分析到此为止,其中有许多代码值得学习。例如二级缓存的思想,LRU的实现,SQLite的WAL机制。文中许多地方的分析和思路,表达的不是很准确和清楚,希望通过今后的学习和练习,提升自己的水平,总之路漫漫其修远兮...

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

推荐阅读更多精彩内容