iOS基础之数据存储

iOS数据存储

1. 概论

在iOS开发中数据存储的方式可以归纳为两类: 存储文件 和 存储到数据库.

2.文件存储

2.1 沙盒

  1. 文件下载思路:
    客户端发送请求->服务器响应,返回NSData->客户端接受数据;
  2. 沙盒机制(sandbox):每个iOS应用都有自己的应用沙盒,即文件系统目录.属于封闭式的,所有APP都在单独的沙盒中运行;为了:1⃣️完美的用户体验需要对跨应用程序进行整合统一;2⃣️封闭跨应用可以保证系统的安全性;(iOS8以后开放了几个固定的系统区域,例如第三方图片编辑,第三方输入法等)

应用沙盒一般包括:

  • Documents:保存应用运行时生成需要的持久化数据iTunes会备份.
  • tmp:保存应用运行时所需的临时数据.应用完毕会删除.不会备份
  • library的Cache:保存应用运行时生成的需要持久化的缓存数据,不自动删除.一般存储体积大、不需要备份的非重要数据
  • Library的Preferences:保存应用偏好设置.会备份.

获取沙盒路径:

  • 获取根路径:NSHomeDirectory()的返回值;
  • 获取tmp路径:NStempoaryDirectory();
  • 获取Library/Preference:通过NSUserDefaults类存取该目录下的设置信息
  • 获取library/Cache:同Documents.
  • 获取Documents路径:
    • 方式1---使用字符串拼接=>根目录路径加上Documents; stringByAppendingPathComponent:@"Documents" // 不建议采用,因为新版本的操作系统可能会修改目录名

    • 方式2:搜索模式:(主要)
      代码如下:

    在某个范围搜索摸个文件路径  函数:NSSearchPathForDirectoriesInDomains:(参数directory,参数domainMask,参数rxpandTitle)
    // 1. directory   表示获取哪个文件夹目录
    // 2. domainMask  表示查找范围
    // 3. rxpandTitle 表示是否展开波浪号
    例:获取Documents路径.
    NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];  // 在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素,所以下标为0;
    // 最后 writeToFile 即可

注意:

  1. iOS 8.0 之后,出于安全考虑, 将沙盒路径和mainBundle路径进行了分离.
  2. 写进文件时要对操作文件名和路径进行 stringByAppendingPathComponent 拼接

2.2 文件数据存储与读取方式:

  1. plist存储(XML 属性列表) - 基本对象类型--数据
    • plist 文件的 根节点 只有 NSArray 和 NSDictionary 两种数据类型,其它基本对象类型类型 可以通过 NSArray 和 NSDictionary 间接存储到 plist 文件
    • 使用writeToFile: atomically:方法直接将对象写到属性列表文件中
  2. preference存储 (偏好设置:保存用户名、字体大小、是否自动登录) - 键值对
    • 通过 [NSUserDefaults standardUserDefaults] 获取 NSUserDefaults 对象,这个对象专门用来做偏好设置存储.
    • 存储:[[NSUserDefaults standardUserDefaults] setObject:@"hm" forKey:@"account"];
    • 读取:NSString *account = [[NSUserDefaults standardUserDefaults] objectForKey:@"account"];
    • 偏好设置注意点: iOS8之前, 通常还需要做一个同步操作, 就是把缓存数据同步到硬盘当中[defaults synchornize];
  3. NSKeyedArchiver 归档(转换成二进制数据存储到闪存中) - 对象:遵守NSCoding自定义对象类型和基本对象类型--用户详细信息
    • 保存:[NSKeyedArchiver archiveRootObject:person toFile:path]; path = NSSearchPathForDirectoriesInDomains xxx
    • 遵守NSCoding协议,协议有2个方法:encodeWithCoder:会在归档时调用;initWithCoder:会在解档时调用.
    • 在归档方法中需要指定如何归档对象中的对应实例变量,- (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_name forKey:NameKey]; }
    • 在解档方法中指定如何解码文件中的数据为对象的实例变量,- (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super init]) _name = [aDecoder decodeObjectForKey:NameKey]; return self;
    • 注意解档时,如果父类也遵守了NSCoding协议.则需要[super initWithCoder].特别的,UIView实现了NScoding协议方法.加载解析storyboard/xib时会使用[super initWithCoder]方法.

3. 数据库存储

