iOS数据存储方式(Core Data/Keycahin/NSUserDefault...)

前言

在iOS开发中数据存储的方式可以归纳为磁盘缓存和内存缓存:磁盘缓存分为两类:文件、数据库存储。

  • 文件存储:NSKeyedArchiver归档、NSUserDefault偏好设置,归档或者plist文件存储可以选择保存到沙盒中,偏好设置系统规定只能保存到沙盒的Library/Preferences目录。
  • 数据库存储:使用FMDB、SQLite,通过SQL直接访问数据库,也可以通过ORM进行对象关系映射访问数据库。这两种方式对应iOS中FMDB、SQLite和Core Data的内容,在此将重点进行分析:
  • 内存缓存:苹果提供NSCache、NSMutableURL通过设置相应策略实现内存缓存

iOS数据持久化方式

  1. NSKeyedArchiver
  2. Keychain
  3. NSUserDefault
  4. Core Data
  5. FMBD
  6. SQLite
  7. Realm

iOS数据临时存储方式

  1. NSCache
  2. NSMutableURL

NSKeyedArchiver(归档)

NSKeyedArchiver 苹果开发文档是这么描述

NSKeyedArchiver, a concrete subclass of NSCoder
, provides a way to encode objects (and scalar values) into an architecture-independent format that can be stored in a file. When you archive a set of objects, the class information and instance variables for each object are written to the archive. NSKeyedArchiver
’s companion class, NSKeyedUnarchiver
, decodes the data in an archive and creates a set of objects equivalent to the original set.

NSKeyedArchiver是NSCoder的具体子类,提供了一种方法来将对象(和标量值)编码成与体系结构无关的格式可以存储在一个文件中。 当您将一组对象归档时,每个对象的类信息和实例变量都被写入到文档中。NSKeyedArchiver的伙伴类,NSKeyedUnarchiver,在一个存档对其解码并且创建一组与原始集相同的对象。

特点:

  • 存储自定义模型对象和Foundation对象数据
    NSKeyedArchiver归档相对较plist存储而言,它可以直接存储自定义模型对象,而plist文件需要将模型转为字典才可以存储自定义对象模型;
  • 归档不能存储大批量数据,存储数据到文件是将所有的数据一下子存储到文件中,从文件中读取数据也是一下子读取所有的数据;

缺点:

添加、删除、更新数据需要将所有数据解档在执行相应处理,数据量大有可能会性能低情况

使用

  • 遵循NSCoding 协议,需要实现两个方法: -initWithCoder:(解档)和 encodeWithCoder:(归档)。
  • 遵循NSCoding协议的类可以被序列化和反序列化,这样可以归档到磁盘上或分发到网络上。

User类

@interface User : NSObject <NSCoding>

@property (nonatomic, copy) NSString *userName;

@property (nonatomic, copy) NSString *password;

@end     

@implementation User

#pragma mark - NSCoding
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    
    if (!(self = [super init])) {
        return nil;
    }
    
    self.userName = [aDecoder decodeObjectForKey:@"userName"];
    
    self.password = [aDecoder decodeObjectForKey:@"password"];
    
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    
    [aCoder encodeObject:self.userName forKey:@"userName"];
    
    [aCoder encodeObject:self.password forKey:@"password"];
    
    
}

- (NSString *)description {
//    if (_userName) {
//        return [NSString stringWithFormat:@"user: %@   password: %@",self.userName,self.password];
//    }
    NSMutableString *str = @"".mutableCopy;
    [str appendString:@"user:{\n"];
    if (self.userName) [str appendFormat:@"  userName:%@,\n", self.userName];
   
    if (self.password) [str appendFormat:@"  service:%@,\n", self.password];
    [str appendString:@"}"];
    return str;
}

@end

NSKeyedArchiverExample:

- (IBAction)addClick:(id)sender {
    
    // 归档
    User *user = [[User alloc] init];
    
    user.userName = @"linmingjun";
    user.password = @"5788";
    
    
    NSMutableString *str = @"".mutableCopy;
    [str appendString:@"LTKeychain:{\n"];
    
    [str appendFormat:@"归档userName:%@   password: %@\n\n", user.userName, user.password];
    
    
    [str appendString:@"}"];
    
    _detailTextView.text = str;

    
}


- (IBAction)selectClick:(id)sender {
    
    // 解档
    User *unUser = [NSKeyedUnarchiver unarchiveObjectWithFile:@"/Users/lmj/Desktop/user"];
    NSMutableString *str = @"".mutableCopy;
    [str appendString:@"LTKeychain:{\n"];
    
    [str appendFormat:@"解档userName:%@   password: %@  \n", unUser.userName , unUser.password];
    
    [str appendString:@"}"];
    
    _detailTextView.text = str;
}

程序运行结果

Keychain(钥匙扣)

Keychain 苹果官方文档是这样描述
Reference:Keychain Service

Computer users typically have to manage multiple accounts that require logins with user IDs and passwords. Secure FTP servers, AppleShare servers, database servers, secure websites, instant messaging accounts, and many other services require authentication before they can be used. Users often respond to this situation by making up very simple, easily remembered passwords, by using the same password over and over, or by writing passwords down where they can be easily found. Any of these cases compromises security.
The Keychain Services API provides a solution to this problem. By making a single call to this API, an application can store login information on a keychain where the application can retrieve the information—also with a single call—when needed. A keychain is an encrypted container that holds passwords for multiple applications and secure services. Keychains are secure storage containers, which means that when the keychain is locked, no one can access its protected contents. In OS X, users can unlock a keychain—thus providing trusted applications access to the contents—by entering a single master password. In iOS, each application always has access to its own keychain items; the user is never asked to unlock the keychain. Whereas in OS X any application can access any keychain item provided the user gives permission, in iOS an application can access only its own keychain items.

