iOS常用的数据存储

在iOS开发过程中,会经常使用到数据存储.数据存储有好几种常见的方式:plist存储,偏好设置,归档,数据库等,该篇文章是我想做个记录,也想和大家分享下自己的一些见解,涉及的内容不是很深,只做实用性的参考.如果哪位大神对此有深入研究,可以留下来深入讨论下,我请吃饭.

沙盒
  • iOS的存储都是存在沙盒中的,应用程序只能访问自己的沙盒
  • 沙盒目录里有三个文件:Documents, Library, tmp
    沙盒目录图
沙盒目录.png
  • Documents:用于存储用户数据或其它应该定期备份的信息
    • 注意,该文件中的内容会在手机连接电脑时进行同步到云上,所以苹果不建议开发者将大量的数据写入到该文件中.如果将视频图片等大量的数据存入到该文件中,会被苹果拒的
// Documents获取方式
NSString *docStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
  • Library:该目录下有两个子目录,Caches和Preferences.
    • Caches:一般用于存放应用程序专用的支持文件,保存应用程序再次启动所需要的信息
    • 数据存在该文件中
//Caches获取方法
   NSString *cachesStr = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]
- Preferences:存放应用程序的偏好设置的文件.
  • tmp:用于存放临时文件,当程序退出时该文件中的数据会被清空
//tmp获取方式
 NSString *tmpStr =  NSTemporaryDirectory();
关于沙盒的详细信息,可以去搜下别人的文章,我发现有很多文章对沙盒有详细的讲解的,或者参考苹果官方文档:About Files and Directories
Plist存储

定义:plist文件的全名是:Property List,它是一种用来存储串行化后的对象的文件。文件的扩展名是.plist,通常就称为plist文件.主要是用来存储一些OC对象,比如NSArray/NSDictionary,不能存放自定义对象.一般常用的Foundation对象有NSString, NSData, NSDate, NSNumber, NSArray, NSDictionary
存储原理:写入沙盒.只要有writeToFile的对象,就能进行plist存储,调用writeToFile就能自动生成plist格式的文件
注意:plist存储,不能存储自定义对象,会失败的。
plist文件在数据更新的时候需要先取出然后修改在存储
常用方法:

// 写入方法
- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;
- (BOOL)writeToURL:(NSURL *)url atomically:(BOOL)atomically;
// 取出 字典
+ (nullable NSDictionary<KeyType, ObjectType> *)dictionaryWithContentsOfFile:(NSString *)path;
+ (nullable NSDictionary<KeyType, ObjectType> *)dictionaryWithContentsOfURL:(NSURL *)url;
// 取出 数组
+ (nullable NSMutableArray<ObjectType> *)arrayWithContentsOfFile:(NSString *)path;
+ (nullable NSMutableArray<ObjectType> *)arrayWithContentsOfURL:(NSURL *)url;

获取沙盒路径

NSString *str = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

偏好设置

在开发中偏好设置用的还是比较多的,存取比较方便,不需要关心文件名,其实底层是利用字典存储一些键值对,你可以存储后打开路径文件看一下.
存储过程是用NSUserDefaults的单利调用setObject:forKey进行存储.
注意:不能存储对象,不能及时存储,需要做同步操作,把内存中的数据同步到硬盘上。

简单使用

 // 存储名字jack到沙盒中,存储的key是 @"name"
    [[NSUserDefaults standardUserDefaults] setObject:@"jack" forKey:@"name"];
    // 同步
    [[NSUserDefaults standardUserDefaults] synchronize];
    // 用key  @"name" 从沙盒中取出名字
    NSString *str =[[NSUserDefaults standardUserDefaults] objectForKey:@"name"];
    

存储方法:

// 存储方法(set开头)
- (void)setObject:(nullable id)value forKey:(NSString *)defaultName;
- (void)setInteger:(NSInteger)value forKey:(NSString *)defaultName;
- (void)setFloat:(float)value forKey:(NSString *)defaultName;
- (void)setDouble:(double)value forKey:(NSString *)defaultName;
- (void)setBool:(BOOL)value forKey:(NSString *)defaultName;
- (void)setURL:(nullable NSURL *)url forKey:(NSString *)defaultName

读取数据的方法:

// 读取数据的方法
- (nullable id)objectForKey:(NSString *)defaultName;
- (nullable NSString *)stringForKey:(NSString *)defaultName;
- (nullable NSArray *)arrayForKey:(NSString *)defaultName;
- (nullable NSDictionary<NSString *, id> *)dictionaryForKey:(NSString *)defaultName;
- (nullable NSData *)dataForKey:(NSString *)defaultName;
- (nullable NSArray<NSString *> *)stringArrayForKey:(NSString *)defaultName;
- (NSInteger)integerForKey:(NSString *)defaultName;
- (float)floatForKey:(NSString *)defaultName;
- (double)doubleForKey:(NSString *)defaultName;
- (BOOL)boolForKey:(NSString *)defaultName;
- (nullable NSURL *)URLForKey:(NSString *)defaultName NS_AVAILABLE(10_6, 4_0);

删除数据:

// 删除数据
- (void)removeObjectForKey:(NSString *)defaultName;
归档

个人理解,归档的过程就是一个序列化的过程,然后存储到指定的路径路径文件.归档可以存储对象,需要遵守NSCoding协议,然后重写下面两种方法

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
具体使用方法:
  • 自定义模型
Person类的.h文件

#import <UIKit/UIKit.h>

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, assign) NSInteger age;

@end

Person类的.m文件

#import "Person.h"
@interface Person ()<NSCoding>

@end

@implementation Person

// 解档
// 从一个给定unarchiver的数据中返回一个初始化对象。
- (instancetype)initWithCoder:(NSCoder *)decoder{
    
    if (self = [super init]) {
        
        self.name = [decoder decodeObjectForKey:@"name"];
        self.age = [decoder decodeIntegerForKey:@"age"];
        self.height = [decoder decodeFloatForKey:@"height"];
        
    }
    return self;
}
// 归档
/**
 通过一个给定的archiver把消息接收者进行编码。
 当接收到encodeObject消息的时候,类终端encodeWithCoder方法被调用。
 */
- (void)encodeWithCoder:(NSCoder *)coder{
    
    [coder encodeObject:self.name forKey:@"name"];
    [coder encodeInteger:self.age forKey:@"age"];
    [coder encodeFloat:self.height forKey:@"height"];
    
}
  • 第一种用法
// 创建对象
    Person *person = [Person new];
    person.name = @"jack";
    person.age = 20;
    person.height = 180.0f;

// 获取路径
    NSString *str = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.archiver"];
// 归档
   BOOL flag = [NSKeyedArchiver archiveRootObject:person toFile:filePath];
    if (flag) {
        NSLog(@"归档成功");
    }else{
        NSLog(@"归档失败");
    }

// 解档
   Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:str];
  • 第二种用法
// 创建对象
    Person *person = [Person new];
    person.name = @"jack";
    person.age = 20;
    person.height = 180.0f;

    //  归档成二进制 (可以用偏好设置保存或者直接写入到沙盒)
    NSData *dataArc = [NSKeyedArchiver archivedDataWithRootObject:person];
   [[NSUserDefaults standardUserDefaults] setObject:dataArc forKey:@"archiver"];
   [[NSUserDefaults standardUserDefaults] synchronize];

  // 解档
    NSData *dataUn = [[NSUserDefaults standardUserDefaults] objectForKey:@"archiver"];
    Person *unPerson = [NSKeyedUnarchiver unarchiveObjectWithData:dataUn];
    
  • 第三种用法
    // 获取路径
    NSString *str = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.archiver"];
    // 创建一个二进制对象
    NSMutableData *data = [[NSMutableData alloc] init];
    // 设置归档写入对象
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    // 归档
    [archiver encodeObject:person forKey:@"archiver"];// archivingDate的encodeWithCoder方法被调用
    [archiver finishEncoding];
    // 保存二进制到沙盒中
    [data writeToFile:str atomically:YES];

    // 解档
    // 通过文件获取一个二进制对象
    NSMutableData *data = [[NSMutableData alloc] initWithContentsOfFile:str];
    // 读取二进制文件
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    // 获得类
    Person *person = [unarchiver decodeObjectForKey:@"person"]; ;// initWithCoder方法被调用
    [unarchiver finishDecoding];
    

