数据持久化

在程序开发中,数据层永远是程序的核心结构之一。我们将现实事物进行抽象,使之变成一个个数据。对这些数据的加工处理是代码中能体现技术水平的一大模块,比如数据的请求、解析、缓存、持久化等等。适当的对数据进行持久化存储可以实现应用的离线功能,以此提高用户体验。在iOS开发中,苹果提供了四种持久化方案供我们选择。这些方案分别包括属性列表plist、数据归档NSKeyedValueArchiver/NSUserDefaults、数据库sqlitecoreData等,它们的区别如下

  • 属性列表

属性列表是一种明文的轻量级存储方式,其存储格式有多种,最常规格式为XML格式。在我们创建一个新的项目的时候,Xcode会自动生成一个info.plist文件用来存储项目的部分系统设置。plist只能用数组NSArray或者字典NSDictionary进行读取,由于属性列表本身不加密,所以安全性几乎可以说为零。因为,属性列表正常用于存储少量的并且不重要的数据。

在程序启动后,系统会自动创建一个NSUserDefaults的单例对象,我们可以获取这个单例来存储少量的数据,它会将输出存储在.plist格式的文件中。其优点是像字典一样的赋值方式方便简单,但缺点是无法存储自定义的数据。

  • 数据归档/序列化

与属性列表相反,同样作为轻量级存储的持久化方案,数据归档是进行加密处理的,数据在经过归档处理会转换成二进制数据,所以安全性要远远高于属性列表。另外使用归档方式,我们可以将复杂的对象写入文件中,并且不管添加多少对象,将对象写入磁盘的方式都是一样的。

使用NSKeyedArchiver对自定义的数据进行序列化,并且保存在沙盒目录下。使用这种归档的前提是让存储的数据模型遵守NSCoding协议并且实现其两个协议方法。(当然,如果为了更加安全的存储,也可以遵守NSSecureCoding协议,这是iOS6之后新增的特性)

  • 数据库

sqlite是一个轻量级、跨平台的小型数据库,其拥有可移植性高、有着和MySql几乎相同的数据库语句以及无需服务器即可使用的优点:

可以存储大量的数据,存储和检索的速度非常快;
能对数据进行大量的聚合,这样比起使用对象来进行这些操作要快。

当然,它也具有明显的缺点:

它没有提供数据库的创建方式;
它基于C语言框架设计,没有面向对象的API,所以使用起来比较麻烦;
复杂的数据模型的数据建表相对而言比较麻烦。

当然,我们也可以使用基于sqlite封装的开源数据库FMDB来减少使用sqlite的工作量

  • coreData

coreData是苹果官方iOS5之后推出的综合型数据库,其使用了ORM(Object Relational Mapping)对象关系映射技术,将对象转换成数据,存储在本地数据库中。coreData为了提高效率,甚至将数据存储在不同的数据库中,且在使用的时候将本地数据放到内存中使得访问速度更快。我们可以选择coreData的数据存储方式,包括sqlite、xml等格式。但也正是coreData是完全面向对象的,其在执行效率上比不上原生的数据库。除此之外,coreData拥有数据验证、undo等其他功能,在功能上是四种持久化方案最多的。

上面已经分别介绍了四种方案的优缺点,在开发中,并没有说哪种持久化方案是最好的,只能说在不同开发场景下,最适合使用的持久化方案。下面我们将用代码实战的方式对这些持久方案进行更加详细的了解

属性列表

在我们每次创建新的项目的时候,Xcode帮助我们生成了Info.plist文件,里面存储了关于项目名字、版本、bundle id等等关键信息,这个plist文件也是逆向工程(越狱)中获取app数据的重要文件。OK,那么什么情况下用plist存储呢?打个比方,最近在实现公司项目业务的时候,需要使用选择器UIPickerView给用户选择所在城市。对于城市数据,并没有加密的必要,而且这时候使用plist会达到更高一些的效率。既然已经知道需要的数据,那么很容易就得得出省-市这样的一对多的数据类型,我们的plist使用字典,将省份作为key,存储对应的城市的数组作为value

1、创建plist文件。New Files -> iOS -> Resource -> Property List -> Next

2、给这个plist文件命名cities,点击Create后创建好。然后我们选中文件,默认是字典的数据结构存储了,我们点击Root行右边的加号添加一些键值对,然后修改左边的key的文字,并且将每一个key对应的value设置为Array数组

3、按照在字典中添加键值对的方式,在设置好每个key对应的类型之后,移动到每一行上面点击出现的+给每一个省份添加数组元素,并且赋值,最终效果图如下

当然,像这种城市的plist文件百度一下就可以找到,但是创建plist的方式相信大家看完之后也就明白了。从plist读取数据的方式也很简单,苹果把读取的方法封装在NSArray、NSDictionary中,读取步骤分为两步:

//获取plist文件路径:

NSString  filePath = [[NSBundle mainBundle] pathForResource: @"cities" ofType: @"plist"];

//通过数组或者字典的构造器方法创建容器:

NSDictionary  dict = [NSDictionary dictionaryWithContentsOfFile: filePath];

//NSArray  array = [NSArray arrayWithContentsOfFile: filePath];

实现选择器的大概思路是用两个数组分别存储省份以及当前选中省份的城市数组,然后在滑动pickerView的回调事件中根据选中的省份更新城市数据源。

另外,还有一个NSUserDefault,其支持的数据格式有:NSNumber、NSString、NSDate、NSArray(成员必须也是支持的格式类型)、NSDictionary。其使用和读取也是非常的简单,像字典一样的存取方式

读取:NSString  str = [[NSUserDefaults standardUserDefaults] valueForKey: @"str"];

存储:[[NSUserDefaults standardUserDefaults] setValue: @"str" forKey: @"str"];

同样的也可以使用setObject:forKey:或者objectForKey:等字典存取方法

数据归档/数据序列化

更多时候,NSUserDefaults已经提供了存储简单变量的持久化方案。然而,当我们想要存储复杂的自定义数据时,NSUserDefaults无法为我们提供更多的帮助,这时候考虑的是另外的持久化方案。而并非所有的程序都需要查询数据、数据迁移这些操作,而且也并非所有的数据都有复杂的关系图。这时候,数据归档绝对是不二的选择。

我们使用archive格式的文件将归档化的数据存储在沙盒目录下,这种格式的文件读取出来是二进制数据NSData,然后使用NSKeyedUnarchiver类对数据进行反序列化。假设当前需要进行持久化存储的是一款离线游戏,我需要存储游戏前十名的成绩、成绩持有者和记录创建时间这些数据,那么相对应的LXDGameRecord类声明如下,其必须遵循NSCodingNSSecureCoding协议之一:

@interface LXDGameRecord : NSObject

@property (nonatomic, copy) NSString  userName;

@property (nonatomic, strong) NSDate  createDate;

@property (nonatomic, strong) NSNumber  score;

@end



@implementation LXDGameRecord

/ 协议方法-对数据进行反序列化并读取/

- (id)initWithCoder: (NSCoder )aDecoder

{
	self.userName = [aDecoder decodeObjectForKey: kUserNameKey];

	self.createDate = [aDecoder decodeObjectForKey: kCreateDateKey];

	self.score = [aDecoder decodeObjectForKey: kScoreKey];
}

/ 协议方法-对数据进行序列化/

- (void)encodeWithCoder:(NSCoder )aCoder

{
	[aCoder encodeObject: self.userName forKey: kUserNameKey];

	[aCoder encodeObject: self.createDate forKey: kCreateDateKey];

	[aCoder encodeObject: self.score forKey: kScoreKey];
}

@end 

对于任意自定义类型的数据,只要遵循上面的步骤,就能对数据进行归档了。这里还要讲一下一个小技巧:使用static const修饰来替代宏定义。上面的序列化中,我们可以看到NSCoding的协议方法中对数据进行序列化并且使用一个key来保存它。正常情况下我们可以使用宏来定义key,但是过多的宏定义在编译时也会造成大量的损耗。这时候可以使用static定义静态变量来取代宏定义。

static NSString  const kUserNameKey = @"userName";

让自定义的数据遵循NSCoding协议后,我们就能使用NSKeyedArchiverNSKeyedUnarchiver来对持久化的数据进行存取操作了

/使用NSKeyedUnArchiver对数据反序列化并读取/

NSString  filePath = [self applicationDocumentStorage];

NSData  fileData = [NSData dataWithContentFile: filePath];

NSKeyedUnarchiver  unarchiver = [[NSKeyedArchiver alloc] initForReadingWithData: fileData];

NSArray  datas = [unarchiver decodeObjectForKey: kArchiveKey];   //反序列化

[unarchiver finishDecoding];  //完成反序列化



/ 使用NSKeyedArchiver对数据进行序列化/

NSMutableData  recordData = [NSMutableData data];

NSKeyedArchiver  archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData: recordData];

[archiver encodeObject: datas forKey: kArchiveKey];  //序列化数据

[archiver finishEncoding];  //完成序列化操作

[recordData writeToFile: filePath];  //序列化完成后写入本地磁盘

下面展示的是曾经学习时仿qq登录界面中使用数据归档存储用户注册的账号(由于没有服务器,只能存储在本地),其中用户的数据包括了昵称、头像图片、用户账号、密码这些数据,在输入完账号自己从代理方法中访问这些数据并获取用户的头像

sqlite数据库

基于sqlite封装的FMDB几乎是我工作中最常用到的持久化方案。在实际开发中,sqlite占用的内存非常非常的少,在嵌入式设备中,可能只需要几百K即可。其次,它的速度非常的快,几乎快过所有其他的数据库。当然啦,开始使用数据库进行开发之前,你得了解sqlite支持的数据类型,包括NULL(空值)、Integer(整型)、Real(实数)、Text(字符串)、BLOB(二进制)