计算机用户通常要管理多个账户,需要登录用户id密码。 安全FTP服务器、AppleShare服务器、数据库服务器、安全的网站,即时通讯账号,和许多其他服务要求身份验证才可以使用。 用户经常应对这种情况通过编造很简单,容易记住的密码,通过使用相同的密码,或通过密码写下来,他们可以很容易地找到。

钥匙链服务API提供了一个解决这个问题的。 通过单个调用此API,应用程序可以将登录信息存储在一个钥匙链,应用程序可以使用单个时调用检索信息也需要的。 一个钥匙链是一个加密的容器保存密码用于多个应用程序和安全服务。 钥匙是安全存储容器,这意味着当钥匙链锁着的,没有人可以访问它的保护内容。 在OS X中,用户可以解锁keychain-thus提供受信任应用程序访问内容进入一个主密码。 在iOS,每个应用程序总是能够访问自己的密钥链项;用户不要求解锁钥匙链。 而在OS X的任何应用程序都可以访问任何密钥链项提供了用户赋予权限,在iOS应用程序只能访问自己的密钥链项。


通常情况下,我们用NSUserDefaults存储数据信息,但是对于一些私密信息,比如密码、证书等等,就需要使用更为安全的keychain了。keychain里保存的信息不会因App被删除而丢失,在用户重新安装App后依然有效,数据还在。

功能:

Reference:Keychain Services Programming Guide

大多数iOS应用程序只需要使用钥匙链服务添加一个新密码钥匙链,修改现有的密钥链项,在需要的时候或检索一个密码。 钥匙链完成这些任务的服务提供了以下功能:
SecItemAdd
将一个条目添加到钥匙链

SecItemUpdate
修改现有的密钥链项

SecItemCopyMatching
应用程序通过调用字典包含属性,确定密钥链项。

使用

- (IBAction)addClick:(id)sender {
    
    SetPasswordBlock setupPasswordBlock = ^(NSString * account, NSString * password){
        NSError *error = nil;
        
        [LTKeychain setPassword:account forService:kService account:account error:&error];
        
        return error;
        
    };
    
    //    NSError *error = setupPasswordBlock(@"hell",@"123");
    
    NSString *account = @"LinMingJun";
    NSString *password = @"MyAccount";
    NSString *account2 = @"LinMingjunTwo";
    NSString *password2 = @"MyAccount";
    
    NSError *setPasswordError = setupPasswordBlock(account,password);
    NSError *setPasswordError2 = setupPasswordBlock(account2,password2);
    
    NSMutableString *str = @"".mutableCopy;
    [str appendString:@"LTKeychain:{\n"];
    
    [str appendFormat:@"添加account:%@   setPasswordError%@: \n\n", account, setPasswordError ? @"失败" : @"成功"];
    
    [str appendFormat:@"添加account2:%@   setPasswordError2%@: %@\n\n", account, setPasswordError2 ? @"失败" : @"成功", setPasswordError2];
    
    
    [str appendString:@"}"];
    
    _detailTextView.text = str;
    
    
}


- (IBAction)deleteClick:(id)sender {
    // delete
    NSMutableString *str = @"".mutableCopy;
    [str appendString:@"LTKeychain:{\n"];
    
    if ([LTKeychain deletePassWordForService:kService account:@"LinMingJun" error:nil]) {
        [str appendFormat:@"delete success! \n\n"];
    
    } else {
        [str appendFormat:@"delete fail! \n\n"];
    }
    
    [str appendString:@"}"];
    
    _detailTextView.text = str;
}
- (IBAction)updateClick:(id)sender {
    
    
    
}
- (IBAction)selectClick:(id)sender {
    // select password
    NSError *error = nil;
    NSString *password =  [LTKeychain passwordForService:kService account:@"MyAccount" error:&error];
    
    NSArray *array = [LTKeychain allAccounts];

    NSMutableString *str = @"".mutableCopy;
    [str appendString:@"LTKeychain:{\n"];

    
    [str appendFormat:@"select: password = %@ \n\n", password];

    [str appendFormat:@"select: array = %@ \n\n", array];
    
    [str appendString:@"}"];
    
    _detailTextView.text = str;
    
}
程序运行截图

<b>根据YYKeyChain提供的思路,自己书写了一遍 </b>

LTKeychain.h

//
//  LTKeychain.h
//  DataStorageExample
//
//  Created by lmj  on 16/5/26.
//  Copyright (c) 2016年 linmingjun. All rights reserved.
//  https://developer.apple.com/library/mac/documentation/Security/Reference/keychainservices/

#import <Foundation/Foundation.h>
#import <Security/Security.h>

@class LTKeychainQuery;
@interface LTKeychain : NSObject

#pragma mark - Classic methods

/**
*/
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account;

/**
*/
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account;

/**
*/
+ (BOOL)deletePassWordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
+ (BOOL)deletePassWordForService:(NSString *)serviceName account:(NSString *)account;


/**
*/
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account;


/**
*/
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;