在自定义模型的.m文件中,重写 - (instancetype)initWithCoder:(NSCoder *)decoder 时,调用的是[super init], 而不是 [super initWithCoder:decoder]; 这是因为Person类继承的是NSObject,NSObject类没有遵守NSCoding这个协议.
initWithCoder原理:只要解析文件就会调用,xib,storyboard都是文件,因此只要解析这两个文件,就会调用initWithCoder。
因此如果在storyboard使用自定义view,重写initWithCoder方法,一定要调用[super initWithCoder:],因为只有系统才知道怎么解析storyboard,如果没有调用,就解析不了这个文件。
重写NSCoding协议的方法时有一个不便的地方就是,当自定义模型特别多的时候,每个.m文件中都需要重写一遍,挺麻烦的.下面这个文件是通过运行时的方法遍历模型中的属性进行设置的,在使用的时候,需要导入#import "GGArchiverCoding.h", #import <objc/runtime.h>头文件,在.m文件中写入这句话就可以了:NSCodingImplementation;

GGArchiverCoding.h 文件
注意:该文件可以原版copy的

#define NSCodingImplementation \
- (instancetype)initWithCoder:(NSCoder *)aDecoder{\
    if (self = [super init]) { \
        unsigned int count; \
        Ivar *ivarList = class_copyIvarList([self class], &count);\
        for (int i = 0; i < count; i++) {\
            Ivar ivar = ivarList[i];\
            const char *ivarName = ivar_getName(ivar);\
            NSString *key = [NSString stringWithUTF8String:ivarName];\
            id value = [aDecoder decodeObjectForKey:key]; \
            [self setValue:value forKey:key];\
        }\
    }\
    return self;\
}\
- (void)encodeWithCoder:(NSCoder *)aCoder{ \
    unsigned int count;\
    Ivar *ivarList = class_copyIvarList([self class], &count);\
    for (int i = 0; i < count; i++) {\
        Ivar ivar = ivarList[i];\
        const char *ivarName = ivar_getName(ivar);\
        NSString *key = [NSString stringWithUTF8String:ivarName];\
        id value = [self valueForKeyPath:key];\
        [aCoder encodeObject:value forKey:key];\
    }\
}

iOS中最常见的三种存储方式介绍完了,虽然使用率还是蛮好的,对于大型数据的存储上面三种就做不到了,或者说想做的很麻烦的.数据库就能这解决这种问题

SQLite介绍(PS:下面只是对SQLite3的简单实用的介绍,没有做详细的讲解,事后详情再补上.如果哪位大神有详细介绍的文章,可以跟帖链接,共同学习)

实际开发过程中,很少会直接用SQLite3,常常会采用一些对SQLite3封装好的三方框架,FMDB就是一个非常优秀的框架.
(度娘介绍):SQLite是一款轻型的嵌入式数据库,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了,它的处理速度比Mysql、PostgreSQL这两款著名的数据库都还快,数据库(Database)是按照数据结构来组织、存储和管理数据的仓库.
想学习数据就,建议安装下Navicat,用着很不错的一款数据库软件.
数据库可以分为两大种类:关系型数据库(主流)和 对象型数据库
常用关系型数据库PC端:Oracle、MySQL、SQL Server、Access、DB2、Sybase.嵌入式\移动客户端:SQLite

数据库的存储结构和excel很像,以表(table)为单位
数据库的每一列叫字段(column,列,属性)
每一行叫记录(row,record,每行存放多个字段对应的值)
建表:Tabel Name: 填写表明,建议以_t开头

常用的关键字有:select、insert、update、delete、from、create、where、desc、order、by、group、table、alter、view、index等等数据库中不可以使用关键字来命名表、字段

  • SQL语句种类:
    • DDL:数据定义语句:包括create(创表)等操作
    • DML:数据操作语句:包括:insert(增加)、update(修改)、delete(删除)等操作
    • DQL:数据查询语句:最常用的关键字select(查询),别的常用的关键字有where,order by,group by和having
  • 字段类型:SQL将数据划分为四种数据类型
    • integer : 整形值
    • real:浮点值
    • text:文本字符串
    • blob:二进制数据
  • 主键:用来唯一标识某一条记录,在创表的时候用primary key声明一个主键

创表

// 在编写数据库语句是,尽可能多的添加一些约束条件,这样可以方便交流