在使用sqlite前要导入libsqlite3.0.lib,其操作步骤大致如下:

  • 使用sqlite3_open(const char filename, sqlite3 ppDb)方法打开指定路径下的数据库存入到创建的数据库变量中,如果存在数据库就打开。不存在数据库则关闭。成功打开数据库的时候会返回SQLITE_OK

      ​static NSString  const dbName = @"myDBText.db";
      ​NSString  documentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
      NSString  filePath = [documentDirectory stringByAppendingPathComponent: dbName];
    	
      ​if (SQLITE_OK == sqlite3_open(filePath.UTF8String, &_database)) {
      ​	NSLog(@"数据库打开成功!");
      } else {
      ​	NSLog(@数据库打开失败!“);
      }
    
  • 执行SQL语句。有返回值的语句执行第四步,无返回值的执行第三步

  • 对于无返回值的SQL语句(包括增删改等操作)通过sqlite3_exec()函数执行

      char  error;
      NSString  executeSql = @"select  from table student where name like '张%'";
      if (SQLITE_OK != sqlite3_exec(_database, executeSql.UTF8String, NULL, NULL, &error)) {
          NSLog(@"执行SQL语句过程发生错误!");
      }
    
  • 对于有返回值的SQL语句首先通过sqlite3_prepare_v2()进行语法评估,然后通过sqlite3_step()函数依次取出查询结果的每一行数据,最后通过sqlite3_column_type()方法获取对应列的数据

      NSMutableArray  rows = [NSMutableArray new];
      sqlite3_stmt  stmt;        //检查语法正确性
      if (SQLITE_OK == sqlite3_prepare_v2(_database, executeSql.UTF8String, -1, &stmt, NULL)) {
    	
      //单步执行sql语句
          while (SQLITE_ROW == sqlite3_step(stmt)) {
              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[[NSString stringWithUTF8String: name]] = [NSString stringWithUTF8String: (const char )value];
              }
              [rows addObject: dic];
          }
      }
      sqlite3_finalize(stmt);
    

sqlite的原生语句对于开发者而言有时是一个灾难,在熟练使用之前,我们很难保证数据库的语句和执行代码没有任何问题。对此,我们可以在github上面找到基于sqlite3封装的FMDB,它提供了特有的机制来保证数据库访问是线程安全的,至于使用方法在网上一搜一大把教程,这里就不在细说。但是,使用FMDB有一个要注意的问题是——当我们把图片转换成二进制数据存储在数据库中的时候,再次读取出这个二进制数据初始化成图片的时候会出错误,无法正常转换成图像。解决方案详见这里

coreData

coreData是iOS5之后苹果推出的数据持久化框架,其提供了ORM的功能,将对象和数据相互转换。其中,它提供了包括sqlite、xml、plist等本地存储文件,默认使用sqlite进行存储。coreData具有两个模型:关系模型和对象模型,关系模型即是数据库,对象模型为OC对象。其关系图如下

由于我们不需要关心数据的存储,coreData使用起来算是最简单的持久化方案。要使用coreData有两个方式,一个是在创建项目的时候勾选use core data,另一个则是手动创建。在这里我们要讲解的是前者创建的方式

1、创建新项目勾选使用coreData

2、创建关系模型,在这里我创建的模型名字是LXDCoreDataDemo

3、在创建的关系模型中添加实体,命名为Person,并且添加三个字段:name、age、score

到了这里我们的实体模型就创建好了,接下来就是通过NSManagedObject来将实体模型转换成对象。通过从coreData取出的对象,全部都是继承自NSManagedObject的子类。那么我们需要根据当前的关系模型来创建Person

选择LXDCoreDataDemo -> Next -> Person -> Create,我们就创建好了Person,这时候三个成员属性都会自动添加完成

接着我使用故事板创建了下面的视图,在我点击按钮的时候往数据库中插入新的person数据

在执行操作的类实现文件中,我们要加入AppDelegatePerson的头文件,因为在创建项目的时候如果我们勾选了use core data的选项,appDelegate文件中会帮我们生成用于管理、存储这些模型的对象,我们可以通过添加头文件来使用。插入数据的代码如下:

//先取出coredata上下文管理者
AppDelegate appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext context = appDelegate.managedObjectContext;

//保存新数据
Person person = [NSEntityDescription insertNewObjectForEntityForName: @"Person" inManagedObjectContext: context];
person.name = _userName.text;
person.name = _userScore.text;
person.age = @([_userAge.text integerValue]);
[appDelegate saveContext];

//查询所有数据
NSError error;
NSFetchRequest request = [NSFetchRequest new];
NSEntityDescription entity = [NSEntityDescription entityForName: @"Person" inManagedObjectContext: context];
[request setEntity: entity];
NSArray results = [[context executeFetchRequest: request error: &error] copy];
for (Person p in results) {
	NSLog(@"%@, %@, %@", p.name, p.age, p.score);
}

想要对coreData有更深入的了解可以购买Core Data应用开发实践指南,里面详细讲述了coreData的各种使用技巧。

对于我们开发者而言,使用适当的持久化方案可以帮助我们获得更高的开发效率。更快的数据加载速度可以显著提高应用的用户体验感。