可以通过SQL直接访问数据库,也可以通过ORM进行对象关系映射访问数据库。这两种方式恰恰对应iOS中SQLite和Core Data的内容,在此将重点进行分析:

3.1 SQLite 关系数据库

SQLite是目前主流的嵌入式关系型数据库;特点是:轻量级,跨平台.

  • 基于C语言开发的轻型数据库.
  1. 需要设置一个自增的id,主键PK: 唯一区别数据库上内容 ,网络数据库会查询当前可用的id, 查到之后建立排它锁, 读取数据完成会打开排它锁 ; 自动增长的id 生成是数据库来负责,程序员无需关心!
  2. SQLite是采用的动态数据类型,即使创建时定义一种类型,在实际操作时也可以存储其他类型.不过不推荐
  3. 建立连接后移动端通常使用持久化连接开发数据库;(跟网络连接不同);

在iOS中操作SQLite数据库可以分为以下几步(注意先在项目中导入libsqlite3框架):

  1. 打开数据库,利用sqlite3_open()打开数据库会指定一个数据文件保存路径,如果 文件存在则直接打开,否则创建并打开; 打开数据库会得到一个sqlite3类型的对象,后面需要借助这个对象进行其他操作,称为句柄。
  2. 执行SQL语句,(包括有返回值和无返回值语句).
    • SQL语句推荐使用从文件加载,这样不需要
    • 对于无返回值语句(增删改),直接通过sqlite3_exec()函数执行;
    • 对于有返回值语句(查),则首先通过sqlite3_prepare_vc2()进行sql语句评估(语法检测),然后通过sqlite3_step()依次取出查询结果的每一行数据,对于每行数据都可以通过对应的sqlite3_colimn_类型()方法获得对应类的数据,如此反复循环直到遍历完成.最后释放句柄.

数据库大量操作问题
如果一次向数据库中插入大量数据,十分耗时,如何解决:
耗时是因为SQLite数据库操作中,如果不显示开启事务,那么每一条数据库操作指令,都会隐式打开/提交一次事务; 而事务的开启和关闭时内存操作, 比较耗时;
解决办法就是在操作之前主动开启事务,在操作结束,提交事务即可 ; 验证效果可以使用CACurrentMediaTime() //取绝对时间观察耗时
事务是一个执行单元,表示这个单元内,要么都执行成功,要么都失败;
注意: 手动开启事务之后,执行单元功能也需要我们手动执行: 在事务开启期间,只要有一个操作出现错误,就回滚事务到初始状态;

[CATransaction begin]; //开启事务
for i in 10000
    {
    xxx;
    
    if xxx {  [CATransition rollBack] } //判断如果操作失败回滚

    }
[CATransaction commit]; //关闭事务

在整个操作过程中无需管理数据库连接,对于嵌入式SQLite操作是持久连接(尽管可以通过sqlite3_close()关闭),不需要开发人员自己释放连接。纵观整个操作过程,其实与其他平台的开发没有明显的区别,较为麻烦的就是数据读取,在iOS平台中使用C进行数据读取采用了游标的形式,每次只能读取一行数据,较为麻烦。因此实际开发中不妨对这些操作进行封装:

  • 1.定义一个sqlite3类型属性句柄使用它对数据库操作:@property (nonatomic) sqlite3 *database;

  • 2.打开数据库

//打开或创建并打开数据库(一般保存到沙盒Documents目录中)
NSString *directory=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];

    NSString *filePath=[directory stringByAppendingPathComponent:dbname];
    //注意filePath需要转换为C的字符串才可做此函数参数,XCode7beat5之后不需要加UTF8String转了 
    //返回true表示打开成功
    return SQLITE_OK == sqlite3_open(filePath, &_database) 
  • 3.操作数据库: 单步执行sql语句插入,修改,删除
//插入,修改,删除只是sql执行语句不同,步骤相同
// 从bundle的 .sql文件中加载 sql语句: 
NSString *path = [[NSBundle mainBundle] pathForResource : "db.sql" ofType:nil];
NSString * sql = [NSString contensOfFile:path];
     / *
            参数
        
            1. 数据库全局句柄
            2. 要执行的 SQL
            3. callback,执行完成 SQL 之后,调用的 C 语言函数指针,通常传入 nil
            4. 第三个参数 callback,函数参数的地址,通常传入 nil
            5. 错误信息,有其他方式获取执行情况,通常传入 nil
        
            返回值 如果 == SQLITE_OK 表示成功
        */