// 导入数据库头文件
#import <sqlite3.h>

// 保存数据库实例对象
@property (nonatomic, assign) sqlite3 *db;


// 获取沙盒路径
    NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.db"];
 // 将OC字符串 转成 C语言字符串
    const char *cFilePath = filePath.UTF8String;
// 创表
    // 将根据文件路径打开数据库,如果不存在,则会创建一个新的数据库
    // 返回值表示状态
    int result = sqlite3_open(cFilePath, &_db);
  if (result == SQLITE_OK) {
        NSLog(@"成功打开数据库");
    // 创表
        const char *sql = "CREATE TABLE IF NOT EXISTS t_shop (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, price integer NOT NULL);";
        char *erroMsg = NULL;
        // 创表语句  (exec:执行sql语句)
       sqlite3_exec(_db, sql, NULL, NULL, &erroMsg);
        if (erroMsg) {
            NSLog(@"创表失败--%s", erroMsg);
        }
    }else{
         NSLog(@"打开数据库失败");
    }
  // 移动端在操作数据库的时候一般不会考虑关闭
  // 关闭数据库:sqlite3_close(db);

增加数据

    static int i = 0;
    i++;
    NSString *name = [NSString stringWithFormat:@"name-%d", i];
    
    NSString *sql = [NSString stringWithFormat:@"INSERT INTO t_shop (name, price) VALUES ('%@', %d);", name, i];
    char *errMsg;
    sqlite3_exec(self.db, sql.UTF8String, NULL, NULL, &errMsg);
    NSLog(@"错误信息 = %s", errMsg);

删除数据

// 删除price大于10的数据
    NSString *sql = [NSString stringWithFormat:@"DELETE FROM t_shop where price > 10"];
   char *errMsg;
   sqlite3_exec(self.db, sql.UTF8String, NULL, NULL, NULL);
    NSLog(@"错误信息 = %s", errMsg);

修改数据

// price小于5,设置为0
    NSString *sql = [NSString stringWithFormat:@"UPDATE t_shop set price = 0 where price < 5"];
    char *errMsg;
    sqlite3_exec(self.db, sql.UTF8String, NULL, NULL, &errMsg);
    NSLog(@"%s", errMsg);

查询数据

 // 查询数据如果用  sqlite3_exec(适合执行没有返回数据的操作)  需要用到第三个参数,函数的回调


    // 查询所有字段
    const char *sql = "SELECT * FROM t_shop;";
    // 准备
    //  第三个是sql长度, -1自动计算
    // stmt是用来取出查询结果的
    sqlite3_stmt *stmt = NULL;
    int status = sqlite3_prepare_v2(self.db, sql, -1, &stmt, NULL);
    if (status == SQLITE_OK) { // 准备成功 -- SQL语句正确
        
        // sqlite3_step  步骤(一步一步的执行)
        while (sqlite3_step(stmt) == SQLITE_ROW) { // 成功取出一条数据
            // 后面的数字是列号
            // 列好如果为0,代表的是主键的那列
            const char *name = (const char *)sqlite3_column_text(stmt, 1);
            const char *price = (const char *)sqlite3_column_text(stmt, 2);
            
            NSLog(@"name = %s, price = %s", name, price);
        }
    }
    
FMDB简单用法

FMDB是以0C的方式封装了SQLite的C语言,用起来更加的面相对象,但是SQL语句还是需要写的.这比起单纯的使用SQLite3去做数据存储已经方便了很多,而且FMDB还对数据的存取操作做了线程安全的保证.
下载下来后的FMDB目录结构

FMDB.png

在实际操作过程中,主要用到三个类
FMDatabase:一个FMDatabase对象就代表一个单独的SQLite数据库
FMResultSet:查询后的结果集.
FMDatabaseQueue:用于在多个线程中执行多个查询或更新,保证线程安全的.

  • 简单用法
    创建FMDatabase对象(打开数据库)
// FMDB提供了两个方法去创建FMDatabase对象
// 通过指定数据库文件路径来创建FMDatabase对象
+ (instancetype)databaseWithPath:(NSString*)inPath;
- (instancetype)initWithPath:(NSString*)inPath;
// path文件路径
FMDatabase *db = [FMDatabase databaseWithPath:path];
if (![db open]) {
    NSLog(@"数据库打开失败!");
}

