iOS 中数据持久化的几种方式

本文由我们团队卢方莹 童鞋组内分享后总结。


  • 文件
  • 归档(NSKeyedArchiver)
  • 属性列表(NSUserDefaults)
  • 数据库(SQLite、CoreData、第三方类库)

一、文件

  • 应用程序包: 这里面存放的是应用程序的源文件,包括资源文件和可执行文件。NSString *path = [[NSBundle mainBundle] bundlePath];

  • /Documents:最常用的目录,iTunes同步该应用时会同步此文件夹中的内容,适合存储重要数据。NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;

  • Library/Caches: iTunes不会同步此文件夹,适合存储体积大,不需要备份的非重要数据。NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;

  • Library/Preferences: iTunes同步该应用时会同步此文件夹中的内容,通常保存应用的设置信息。NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

  • /tmp: iTunes不会同步此文件夹,系统可能在应用没运行时就删除该目录下的文件,所以此目录适合保存应用中的一些临时文件,用完就删除。NSString *path = NSTemporaryDirectory();

1
2
3
NSString *filePath = [[self getDocumentPath] stringByAppendingString:@"fileTest.txt"];
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:@"798293@qq.com", @"email", @"787907@qq.com", @"emailDisplay", nil];
[dictionary writeToFile:filePath atomically:YES];

二、归档

归档(又名序列化),把对象转为字节码,以文件的形式存储到磁盘上,程序运行过程中或者再次重新打开程序的时候,可以通过解归档(返序列化)还原这些对象。

  • 归档的对象是Foundation框架中的对象
  • 归档和解归档其中任意对象都需要归档和解归档整个文件
  • 归档后的文件是加密的,所以归档文件的扩展名可以随意取
  • 在带键的归档中,每个归档都有一个key值,解归档时key值要与归档时key值匹配
  • 如果一个自定义的类A,作为另一个自定义类B的一个属性存在;那么,如果要对B进行归档,那么,B要实现NSCoding协议。并且,A也要实现NSCoding协议
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[NSKeyedArchiver archiveRootObject:obj toFile:appSettingPath];会调用对象的encodeWithCoder方法
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_name forKey:kAddressCardName];
[aCoder encodeObject:_emailObj forKey:kAddressCardEmail];
[aCoder encodeInteger:_salary forKey:kAddressCardSalary];
}
[NSKeyedUnarchiver unarchiveObjectWithFile:appSettingPath];会调用对象的initWithCoder方法
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
_name = [aDecoder decodeObjectForKey:kAddressCardName];
_emailObj = [aDecoder decodeObjectForKey:kAddressCardEmail];
_salary = [aDecoder decodeIntegerForKey:kAddressCardSalary];
return self;
}

三、属性列表

NSUserDefaults适合存储轻量级的本地数据,一些简单的数据(NSString类型的)例如密码,网址等,NSUserDefaults肯定是首选,但是如果我们自定义了一个对象,对象保存的是一些信息,这时候就不能直接存储到NSUserDefaults了。他的方便之处在于不用声明太多的变量来存储不同的数据,一个NSUserDefaults就可以搞定,他是应用程序域的,能让我们进行更加方便的使用。

原理:NSUserDefaults类提供了与默认数据库相交互的编程接口。其实它存储在应用程序的一个plist文件里,路径为沙盒Document目录平级的/Library/Prefereces里。如果将默认数据库比喻为SQL数据库,那么NSUserDefaults就相当于SQL语句。

NSUserDefaults支持的数据类型有:NSNumber(NSInteger、float、double),NSString,NSDate,NSArray,NSDictionary,BOOL,NSData

user defaults数据库中其实是由多个层级的域组成的,当你读取一个键值的数据时,NSUserDefaults从上到下透过域的层级寻找正确的值,不同的域有不同的功能,有些域是可持久的,有些域则不行。

  • 应用域(application domain)是最重要的域,它存储着你app通过NSUserDefaults set…forKey添加的设置。
  • 注册域(registration domain)仅有较低的优先权,只有在应用域没有找到值时才从注册域去寻找。
  • 全局域(global domain)则存储着系统的设置
  • 语言域(language-specific domains)则包括地区、日期等
  • 参数域(argument domain)有最高优先权

注意:

偏好设置是专门用来保存应用程序的配置信息的,一般不要在偏好设置中保存其他数据。

如果没有调用synchronize方法,系统会根据I/O情况不定时刻地保存到文件中。所以如果需要立即写入文件的就必须调用synchronize方法。

偏好设置会将所有数据保存到同一个文件中。即preference目录下的一个以此应用包名来命名的plist文件。

1
2
3
4
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"jack" forKey:@"firstName"];
[defaults setInteger:10 forKey:@"Age"];
[defaults synchronize];

四、SQLite

SQLite数据库的几个特点:

  • 基于C语言开发的轻型数据库
  • 在iOS中需要使用C语言语法进行数据库操作、访问(无法使用ObjC直接访问,因为libqlite3框架基于C语言编写)
  • SQLite中采用的是动态数据类型,即使创建时定义了一种类型,在实际操作时也可以存储其他类型,但是推荐建库时使用合适的类型(特别是应用需要考虑跨平台的情况时)
  • 建立连接后通常不需要关闭连接(尽管可以手动关闭)

在iOS中操作SQLite数据库可以分为以下几个步骤:

  • 打开数据库,利用sqlite3_open()打开数据库会指定一个数据库文件保存路径,如果文件存在则直接打开,否则创建并打开。打开数据库会得到一个sqlite3类型的对象,后面需要借助这个对象进行其他操作。
  • 执行SQL语句,执行SQL语句又包括有返回值的语句和无返回值语句。
  • 对于无返回值的语句(如增加、删除、修改等)直接通过sqlite3_exec()函数执行;
  • 对于有返回值的语句则首先通过sqlite3_prepare_v2()进行sql语句评估(语法检测),然后通过sqlite3_step()依次取出查询结果的每一行数据,对于每行数据都可以通过对应的sqlite3column类型()方法获得对应列的数据,如此反复循环直到遍历完成。当然,最后需要释放句柄。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
打开数据库
if(sqlite3_open(filePath.UTF8String, &_database) == SQLITE_OK)
{
NSLog(@"数据库打开成功");
}
执行无返回结果的SQL
if(sqlite3_exec(_database, sql.UTF8String, NULL, NULL, &error) != SQLITE_OK)
{
NSLog(@"执行SQL语句过程中发生错误,错误信息:%s", error);
}
执行有返回结果的SQL
sqlite3_stmt *stmt;
if(sqlite3_prepare_v2(_database, sql.UTF8String, -1, &stmt, NULL) == SQLITE_OK)
{
while(sqlite3_step(stmt) == SQLITE_ROW)
{
int columnCount = sqlite3_column_count(stmt);
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
for(int i = 0; i < columnCount; i++)
{
const char *name = sqlite3_column_name(stmt, i);
const unsigned char *value = sqlite3_column_text(stmt, i);
[dic setValue:[NSString stringWithUTF8String:(const char *)value] forKey:[NSString stringWithUTF8String:name]];
}
[rows addObject:dic];
}
sqlite3_finalize(stmt);
}

五、CoreData

Core Data是iOS5之后才出现的一个框架,它提供了对象-关系映射(ORM)的功能,即能够将OC对象转化成数据,保存在SQLite数据库文件中,也能够将保存在数据库中的数据还原成OC对象。在此数据操作期间,我们不需要编写任何SQL语句

简介

Core Data是个框架(并不是数据库哦),它使开发者可以把数据当做对象来操作,而不必在乎数据在磁盘中的存储方式。对于iOS程序员来说,这很有用,因为我们已经可以通过代码非常熟悉的操作对象了。由Core Data 所提供的数据对象叫做托管对象(Managed Object),而Core Data本身则位于你的应用程序和持久化存储区(Persistent store)之间。为了把数据从托管对象映射到持久化存储区中,Core Data 需要使用托管对象模型。所有的托管对象都必须位于托管对象上下文(Managed object context)里面,而托管对象上下文又位于高速的易失性存储器里面,也就是位于RAM中。

为什么需要有托管对象上下文呢?原因之一就是在磁盘与RAM之间传输数据时会有开销。磁盘读写速度比RAM慢的多,所以不应该频繁地访问它。有了托管对象上下文,就可以非常迅速地获取到了。但它的缺点在于,开发者必须在托管对象上下文上面定期调用save方法,以将变更后的数据写回磁盘。托管对象上下文的另一个功能是记录开发者对托管对象所对的修改,以提供完整的撤销和重做支持。