/**
*/
+ (NSArray *)allAccounts;
+ (NSArray *)allAccounts:(NSError *)error;
+ (NSArray *)accountsForService:(NSString *)serviceName;
+ (NSArray *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error;

@end


/*! Similar.h 
***********************************************
*** OSStatus values unique to Security APIs ***
***********************************************
enum
{
errSecSuccess                               = 0,        No error.
errSecUnimplemented                         = -4,       Function or operation not implemented.
errSecIO                                    = -36,     I/O error (bummers)
errSecOpWr                                  = -49,     file already open with with write permission
errSecParam                                 = -50,      One or more parameters passed to a function where not valid.
errSecAllocate                              = -108,     Failed to allocate memory.
errSecUserCanceled                          = -128,     User canceled the operation.
errSecBadReq                                = -909,     Bad parameter or invalid state for operation.
errSecInternalComponent                     = -2070,
errSecNotAvailable                          = -25291,   No keychain is available. You may need to restart your computer.
errSecDuplicateItem                         = -25299,   The specified item already exists in the keychain.
errSecItemNotFound                          = -25300,   The specified item could not be found in the keychain.
errSecInteractionNotAllowed                 = -25308,   User interaction is not allowed.
errSecDecode                                = -26275,   Unable to decode the provided data.
errSecAuthFailed                            = -25293,   The user name or passphrase you entered is not correct.
};
*/
typedef NS_ENUM(NSUInteger, LTKeychainErrorParam) {
LTKeychainErrorParamSuccess = 0,
LTKeychainErrorParamUnimplemented = -4,
LTKeychainErrorParamIO = -36,
LTKeychainErrorParamOpWr,
LTKeychainErrorParamParam,
LTKeychainErrorParamAllocate,
LTKeychainErrorParamUserCanceled,
LTKeychainErrorParamBadReq,
LTKeychainErrorParamInternalComponent,
LTKeychainErrorParamNotAvailable,
LTKeychainErrorParamDuplicationItem,
LTKeychainErrorParamItemNotFound,
LTKeychainErrorParamInteractionNotAllowed,
LTKeychainErrorParamDecode,
LTKeychainErrorParamAuthFailed,
};




/*!
@enum kSecAttrAccessible Value Constants
@discussion Predefined item attribute constants used to get or set values
in a dictionary. The kSecAttrAccessible constant is the key and its
value is one of the constants defined here.
When asking SecItemCopyMatching to return the item's data, the error
errSecInteractionNotAllowed will be returned if the item's data is not
available until a device unlock occurs.
@constant kSecAttrAccessibleWhenUnlocked Item data can only be accessed
while the device is unlocked. This is recommended for items that only
need be accesible while the application is in the foreground.  Items
with this attribute will migrate to a new device when using encrypted
backups.
@constant kSecAttrAccessibleAfterFirstUnlock Item data can only be
accessed once the device has been unlocked after a restart.  This is
recommended for items that need to be accesible by background
applications. Items with this attribute will migrate to a new device
when using encrypted backups.
@constant kSecAttrAccessibleAlways Item data can always be accessed
regardless of the lock state of the device.  This is not recommended
for anything except system use. Items with this attribute will migrate
to a new device when using encrypted backups.
@constant kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly Item data can
only be accessed while the device is unlocked. This class is only
available if a passcode is set on the device. This is recommended for
items that only need to be accessible while the application is in the
foreground. Items with this attribute will never migrate to a new
device, so after a backup is restored to a new device, these items
will be missing. No items can be stored in this class on devices
without a passcode. Disabling the device passcode will cause all
items in this class to be deleted.
@constant kSecAttrAccessibleWhenUnlockedThisDeviceOnly Item data can only
be accessed while the device is unlocked. This is recommended for items
that only need be accesible while the application is in the foreground.
Items with this attribute will never migrate to a new device, so after
a backup is restored to a new device, these items will be missing.
@constant kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly Item data can
only be accessed once the device has been unlocked after a restart.
This is recommended for items that need to be accessible by background
applications. Items with this attribute will never migrate to a new
device, so after a backup is restored to a new device these items will
be missing.
@constant kSecAttrAccessibleAlwaysThisDeviceOnly Item data can always
be accessed regardless of the lock state of the device.  This option
is not recommended for anything except system use. Items with this
attribute will never migrate to a new device, so after a backup is
restored to a new device, these items will be missing.
*/

typedef NS_ENUM(NSUInteger, LTKeychainAccessible) {
LTKeychainAccessibleNone = 0, ///< no value
LTKeychainAccessibleWhenUnlocked,
LTKeychainAccessibleAfterFirstUnlock,
LTKeychainAccessibleAlways,
LTKeychainAccessibleWhenPasscodeSetThisDeviceOnly,
LTKeychainAccessibleWhenUnlockedThisDeviceOnly,
LTKeychainAccessibleAfterFirstUnlockThisDeviceOnly,
LTKeychainAccessibleAlwaysThisDeviceOnly,
};



typedef NS_ENUM(NSUInteger, LTKeychainSynchronizationMode) {
LTKeychainSynchronizationModeAny = 0,

LTKeychainSynchronizationModeNO,

LTKeychainSynchronizationModeYES,

};



/**
Similar SecItem.h

@enum Attribute Key Constants
@discussion Predefined item attribute keys used to get or set values in a
dictionary. Not all attributes apply to each item class. The table
below lists the currently defined attributes for each item class:

kSecClassGenericPassword item attributes:
kSecAttrAccessible
kSecAttrAccessControl
kSecAttrAccessGroup
kSecAttrCreationDate
kSecAttrModificationDate
kSecAttrDescription
kSecAttrComment
kSecAttrCreator
kSecAttrType
kSecAttrLabel
kSecAttrIsInvisible
kSecAttrIsNegative
kSecAttrAccount
kSecAttrService
kSecAttrGeneric

typedef 
kSecAttrSynchronizable

*/

@interface LTKeychainQuery : NSObject

/** item attribute keys  */
@property (nonatomic, copy) NSString *service;

@property (nonatomic, copy) NSString *account;

@property (nonatomic, copy) NSString *label;

@property (nonatomic, copy) NSNumber *type;

@property (nonatomic, copy) NSString *creator;

@property (nonatomic, copy) NSNumber *comment;

@property (nonatomic, copy) NSNumber *description;

@property (nonatomic, readonly, strong) NSDate *modificationDate;

@property (nonatomic, readonly, strong) NSDate *creationDate;

@property (nonatomic, copy) NSString *accessGroup;

/** password */

/**
Convenience accessor for setting and getting a password string. Passes through
to `passwordData` using UTF-8 string encoding.
*/
@property (nonatomic, copy) NSString *password;

/** Root storage for password information */
@property (nonatomic, copy) NSData *passwordData;

/**
This property automatically transitions between an object and the value of
`passwordData` using NSKeyedArchiver and NSKeyedUnarchiver.
*/
@property (nonatomic, copy) id<NSCoding> passwordObject;

/** Accessible strategy */
@property (nonatomic) LTKeychainAccessible accessible; //  kSecAttrAccessible
/** 
Specifies a dictionary key whose value is
a CFBooleanRef indicating whether the item in question can be synchronized. 

1. To add a new item which can be synced to other devices, or to obtain
synchronizable results from a query, supply this key with a value of
“kCFBooleanTrue”

2. or has a value of
“kCFBooleanFalse”, then no synchronizable items will be added or returned.

3. A predefined value, "kSecAttrSynchronizableAny", may be provided instead of
kCFBooleanTrue if both synchronizable and non-synchronizable results are
desired.


return type CFTypeRef
*/
@property (nonatomic) LTKeychainSynchronizationMode synchronizable NS_AVAILABLE_IOS(7_0); //  kSecAttrSynchronizable



- (BOOL)saveItem:(NSError **)error;


- (BOOL)deleteItem:(NSError **)error;


- (NSArray *)selectAllItems:(NSError **)error;

- (BOOL)selectItem:(NSError **)error;




@end

NSUserDefault(偏好设置)

苹果官方文档:NSUserDeulat

NSUserDefaults适合存储轻量级的本地数据,存储保存引导页页的数据或用户名、密码,

特点:

  • NSUserDefaults存储的数据类型有:NSNumber、NSString、NSDate、NSArray、NSDictionary、BOOL.
    NSInteger、float、double
  • 调用 synchronize 立刻同步数据到文件内

使用

LTNSUserDefault工具类

@interface LTNSUserDefault : NSObject


+ (void)addUserDefaultArrayFromStr:(NSString *)text;

+ (NSArray *)arrayForKey;

+ (void)addUserDefaultObject:(id)object   key:(NSString *)key ;

+ (void)removeUserDefaultObjectFromKey:(NSString *)key;

+ (nullable id)objectForKey:(NSString *)key;

+ (void)removeAllArray;

@end


#import "LTNSUserDefault.h"

static NSString *const kArrayKey = @"array";

@implementation LTNSUserDefault

+ (void)addUserDefaultArrayFromStr:(NSString *)text {
    
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    
    NSArray *array = [userDefaults arrayForKey:kArrayKey];

    if (array.count > 0) {
        
    } else {
        array = [NSArray array];
    }
    
    NSMutableArray *mutableArray = [array mutableCopy];
    [mutableArray addObject:text];
    if (mutableArray.count > 4) {
        [mutableArray removeObjectAtIndex:0];
    }
    
    [userDefaults setObject:mutableArray forKey:kArrayKey];
    [userDefaults synchronize];
}

+ (NSArray *)arrayForKey {
    NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
    
    
    return [userDefault arrayForKey:kArrayKey];
    
}

+ (void)removeAllArray {
    NSUserDefaults * userDefault = [NSUserDefaults standardUserDefaults];
    [userDefault removeObjectForKey:kArrayKey];
    [userDefault synchronize];
}


+ (void)removeUserDefaultObjectFromKey:(NSString *)key {
    NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
    
    [userDefault removeObjectForKey:key];
    
    [userDefault synchronize];
}

+ (id)objectForKey:(NSString *)key {
    NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
    
    return  [userDefault objectForKey:key];
    
}

+ (void)addUserDefaultObject:(id)object key:(NSString *)key {
    
    NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
    
    [userDefault setObject:object forKey:key];
    
    [userDefault synchronize];
}

NSUserDefaultExample:

#import <UIKit/UIKit.h>

@interface NSUserDefaultExample : UIViewController
@property (weak, nonatomic) IBOutlet UIButton *addBtn;
@property (weak, nonatomic) IBOutlet UIButton *deleteBtn;
@property (weak, nonatomic) IBOutlet UITextView *detailTextView;

@end




#import "NSUserDefaultExample.h"
#import "LTNSUserDefault.h"



static NSString *const kAccount = @"Account";
static NSString *const kStrName = @"Name";
static NSString *const kFloatValue = @"Float";
static NSString *const kDoubleValue = @"Double";
static NSString *const kDictionaryValue = @"Dictionary";
static NSString *const kArrayValue = @"NSArray";
static NSString *const kArray = @"array";


@interface NSUserDefaultExample ()

@end

@implementation NSUserDefaultExample

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
}