// 关闭数据库
// [db close];

文件路径有三种情况(在FMDB文档里有说明的)
1,具体的系统文件路径,如果不存FMDatabase对象在会自动创建
2,数据库路径为空字符串@"", 会在零食目录创建一个空的数据库,当FMDatabase关闭连接时,数据库文件也会被删除
3,nil,会创建一个内存中临时数据库,当FMDatabase关闭连接时,  数据库也会被销毁.

执行更新

在FMDB中,除查询以外的所有操作,都称为“更新”create、drop、insert、update、delete等
// 下面这些方法在FMDatabase.h文件有详细的说明,建议查看头文件.
使用executeUpdate:方法执行更新,sql和format是数据库字符串
- (BOOL)executeUpdate:(NSString*)sql, ...
- (BOOL)executeUpdateWithFormat:(NSString*)format, ...
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments
// 示例
// 创表
NSString *creatTableSql = @"CREATE TABLE IF NOT EXISTS t_userTable (id integer PRIMARY KEY autoincrement,  name text, price integer)";
[db executeUpdate:creatTableSql];
// 增加数据
[db executeUpdate:@"UPDATE t_shop SET price = ? WHERE name = ?;", @20, @"Jack"] 

执行查询

查询方法在FMDatabase.h文件中,返回的是FMResultSet结果集
常用的查询方法,sql和format是数据库字符串
- (FMResultSet *)executeQuery:(NSString*)sql, ...
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments
// 示例
// 查询数据
FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_shop"];
// 遍历结果集
while ([rs next]) {
    NSString *name = [rs stringForColumn:@"name"];
    int price = [rs intForColumn:@"price"];
} 

// FMDatabaseQueue提供了三个对象方法
//1. 同步队列执行数据库操作。
- (void)inDatabase:(void (^)(FMDatabase *db))block;
//2.使用事务进行同步队列执行数据库操作
- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;
//3.使用递延交易
- (void)inDeferredTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;

在项目中用FMDB做数据的存取的时候需要考虑到线程的安全问题,所以我在使用的时候是这样用的


//  数据库队列
static FMDatabaseQueue *queue = nil;

    if (!queue) {
        NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"fmdb.db"];
        // 该方法会根据路径去获取一个队列,如果路径不存在会自动创建一个数据库
        queue = [FMDatabaseQueue databaseQueueWithPath:path];
    }
    
    if (queue == nil) {
        NSLog(@"创建队列失败");
        return ;
    }

   // 创表
    [queue inDatabase:^(FMDatabase *db) {
        NSString *creatTableSql = @"CREATE TABLE IF NOT EXISTS t_userTable (id integer PRIMARY KEY autoincrement, uid integer, name text, timeStamp integer)";
       [db executeUpdate:creatTableSql];
    }];

// 添加数据
    static int i = 0;
    time_t now;
    time_t timeStamp = time(&now);
    i++;
    [queue inDatabase:^(FMDatabase *db) {
        [db executeUpdateWithFormat:@"INSERT INTO t_userTable (uid, name, timeStamp) VALUES (%d, %@, %ld)", i, @"jack", timeStamp];
    }];
    
// 查询数据
    [queue inDatabase:^(FMDatabase *db) {
        NSString *query = [NSString stringWithFormat:@"SELECT * FROM t_userTable"];
        FMResultSet *set = [db executeQuery:query];
        while ([set next]) {
            NSString *uid = [set stringForColumn:@"uid"];
            NSString *name = [set stringForColumn:@"name"];
            long int timeStamp = [set intForColumn:@"timeStamp"];
        }
    }];

// 修改数据
    NSString *update = [NSString stringWithFormat:@"UPDATE t_userTable set uid = 100 where uid > 10"];
    [queue inDatabase:^(FMDatabase *db) {
        [db executeUpdate:update];
    }];

// 删除数据
    NSString *delete = [NSString stringWithFormat:@"DELETE FROM t_userTable where uid < %zd", 10];
    [queue inDatabase:^(FMDatabase *db) {
        [db executeUpdate:delete];
    }];

关于数据库的详细的知识,后期不忙了再补上.大神们如有详细见解,期望您

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

推荐阅读更多精彩内容