上边我们对Core Data简单的介绍了一下。接下来我们需要对CoreData中的重要的名词做一解释。

持久化存储协调器NSPersistentStoreCoordinator

持久化存储协调器(persistent store coordinator)里面包含一份持久化存储区,而存储区里面又含有数据表里面的若干行数据。设置持久化存储协调器的时候,我们通常选用SQLite数据库作为持久化存储区。另外,也可以选用Binary、XML、或In-Memory等形式的持久化存储区。但要注意的是,Binary和XML格式的存储区是Atomic,也就是说,即便你只想修改少量的数据,在保存的时候也依然需要把整个文件都写入磁盘。

同一个持久化存储协调器可以有多个持久化存储区。把CoreData与iCloud相集成的时候,就可能会出现这样的情况。我们可以把不属于iCloud的数据放在一个存储区里面,而把属于iCloud的数据放在另外一个存储区里面,这样既能节省网络宽带,又能节省iCloud存储空间。

即便你有两个持久化存储区,也不意味着必须使用两种对象图。CoreData的模型配置允许开发者使用多个独立的存储区,但却采用同一套对象图。在设置CoreData的模型配置选项时,可以指明对象图里面的某一部分属于哪一个持久化存储区。

要想创建持久化存储区,需生成NSPersistentStore;要想创建持久化存储协调器,需生成NSPersistentStoreCoordinator类的实例。

托管对象模型NSManagedObjectModel

托管对象模型它位于持久化存储协调器和托管对象上下文之间。顾名思议,托管对象模型是描述数据结构的模型或者图示,而托管对象正是以它为基础产生出来的。可以用Xcode来配置实体(Entity)及实体之间的关系。实体本身并不包含数据,它们只是规定了基于该实体的托管对象具有何种特性。实体也有属性,属性的数据类型可以是整数,字符串,或者日期等。

要想创建托管对象模型,需要生成NSManagedObjectModel类的实例

托管对象上下文NSManagedObjectContext

托管对象上下文中可包含多个托管对象。托管对象上下文负责管理其中对象的生命期,并且负责提供许多强大的功能。

托管对象上下文也可以不止有一个,有时我们需要在后台处理任务(比方说把数据保存到磁盘或者导入数据),这种情况下可以采用多个上下文。加入在前台上下文上面调用Save,那么用户界面就可能会有卡顿现象,尤其当数据变化较大的时候更是如此。要想避免这个问题,有个简单的办法就是只在用户按下手机的Home键时才去调用Save,这时应用程序会转入到后台。还有个稍微复杂的但却很灵活的办法,就是采取两个托管对象上下文。请记住,托管对象上下文是存放在高速内存里面的。你可以配置其中一个上下文,那么就可以将后台上下文中的数据异步存入磁盘。这种分段式的做法可以确保磁盘写入操作不会影响用户界面的流畅度。

要想创建托管对象上下文,需要生成NSManagedObjectContext类的实例

coreData简单创建流程

模型文件操作

  1. 创建模型文件,后缀名为.xcdatamodeld。创建模型文件之后,可以在其内部进行添加实体等操作(用于表示数据库文件的数据结构)

  2. 添加实体(表示数据库文件中的表结构),添加实体后需要通过实体,来创建托管对象类文件。

  3. 添加属性并设置类型,可以在属性的右侧面板中设置默认值等选项。(每种数据类型设置选项是不同的)

  4. 创建获取请求模板、设置配置模板等。

  5. 根据指定实体,创建托管对象类文件(基于NSManagedObject的类文件)

实例化上下文对象

  1. 创建托管对象上下文(NSManagedObjectContext)

  2. 创建托管对象模型(NSManagedObjectModel)

  3. 根据托管对象模型,创建持久化存储协调器(NSPersistentStoreCoordinator)

  4. 关联并创建本地数据库文件,并返回持久化存储对象(NSPersistentStore)

  5. 将持久化存储协调器赋值给托管对象上下文,完成基本创建。