- (IBAction)addClick:(id)sender {
    
    [LTNSUserDefault addUserDefaultArrayFromStr:@"1800"];
    [LTNSUserDefault addUserDefaultArrayFromStr:@"1200"];
    [LTNSUserDefault addUserDefaultArrayFromStr:@"1803"];
    [LTNSUserDefault addUserDefaultArrayFromStr:@"1801"];
    [LTNSUserDefault addUserDefaultArrayFromStr:@"1000"];
    
    
    
    [LTNSUserDefault addUserDefaultObject:@"linmingjun" key:kAccount];
    [LTNSUserDefault addUserDefaultObject:@"1.0" key:kFloatValue];
    [LTNSUserDefault addUserDefaultObject:@"2.00" key:kDoubleValue];
    [LTNSUserDefault addUserDefaultObject:@{ @"Name" : @"linmingjun",
                                            @"blogs" : @"http://www.jianshu.com"} key:kDictionaryValue];
    [LTNSUserDefault addUserDefaultObject:@"linmingjun" key:kAccount];
    
    
    NSMutableString *str = @"".mutableCopy;
    [str appendString:@"LTNSUserDefault:{\n"];
    
    [str appendFormat:@"addUserDefaultArrayFromStr: %@    \n",[LTNSUserDefault arrayForKey]];
    
    
    [str appendFormat:@"addUserDefaultObject: %@    \n",[LTNSUserDefault objectForKey:@"kAccount"]];
    
    [str appendString:@"}"];
    
    
    _detailTextView.text = str;
    
}