return sqlite3_exec(_database, sql, NULL, NULL, &error) == SQLITE_OK
  • 4.操作数据库:查询 - 返回数据
// 1. 预编译 SQL 检查语法正确性
        /**
            参数
            1. 全局数据库句柄
            2. 要执行 SQL 的 C 语言的字符串
            3. 要执行 SQL 的以字节为单位的长度,但是,如果传入 -1,SQLite 框架会自动计算
            4. STMT - 预编译的指令句柄
                - 后续针对`本次查询`所有操作,全部基于此句柄
                - 必须注意的,句柄一定要释放
                - 编译完成后,可以理解为一个临时的数据集合,通过 step 函数,能够顺序获取其中的结果
            5. 关于 STMT 尾部参数的指针,通常传入 NULL
            
            返回值 
            如果编译成功,表示 SQL 能够正常执行,返回 SQLITE_OK
        */ 
        
       COpaquePointer *stmt;
       if (SQLITE_OK != sqlite3_prepare_v2(_database, sql, -1, &stmt, NULL)) { NSLog(@"SQL错误");     sqlite3_finalize(stmt);
         return nil 
 }
       // 
      
       //创建字典数组
       NSMutableArray *rows=[NSMutableArray array];//数据行
       //单步执行sql语句,获得ROW 对应一条完整记录
        while (SQLITE_ROW == sqlite3_step(stmt)) {
            int columnCount = sqlite3_column_count(stmt); //记录查询列数
            // 创建单条记录的字典
            NSMutableDictionary *dic=[NSMutableDictionary dictionary];
            
            for col in 0..<cols {
            // 1> 列名 Int8 / CChar / Byte
            const char *name cName = sqlite3_column_name(stmt, col)
            
            // 2> 数据类型
            let type = sqlite3_column_type(stmt, col)
            
            const unsigned char *value;            
            switch type {
                case SQLITE_FLOAT:      // 小数
                    value = sqlite3_column_double(stmt, col);
                case SQLITE_INTEGER:    // 整数
                    value = Int(sqlite3_column_int64(stmt, col));
                case SQLITE3_TEXT:      // 字符串
                    // 记录 C 语言的字符串
                    value = sqlite3_column_text(stmt, i);                            
                case SQLITE_NULL:       // 空值,一般数据库中允许字段为 nil,但是 OC 的字典不能插入 nil
                    value = NSNull()    // NSNull 就是专门向字典和数组中插入控制使用的
            default:
                print("不支持的数据类型")
            }
            
            dic[[NSString stringWithUTF8String:name]]=[NSString stringWithUTF8String:(const char *)value]; //xcode7之后无需转换UTF8
        }  
        [rows addObject:dic]
    }
    //释放句柄
    sqlite3_finalize(stmt);
    
    //返回数据
    return rows;

一般数据是从网络读取的,但是考虑缓存问题,通常会选择将微博数据保存到本地; 实际开发中并不会在控制器中直接调用数据操作方法, 在这里引入一个 Service ,操作数据库的访问服务层; 进行数据的增删该查,由于访问层不需要过多的设置,所以定义成单例,保证程序只有一个即可;在其中将对数据库的操作转换为对模型的操作

060906123309031.png

Core Data

概述

当前,各类应用开发中只要牵扯到数据库操作通常都会用到一个概念“对象关系映射(ORM)”;iOS中ORM框架首选Core Data; ORM框架的作用就是将 关系数据库中的 表 转换为程序中的 对象,所以其本质还是对数据库的操作,例如: Core Data中如果存储类型配置为SQLite则本质还是操作的SQLite数据库;
上面代码中我们已经可以将数据库操作转换为了对象操作,服务层中的方法中已经将这些操作封装起来;但是操作过程比较复杂:首先手动创建数据库,其次再手动创建模型和访问层;
上述的数据映射到实体的过程完全是手动的;Core Data 就是为了解决这个问题而产生的;

使用Core Data 进行数据存取并不需要手动创建数据库,这个过程完全由Core Data 框架帮我们完成, 开发人员面对的是模型, 只需把模型创建起来, 具体数据库如何创建则不用管;

步骤:

  1. 在项目中添加Data Model 模型文件,并在其中创建实体和关系:
060906183458227.jpg
* **注意: 实体对象不需要创建ID主键** , Attributes中应该是有意义属性 ( 创建过程中应该考虑对象需要的属性 , 而不是数据库中表有几个字段 ;)
* 所有属性应该指定具体类型,因为实体对象会对应 生成ObjC模型类.
* 实体对象中 其他实体对象类型的属性(类似模型嵌套) 应该通过Relationships建立, 并且注意实体之间的对应关系,(例如: 一个用户用多条微博, 而一条微博则只属于一个用户)
  1. 根据创建好的模型文件(.xcdatamodeld文件) 生成具体的实体类, 在Xcode中添加"NSManagedObject Subclass"文件,按照步骤要选择创建的模型及实体,Xcode就会根据所创建的模型生成具体的实体类;
    • 通过模型生成的类过程相当简单,不需要手动维护,如果模型有变化,重新生成即可;
    • 注意: 从此方法创建的类都几次与NSMagagedObject,每个NSManagedObject对象对应着数据库中一条记录
    • 集合属性()会生成访问此属性的分类方法;
    • 使用@dynamic 代表 数据属性实现, 具体实现细节不用关心
  2. 最后Core Data的使用 ;

Core Data 的使用;

要使用Core Data完成数据的存取,先了解一下Core Data几个核心类:

060906214392683.jpg
  • Persistent Object Store : 可以理解为 存储持久对象的数据库(例如:SQLite;Core也支持其他类型的数据存储:如xml,二进制数据等)
  • Managed Object Model: 对象模型 , 对应Xcode中创建的模型文件.
  • Persistent Store Coordinator: 对象模型和实体类之间的转换协调器, 用于管理不同存储对象的上下文.
  • Managed Object Context: 对象管理上下文, 负责实体对象和数据库之间的交互 ;

Core Data使用起来相对直接使用SQLite3的C语言API而言更加面向对象,
操作步骤如下:

1.创建管理的上下文 :
-(NSManagedObjectContext *)createDbContext{
    NSManagedObjectContext *context;
    
    //1. 加载模型文件, 参数为nil表示打开包中所有模型文件并合成一个
    NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil]
        //创建解析器
    NSPersistentStoreCoordinator *sc = [ [NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model];
    //2. 指定保存路径
    NSString *dir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    NSString *path=[dir stringByAppendingPathComponent:@"myDatabase.db"];
    NSURL *url=[NSURL fileURLWithPath:path];
    //3. 添加SQLite持久存储到解析器
    [sc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error];
    if (error) {
        NSLog(@"数据库打开失败!错误:%@",error.localizedDescription);
    } else { 
        //4. 创建管理对象上下文,指定器存储
        context = [NSManagedObjectContext new];
        context.persistentStoreCoordinator = sc;
    }
    return context ; 
}

经过这几个步骤后可以得到管理对象上下文 NSManagedObjectContext , 如果第一次创建上下文,Core Data还会自动创建存储文件 (这里使用SQLite3数据库),并根据模型对象创建对应的表结构 ; 为了方便使用

060906224866794.jpg
2.查询数据:
  1. 对于有条件的查询,Core Data通过谓词来实现: 创建请求 -> 设置请求条件 -> 调用上下文执行请求;
-(void)addUserWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{
    //添加一个对象
    User *us= [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:self.context];
    us.name=name;
    us.screenName=screenName;
    us.profileImageUrl=profileImageUrl;
    us.mbtype=mbtype;
    us.city=city;
    NSError *error;
    //保存上下文
    if (![self.context save:&error]) {
        NSLog(@"添加过程中发生错误,错误信息:%@!",error.localizedDescription);
    }
}
  • 如果有多个条件, 只要使用谓词组合即可, 那么对于关联对象怎么查询呢?需要分两种情况:
    a. 查找一个对象只有唯一一个关联对象的情况, (如一条微博只能属于一个用户) ;通过keypath查询:

    -(NSArray *)getStatusesByUserName:(NSString *)name{
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: @ "Status"];
        request.predicate = [NSPredicate predicateWithFormat:@"user.name=%@",name];
        NSArray *array=[self.context executeFetchRequest:request error:nil];
        return array;
    

    b. 查找一个对象有多个关联对象的情况,例如查找发送微博内容中包含"Watch"并且User为"小娜"的用户(一个用户有多条微博),此时使用 谓词 过滤;

    -(NSArray *)getUsersByStatusText:(NSString *)text screenName: (NSString *) screenName {
        NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Status"] ;
        request.predicate = [NSPredicate predicateWithFormat:@"text LIKE '*Watch*'",text];
        NSArray *statues = [self.context executeFetchRequest:request error:nil];
        
        MSPredicate *userPredicate = [NSPredicate predicateWithFormat:@"user.screenName=%@",screenName];
        NSArray *users = [statues filteredArrayUsingPredicate:userPredicate];
        return users;
    } //如果单纯查找微博中包含"Watch"的用户,直接查出对应的微博,然后通过每个微博的user属性即可获得用户组即可;
    
  • 增删改数据
    插入数据 需要调用实体描述对象 NSEnityDescription返回一个实体对象, 然后设置对象属性,最后保存当前的上下文即可; 这里需要注意,增删改操作完最后必须调用管理上下文的保存方法,否则操作不会执行(同ios绘图)