Mou icon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 从应用程序包中加载模型文件
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];
// 传入模型对象,初始化持久化存储协调器
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// 构建SQLite数据库文件的路径
NSString *docs = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSURL *url = [NSURL fileURLWithPath:[docs stringByAppendingString:@"person"]];
// 添加持久化存储器,用sqlite作为存储库
NSError *error = nil;
NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error];
if(store == nil)
{
[NSException raise:@"添加数据库错误" format:@"%@", [error localizedDescription]];
}
// 创建托管对象上下文
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
context.persistentStoreCoordinator = psc;
1
2
3
4
5
6
NSManagedObject *card = [NSEntityDescription insertNewObjectForEntityForName:@"Card" inManagedObjectContext:context];
[card setValue:@"4768558865" forKey:@"no"];
[person setValue:card forKey:@"card"];
// 利用上下文对象,将数据同步到持久化存储库
NSError *errorSave = nil;
BOOL sucess = [context save:&errorSave];

查询数据

NSFetchRequest

在执行fetch操作前,可以给NSFetchRequest设置一些参数,这些参数包括谓词、排序等条件,下面是一些基础的设置。

  • 设置查找哪个实体,从数据库的角度来看就是查找哪张表,通过fetchRequestWithEntityName:或初始化方法来指定表名。

  • 通过NSPredicate类型的属性,可以设置查找条件,这个属性在开发中用得最多。NSPredicate可以包括固定格式的条件以及正则表达式。

  • 通过sortDescriptors属性,可以设置获取结果数组的排序方式,这个属性是一个数组类型,也就是可以设置多种排序条件。(但是注意条件不要冲突)

  • 通过fetchOffset属性设置从查询结果的第几个开始获取,通过fetchLimit属性设置每次获取多少个。主要用于分页查询,后面会讲。

MOC执行fetch操作后,获取的结果是以数组的形式存储的,数组中存储的就是托管对象。NSFetchRequest提供了参数resultType,参数类型是一个枚举类型。通过这个参数,可以设置执行fetch操作后返回的数据类型。

  • NSManagedObjectResultType: 返回值是NSManagedObject的子类,也就是托管对象,这是默认选项
  • NSManagedObjectIDResultType: 返回NSManagedObjectID类型的对象,也就是NSManagedObject的ID,对内存占用比较小。MOC可以通过NSManagedObjectID对象获取对应的托管对象,并且可以通过缓存NSManagedObjectID参数来节省内存消耗
  • NSDictionaryResultType: 返回字典类型对象
  • NSCountResultType: 返回请求结果的count值,这个操作是发生在数据库层级的,并不需要将数据加载到内存中

NSPredicate

在iOS开发过程中,很多需求都需要用到过滤条件。例如过滤一个集合对象中存储的对象,可以通过Foundation框架下的NSPredicate类来执行这个操作。

CoreData中可以通过设置NSFetchRequest类的predicate属性,来设置一个NSPredicate类型的谓词对象当做过滤条件。通过设置这个过滤条件,可以只获取符合过滤条件的托管对象,不会将所有托管对象都加载到内存中。这样是非常节省内存和加快查找速度的,设计一个好的NSPredicate可以优化CoreData搜索性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 从数据库查询数据
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:context];
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];
request.sortDescriptors = [NSArray arrayWithObject:sort];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name like %@", @"MJ55*"];
request.predicate = predicate;
// 执行请求
NSError *errorFetch = nil;
NSArray *objs = [context executeFetchRequest:request error:&errorFetch];
if(errorFetch)
{
[NSException raise:@"查询错误" format:@"%@", [errorFetch localizedDescription]];
}

总结

NSPersistentStoreCoordinator有四种可选的持久化存储方案,用得最多的是SQLite的方式。其中Binary和XML这两种方式,在进行数据操作时,需要将整个文件加载到内存中,这样对内存的消耗是很大的。

  • NSSQLiteStoreType : SQLite数据库
  • NSXMLStoreType : XML文件
  • NSBinaryStoreType : 二进制文件
  • NSInMemoryStoreType : 直接存储在内存中

在coredata中所有的托管对象被创建出来后,都是关联着context对象的,所以在对象进行任何操作后,都会被记录在context中,在最后调用context的save方法后,context会将操作交给coordinator去处理,coordinator将会将这个存储任务指派给NSPersistentStore对象。

Demo