- (IBAction)deleteClick:(id)sender {
    
    
    NSMutableString *str = @"".mutableCopy;
    [str appendString:@"LTNSUserDefault:{\n"];

    [str appendFormat:@"account: %@    \n",[LTNSUserDefault objectForKey:kAccount]];
    
    
    [str appendString:@"}"];
    
    _detailTextView.text = str;
}

演示图

程序运行界面

Core Data

Reference: Core Data苹果官方文档
Core Data苹果官方文档

基本概念

Core Data is a framework that you use to manage the model layer objects in your application. It provides generalized and automated solutions to common tasks associated with object life cycle and object graph management, including persistence.

Core Data typically decreases by 50 to 70 percent the amount of code you write to support the model layer. This is primarily due to the following built-in features that you do not have to implement, test, or optimize:

Core Data是一个框架,用于在应用程序中管理对象模型层。 它提供了广义和自动化来解决常见的任务与对象生命周期和有关包括持久性对象图管理。

Core Data通常减少50 - 70%编写的代码来支持模型层。 这主要是由于以下的内置功能,你不需要实现,测试,或优化:

功能

  • 更改跟踪和内置管理之外的撤销和重做基本的文本编辑。
  • 维护变化的传播,包括维护对象之间的一致性关系。
  • 对象延迟加载,部分物化期货(断层),即写即拷实现数据共享,减少开销。
  • 自动验证的属性值。 管理对象扩展标准键值编码验证方法,以确保在可接受范围内的单个值都位于可接受的范围内,从而使值的组合有意义。
  • 模式迁移工具,简化模式变化和允许您执行高效的就地模式迁移。
  • 可选的集成与应用程序的控制器层支持用户界面同步。
  • 分组、过滤和组织内存中的数据和用户界面。
  • 支持自动将对象存储在外部数据存储库。
  • 复杂的查询编译。 而不是编写SQL,您可以创建复杂的查询与获* 取请求一个NSPredicate对象相关联。
  • 跟踪和乐观锁定支持自动multiwriter解决冲突的版本。
  • 与OS X和iOS工具链的有效整合。

Core Data 具体的设计

Core Data核心类关系图

在 CoreData 中,有四大核心类:

  • Persistent Store Coordinator:持久化存储协调器;被管理对象模型和实体类之间的转换协调器,可以为不同的被管理对象模型创建各自的持久化存储协调器,一般情况下会合并多个被管理对象模型,然后创建一个持久化存储协调器统一来管理

  • Managed Object Model:对象模型;对应 Xcode 中创建的 .xcdatamodeld 对象模型文件

  • Persistent Object Store:存储持久对象的数据库,CoreData 提供数据存储类型有四种,分别为 NSSQLiteStoreType (SQLite 数据库)NSXMLStoreType(XML 文件)、NSBinaryStoreType(二进制文件)、NSInMemoryStoreType(内存中),一般使用 NSSQLiteStoreType(SQLite数据库存储)

  • Managed Object Context:管理对象上下文;负责实体对象与数据库的数据交互

[创建一个托管对象模型]

创建一个托管对象模型和一个实体及其属性,关键步骤如成绩管理数据表关系图所示

Students (学生表)
Scores (分数表)
Courses (课程表)
Classes (班级表)

表设计

模型创建的过程中需要注意:

  • 所有的属性应该指定具体类型(尽管在SQLite中可以不指定),因为实体对象会对应生成ObjC模型类。
  • 实体对象中其他实体对象类型的属性应该通过Relationships建立,并且注意实体之间的对应关系。

** Student与Course 之间多对多的“选修”联系,即一个学生可以修选多门课程(一对多),一个课程提供多个同学选修(一对多) **

** Classes与Student存在一对多的“归属”关系,即一个班由多个学生组成,一个学生只能归属某个班级 **

** 同一个学生在同一个学期下不允许修读同一门课程(Score与 Course、Student分别都是一对一的关系 )**


成绩管理数据表关系图
  • NSSet 是一个无序的集合 ,可以使用NSSet 与NSArray根据需要转换使用,其他使用方法 同单表使用的增删查改,只是访问时多层访问
    .
  • @synthesize :没有自定义存取方法时, 编译器系统自动生成 getter/setter方法
  • @dynamic :不自动生成getter/setter方法,由自己实现存取方法 或 在运行时动态创建绑定 ,如coreData在实现NSManagedObject子类时使用,由Core Data在运行时动态生成