//添加一个对象
    User *us= [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:self.context];
    us.name=name;
    us.screenName=screenName;
    us.city=city;
//删除一个对象
    //[self.context deleteObject:user];
//修改一个数据
    //User *us=[self getUserByName:name]; //根据name获取对象
    //修改属性
    //us.name=name1;
    //us.screenName=screenName1;
    //us.city=city1;
    
    NSError *error;
    //最后都需要 - 保存上下文
    if (![self.context save:&error]) {
        NSLog(@"添加过程中发生错误,错误信息:%@!",error.localizedDescription);
    }

调试

虽然Core Data操作(如果使用SQLite数据库)最终转换为SQL操作,但是调试起来却不如SQL那么方便,因为看不到最终生成的SQL语句, 可以在Xcode设置: Product-Scheme-Edit Scheme-Run-Arguments中的passed On launch中依次添加两个参数(注意参数顺序不能错):-com.apple.CoreData.SQLDebug和1 ;
之后运行程序过程中如果操作了SQL语句就打印在输出面板; 还有一点注意: 如果模型发生变化,此时可以重新生成实体类文件,但是所生成的数据库并不会自动更新,需要重新生成;

FMDB框架

相对比与SLQite3来说Core Data存在着诸多优势,它面向对象,开发人员不必过多的关系更多的数据库操作知识,但是其本身也有一些限制,例如: 不能跨平台,ORM框架都存在性能问题(最终要转换SQL操作); 所以最好是对SQL进行封装--FMDB;

1. 使用

1.FMDB既然是对于libsqlite3框架的封装,自然使用起来也是类似的,使用前也要打开一个数据库,这个数据库文件存在则直接打开否则会创建并打开。这里FMDB引入了一个FMDatabase对象来表示数据库,打开数据库和后面的数据库操作全部依赖此对象。下面是打开数据库获得FMDatabase对象的代码:

-(void)openDb:(NSString *)dbname{
    //取得数据库保存路径,通常保存沙盒Documents目录(如果此参数设为nil会默认在内存中创建数据库,如果设为@""则会在沙盒中的临时目录创建)
    NSString *directory=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    NSLog(@"%@",directory);
    NSString *filePath=[directory stringByAppendingPathComponent:dbname];
    //创建FMDatabase对象
    self.database=[FMDatabase databaseWithPath:filePath];
    //打开数据
    if ([self.database open]) {
        NSLog(@"数据库打开成功!");
        // 执行数据库操作
        //1.更新
        //[FMDatabase executeUpdate:error:withArgumentsInArray:orVAList:]
        //2. 查询
        FMResultSet *rs = [database executeQuery:@"要执行的sql语句"];
        while([rs next]){
            // 提取查询数据
            NSString *rsData = [rs stringForColumn:@"first name"];
        }
        //关闭数据库
        [database close]
    }else{
        NSLog(@"数据库打开失败!");
    }
}

2.在FMDB中FMDatabase类提供了两个方法executeUpdate:和executeQuery:分别用于执行无返回结果的更新和有返回结果的查询。唯一需要指出的是,如果调用有格式化参数的sql语句时,格式化符号使用“?”而不是“%@”、等。(是为了安全)

多线程中FMDB应用.

直接使用libsqlite3进行数据库操作其实是线程不安全的, 因此在多线程中需使用FMDatabaseQueue对象, 相比FMDatabase而言,他是线程安全的;

//创建 , 最好放在一个单例中: 
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:apath];
//使用
[queue inDatabase:^(FMDatabase *db) {
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
    
    FMResultSet *rs = [db excuteQuery:@"select * from foo"];
    while ([rs next]){
        //......
    }
}];

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

推荐阅读更多精彩内容