Delete Rule

  • No Action:当A被删除时,B对象不变,但会指向一个不存在的对象,一般不建议使用;

  • Nullify(作废):当A对象被删除时,B对象指向的A对象会置为空,如果A与B的关系式一对多,则是A对象从B容器中移除

  • Cascade(级联):当A对象被删除时,A对象指向的B对象也会被删除;

  • Deny(拒绝):当删除指向对象B存在的A对象时,操作将会被拒绝;

初始化核心数据堆栈

Core Data堆栈框架对象的集合,被评估为Core Data的初始化的一部分,并在你的应用程序和外部数据存储对象之间的调解。核心数据栈处理所有与外部数据存储区的交互,以便您的应用程序可以集中于它的业务逻辑。堆栈包含三个主要目标:

  • 管理对象上下文(NSManagedObjectContext):就是你的应用程序将以最互动的对象

  • 持久性存储协调员(NSPersistentStoreCoordinator):坐在中间的Core Data堆栈, 协调员负责实现实例的实体定义模型的内部。 创建新实例的实体模型,从持久存储和检索现有实例,可以在磁盘或持久化存储在内存中。

  • 管理对象模型(NSManagedObjectModel):
    实例描述了数据访问的就是Core Data堆栈。 在Core Data的创建堆栈,NSManagedObjectModel通常被称为“mom”,加载到内存的堆栈的创建的第一步

在初始化Core Data堆栈访问你的应用程序数据之前。堆的初始化准备Core Data为数据请求和数据的创建。下面是如何创建一个Core Data堆栈的例子。

CoreDataManager:

重点

As part of the initialization of Core Data, assign the adding of the persistent store (NSPersistentStore
) to the persistent store coordinator (NSPersistentStoreCoordinator
) to a background queue. That action can take an unknown amount of time, and performing it on the main queue can block the user interface, possibly causing the application to terminate.

NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error];

苹果官方文档有提到指定添加持久性存储(NSPersistentStore)到持久存储协调员(NSPersistentStoreCoordinator)这个动作要加入到后台队列。在主队列执行可能阻止用户界面,可能会导致应用程序终止。

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@interface CoreDataManager : NSObject


@property (nonatomic, strong) NSManagedObjectContext *context;

- (NSManagedObjectContext *)initalizeCoreData;

+ (CoreDataManager *)sharedManager;

@end



#import "CoreDataManager.h"

@implementation CoreDataManager

- (instancetype)init {
    
    if (!(self = [super init])) {
        return nil;
    }
    
    _context =  [self initalizeCoreData];
    
    return self;
}

+ (CoreDataManager *)sharedManager {
    
    static CoreDataManager *manager;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[CoreDataManager alloc] init];
    });
    
    return manager;
}

- (NSManagedObjectContext *)initalizeCoreData {
    /**
     创建模型比较简单,我们只需要增加一个新文件到我们的项目,在 Core Data 选项中选择 Data Model template。这个模型文件将会被编译成后缀名为 .momd 类型的文件,我们将会在运行时加载这个文件来为持久化存储创建需要用的 NSManagedObjectModel,模型的源码是简单的 XML
     */
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Workspace" withExtension:@"momd"];
    
    // 指定本地资源文件路径,打开「被管理对象模型」文件
    NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    
    /** 第二种方式打开对象模型文件
     // 打开「被管理对象模型」文件,参数为 nil 则打开包中所有模型文件并合并成一个
     NSManagedObjectModel *model=[NSManagedObjectModel mergedModelFromBundles:nil];
     */
    
    NSAssert(mom != nil, @"Error initializing Managed Object Model");
    
    // 创建 持久化存储协调器
    NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
    
    // 创建被管理对象上下文,参数:NSMainQueueConcurrencyType 指定上下文将与主队列有关。
    NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    // 设置被管理对象上下文的持久化存储协调器
    [moc setPersistentStoreCoordinator:psc];
    [self setContext:moc];
    
    // 获取数据库保存路径,通常保存沙盒 Documents 目录下
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *documentURL = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    // SQLite 在应用程序的文件目录
    NSURL *storeURL = [documentURL URLByAppendingPathComponent:@"DataModel.sqlite"];
    
    // 将持久化存储协调器设置到被管理对象上下文后,开启线程异步将Block作为一个任务加入到全局并发队列执行(设置队列优先级为默认)
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSError *error = nil;
        // 获取被管理对象上下文的持久化存储协调器
        NSPersistentStoreCoordinator *psc = [[self context] persistentStoreCoordinator];
        // 通过获取持久化存储协调器,添加SQLite 类型的持久化存储
        // 指定添加持久性存储(NSPersistentStore)到持久存储协调员(NSPersistentStoreCoordinator)这个动作要加入到后台队列。在主队列执行可以阻止用户界面,可能会导致应用程序终止。
        NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error];
        NSAssert(store != nil, @"Error initializing PSC: %@\n%@", [error localizedDescription], [error userInfo]);
    });
    
    
    return moc;
}

/*
 NSPersistentStoreCoordinator.h
 // Persistent store types supported by Core Data: 持久化存储支持core data的类型
 COREDATA_EXTERN NSString * const NSSQLiteStoreType NS_AVAILABLE(10_4, 3_0);
 COREDATA_EXTERN NSString * const NSXMLStoreType NS_AVAILABLE(10_4, NA);
 COREDATA_EXTERN NSString * const NSBinaryStoreType NS_AVAILABLE(10_4, 3_0);
 COREDATA_EXTERN NSString * const NSInMemoryStoreType NS_AVAILABLE(10_4, 3_0);
 
 */


@end





#import "CoreDataWorkspaceService.h"

#import "CoreDataManager.h"

static NSString *const kStudents = @"Students";

@implementation CoreDataWorkspaceService

+ (CoreDataWorkspaceService *)sharedService {
    
    static CoreDataWorkspaceService *service;
    
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken,^{
        service = [CoreDataWorkspaceService new];
        service.context = [CoreDataManager sharedManager].context;
        
        NSLog(@"沙盒路径:%@",NSHomeDirectory());
    });
    
    return service;
}

@end


@implementation StudentsSerivce

+ (StudentsSerivce *)sharedService {
    
    static StudentsSerivce *service;
    
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken,^{
        service = [StudentsSerivce new];
        service.context = [CoreDataManager sharedManager].context;
        
        NSLog(@"沙盒路径:%@",NSHomeDirectory());
    });
    
    return service;
}

- (NSManagedObjectContext *)context {
    return [CoreDataManager sharedManager].context;
}



/**
 @property (nullable, nonatomic, retain) NSString *studentDes;
 @property (nullable, nonatomic, retain) NSString *studentName;
 @property (nullable, nonatomic, retain) NSString *studentPwd;
 @property (nullable, nonatomic, retain) Classes *student_class;
 @property (nullable, nonatomic, retain) NSSet<Courses *> *student_course;
 @property (nullable, nonatomic, retain) Scores *student_score;
 */
- (BOOL)addStudents:(Students *)students {
    BOOL isSuccess = NO;
    Students *s = [NSEntityDescription insertNewObjectForEntityForName:@"Students" inManagedObjectContext:[self context]];
    
    s.studentId = students.studentId;
    s.studentName = students.studentName;
    s.studentPwd = students.studentPwd;
    s.student_class = students.student_class;
    s.student_course = students.student_course;
    s.student_score = students.student_score;
    NSError *error = nil;
    isSuccess = [[self context] save:&error];
    NSAssert(isSuccess, @"Error info : %@\n",  [error userInfo]);
    return isSuccess;
}

- (BOOL)addStudentWithName:(NSString *)studentId studentName:(NSString *)studentName studentDesc:(NSString *)studentDesc studentPwd:(NSString *)studentPwd{
    BOOL isSuccess = NO;
    Students *s = [NSEntityDescription insertNewObjectForEntityForName:@"Students" inManagedObjectContext:[self context]];
    
    s.studentId = studentId;
    s.studentName = studentName;
    s.studentDes = studentDesc;
    s.studentPwd = studentPwd;
//    s.student_class = students.student_class;
//    s.student_course = students.student_course;
//    s.student_score = students.student_score;
    NSError *error = nil;
    isSuccess = [[self context] save:&error];
    NSAssert(isSuccess, @"Error info : %@\n",  [error userInfo]);
    return isSuccess;
}

- (BOOL)removeStudentsByID:(NSString *)studentId {
    BOOL isSuccess = NO;
    
    Students *s = [self getStudentsByID:studentId];
    
    if (s) {
        NSError *error = nil;
//        [[self context] deletedObjects:s];
        [[self context] deleteObject:s];
        isSuccess = [[self context] save:&error];
        NSAssert(isSuccess, @"delete Error info : %@\n",  [error localizedDescription]);
    }
    
    
    return isSuccess;
}

- (void)removeStudentsForID:(NSString *)studentId {
    Students *s = [self getStudentsByID:studentId];
    
    if (s) {
        NSError *error = nil;
//                [[self context] deletedObjects:s];
        [[self context] deleteObject:s];
        BOOL isSuccess =  [[self context] save:&error];
    }

}

/**
 @property (nullable, nonatomic, retain) NSString *studentDes;
 @property (nullable, nonatomic, retain) NSString *studentName;
 @property (nullable, nonatomic, retain) NSString *studentPwd;
 @property (nullable, nonatomic, retain) Classes *student_class;
 @property (nullable, nonatomic, retain) NSSet<Courses *> *student_course;
 @property (nullable, nonatomic, retain) Scores *student_score;
 */
- (BOOL)updateStudent:(Students *)student {
    BOOL isSuccess = NO;
    
    Students *s = [self getStudentsByID:student.studentId];
    
    if (s) {
        NSError *error = nil;
        
        s.studentDes = student.studentDes;
        s.studentName = student.studentName;
        s.studentPwd = student.studentPwd;
        s.student_class = student.student_class;
        s.student_course = student.student_course;
        s.student_score = student.student_score;
        
        
        [[self context] deleteObject:s];
        isSuccess = [[self context] save:&error];
        NSAssert(isSuccess, @"delete Error info : %@\n",  [error localizedDescription]);
    }
    
    
    return isSuccess;

}

- (NSMutableArray *)getAllStudents {
    NSMutableArray *arrayRuture = @[].mutableCopy;
    
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:kStudents];
    
    NSError *error = nil;
    
    NSArray *array = [[self context] executeFetchRequest:request error:&error];
    
    if (!error) {
        for (Students *s in array) {
            [arrayRuture addObject:s];
        }
    }
    
    NSAssert(arrayRuture.count, @"Error fetching Students objects:Error Info:%@ \n",  [error localizedDescription]);
    
    return arrayRuture;
}

- (Students *)getStudentsByName:(NSString *)name {
    Students *studentsInfo;
    // 实例化查询
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:kStudents];
    // 使用谓词查询是基于 Keypath 查询的,如果键是一个变量,格式化字符串时需要使用 %K 而不是 %@
    request.predicate = [NSPredicate predicateWithFormat:@"%K = %@","studentName",name];
    NSError *error = nil;
    // 执行查询
    NSArray *arrayResult = [self.context executeFetchRequest:request error:&error];
    
    if (!error) {
        studentsInfo = [arrayResult firstObject];
    } else {
        NSLog(@"select data Error,Error Info:%@", error.localizedDescription);
    }
    
    return studentsInfo;

}

- (Students *)getStudentsByID:(NSString *)studentId {
    
    Students *studentsInfo;
    // 实例化查询
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:kStudents];
    // 使用谓词查询是基于 Keypath 查询的,如果键是一个变量,格式化字符串时需要使用 %K 而不是 %@

    request.predicate = [NSPredicate predicateWithFormat:@"%K=%@",@"studentId",studentId];
    NSError *error = nil;
    // 执行查询
    NSArray *arrayResult = [[self context] executeFetchRequest:request error:&error];
    
    if (!error) {
        studentsInfo = [arrayResult firstObject];
    } else {
        NSLog(@"select data Error,Error Info:%@", error.localizedDescription);
    }

    return studentsInfo;
    
}
@end


- (Students *)getStudentsByName:(NSString *)name;



@end

生成的数据库相关文件

Reference:http://www.sqlite.org/fileformat2.html#walindexformat
databaseName.db:数据库文件
databaseName.db-shm Shared Memory:数据库预写式日志索引文件
databaseName.db-wal Write-Ahead Log:数据库预写式日志文件

具体操作界面图

生成数据库表字段

Z_PK 是表的主键,从1开始递增,唯一值
Z_ENT 表在xcdatamodel 中的索引值,创建了4个表,Z_ENT的区间就是[1,4 ]
Z_OPT 表示的是每条数据被操作的次数,初始化值为1,只要是增删改查都会加1

打印SQL语句

在 Produc-Scheme-Edit Scheme-Run菜单下的Arguments中的Arguments Passed On Launch下按先后顺序新增两个项,内容分别为-com.apple.CoreData.SQLDebug和1


D29FC78D-2738-476D-898D-A752B69C56FA.png
打印SQL

CoreData + UITableView的使用

如下图效果图,具体实现详见下载源码


效果图

NSFetchedResultsController+CoreData + UITableView的使用

CoreData为UITableView提供数据的时候,使用NSFetchedReslutsController读取数据,能最大效率的读取数据库并且提供高效的查询分类功能,数据以分组的形式 展示在UITableView中.

特点:

  • 检测数据变化
  • 当数据发生变化时,点对点的更新TableView,大大的提高了更新效率
  • 点对点的更新分组

使用

sectionNameKeyPath: 获取的对象进行分组的参数.
cacheName:返回使用给定参数初始化的获取请求控制器,应用是用来处理重复的任务,比如说设置分组或者排列数据等

创建一个NSFetchedResultsController控制器设置分组、排序、缓存:
- (void)initializeFetchedResultsController {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:kStudents];

    NSSortDescriptor *lastNameSort = [NSSortDescriptor sortDescriptorWithKey:@"studentId" ascending:YES];
    
    
    [request setSortDescriptors:@[lastNameSort]];
    
    CoreDataManager *manager = [CoreDataManager sharedManager];
    
    NSManagedObjectContext *moc = manager.context;
    
    
    NSFetchedResultsController * resultController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:@"student_class" cacheName:@"rootCache"];
    resultController.delegate = self;
    
    self.fetchedResultsController = resultController;
    
    
    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        NSLog(@"Failed to initialize FetchedResultsController: %@\n%@", [error localizedDescription], [error userInfo]);
        abort();
    }

}

当数据发生变化时,点对点的更新tableview

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [_contentView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete:
            [_contentView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeUpdate:
            [self configureCell:[self.contentView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;
        case NSFetchedResultsChangeMove:
            [_contentView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [_contentView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

点对点的更新section

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [_contentView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete:
            [_contentView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeMove:
        case NSFetchedResultsChangeUpdate:
            break;
    }
}

显示分组
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id< NSFetchedResultsSectionInfo> sectionInfo = [[self fetchedResultsController] sections][section];
NSArray *array = [sectionInfo objects];

    Students *s = [array firstObject];
    
    Classes *c = s.student_class;
    
    return c.classNames;
}

将NSFetchedResultsController与UITableView数据源相结合
#pragma mark - Table view data source

- (void)configureCell:(LTTableViewCell *)cell atIndexPath:(NSIndexPath*)indexPath
{
    
    if (!cell) {
//        cell = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([LTTableViewCell class]) owner:nil options:nil] lastObject];
        cell = [[LTTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellReuseIdentifier];
    }
    Students *student = [[self fetchedResultsController] objectAtIndexPath:indexPath];
    cell.tag = [student.studentId integerValue]; // 存储 ID 用于「修改」和「删除」记录操作
    cell.model = student;

}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 80.0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    LTTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellReuseIdentifier];
    [self configureCell:cell atIndexPath:indexPath];
    cell.rightUtilityButtons = [self rightButtons];
    cell.delegate = self;
    
    return cell;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    NSLog(@"[[[self fetchedResultsController] sections] count]---%ld",[[[self fetchedResultsController] sections] count]);
    return [[[self fetchedResultsController] sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
    id< NSFetchedResultsSectionInfo> sectionInfo = [[self fetchedResultsController] sections][section];
    return [sectionInfo numberOfObjects];
}

效果图

程序运行界面

未完待续

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

推荐阅读更多精彩内容