数据持久化的相关知识
四种将数据持久化存储到iOS文件系统的机制:
- plist
- 对象归档
- iOS的嵌入式关系数据库SQLite3
- Core Data
每个应用都有自己的/Documents文件夹,且仅能读写各自的/Documents目录中的内容。
iOS应用的3个支持文件夹:
- Documents:应用将数据存储在Documents中,但基于NSUserDefaults的首选项设置除外。
- Library: 基于NSUserDefaults的首选项设置存储在Library/Preferences文件夹中
- tmp: tmp目录供应用存储临时文件,当iOS设备执行和iTunes的同步时,不会备份其中的tmp文件,但在不需要这些文件时,应用要负责删除tmp中的文件,以免占用文件系统空间。
获取Documents目录
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = paths[0];
常量NSDocumentDirectory表明我们正在寻找Documents目录的路径。第二个常量 NSUserDomainMask表明我们希望将搜索限制在应用的沙盒里。虽然返回的是一个匹配路径的数组,但我们知道数组中位于索引0处的一定是Documents目录,为什么呢?我们知道每个应用只有一个Documents目录,因此只有一个目录符合指定的条件。
可以在刚刚检索到的路径的结尾附加另一个字符来创建文件名。为此要使用专为该目的设计的NSString方法,即stringByAppendingPathComponent:方法,如下所示:
NSString *fileName = [documentsDirectory stringByAppendingPathComponent:@"theFile.txt"];
完成此调用之后,fileName就是指向应用的Documents目录中theFile.txt文件的完整路径,然后我们就可以使用fileName来创建、读取和写入文件了。
获取tmp目录
获取对应用临时目录的引用比获取对 Documents目录的引用容易。名为NSTemporaryDirectory()的Foundation函数将返回一个字符串,该字符串包含到应用临时目录的完整路径。若要创建一个将会存储在临时目录中的文件,首先要找到该临时目录:
NSString *tmpPath = NSTemporaryDirectory();
然后,在路径的结尾附上文件名就可以创建指向该目录下文件的路径,比如:
NSString *tempFile = [tmpPath stringByAppendingPathComponent:@"theFile.txt"];
文件保存方案
单文件持久化
将数据保存在一个文件中是最简单的方法,且对于许多应用,这也是完全可以接受的方法。首先,创建一个根对象,通常是NSArray或NSDictionary(使用归档文件的情况下根对象可以基于某个自定义类)。接下来,使用所有需要保存的程序数据填充根对象。真正保存的时候,代码会将该根对象的全部内容重新写入单个文件。应用在启动时会将该文件的全部内容读入内存,并在退出时注销。
使用单文件的缺点是必须将全部数据加载到内存中,并且不管更改多少也必须将所有数据全部重新写入文件系统。如果应用管理的数据不超过几兆字节,此方法可能非常好,且简单。
多文件持久化
使用多个文件是另一种实现持久化的方法。太复杂!
plist
使用属性列表非常方便,因为可以使用Xcode或Property List Editor应用手动编辑他们,并且只要字典或数组包含特定可序列化对象,就可以将NSDictionary和NSArray 实例写入属性列表或者从属性列表创建它们。
属性列表序列化
序列化对象(serialized object)是指可以被转换为字节流以便于存储到文件中或通过网络进行传输的对象。虽然说任何对象都可以被序列化,但只有某些特定对象才能被放置到某个集合类中(字典,数组),然后才使用该集合类的writeToFile:atomically:方法或writeToURL:atomically:方法将它们存储到属性列表中。可以按照该方法序列化下面的Objective-C类:NSArray,dictionary,data,string及它们的可变子类,NSNumber,NSDate
这里的atomically参数让该方法将数据写入辅助文件,而不是写入指定位置。成功写入该文件之后,辅助文件将被复制到第一个参数指定的位置。这是更安全的做法,因为如果应用在保存期间崩溃,则现有文件不会被破坏。
练习使用
创建一个outlet集合:
@property (strong, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;
在storyboard中,按住control从顶部的view controller图标拖到要对应的每个集合成员上,并选择集合名字。
用于确定文件路径:
- (NSString *)dataFilePath{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = paths[0];
return [documentsDirectory stringByAppendingPathComponent:@"data.plist"];
}
在viewDidLoad中:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *filePath = [self dataFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSArray *array = [[NSArray alloc] initWithContentsOfFile:filePath];
for (int i = 0; i < 4; i++) {
UITextField *theField = self.lineFields[i];
theField.text = array[i];
}
}
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:app];
// Do any additional setup after loading the view, typically from a nib.
}
检查数据文件是否存在,如果不存在就不加载它了,如果存在,就用该文件内容实例化数组,然后将数组中的对象复制到四个文本框中。由于数组是按顺序排列的列表,因此只要根据保存顺序(之后会有)来复制数组,就一定能确保相应地字段获得正确的值。
-
从属性列表中加载数据后,我们获得了对应用实例的引用,并使用该引用订阅UIApplicationWillResignActiveNotification通知,这是由UIApplication定义的字符串常量,最后一个参数app是sender.
- (void)applicationWillResignActive:(NSNotification *)notification{ NSString *filePath = [self dataFilePath]; //对outlet集合中每个元素提取相同的属性(KVC) NSArray *array = [self.lineFields valueForKey:@"text"]; [array writeToFile:filePath atomically:YES]; }
最后一个方法是applicationWillResignActive:。注意它接受一个指向NSNotification的指针作为参数。它是一个通知方法,所有通知都接受一个NSNotification实例作为参数。应用应该在终止运行或者进入后台之前保存数据,所以我们需要使用名为UIApplicationWillResignActiveNotification通知。这样,只要这个应用不再是当前正在与用户进行交互的应用,就会发布通知,包括用户按下Home键,来了个电话。
通知方法通过调用lineFiedls数组中每个文本框的text方法构建一个字符串数组。我们利用了一个便捷的方法,没有迭代数组中的文本框,而是用了valueForKey:方法,并传递@"text"作为参数。NSArray类的valueForKey:方法为我们实现了迭代获取实例变量的text值,返回包含这些值的数组。然后我们将该数组的内容写入一个属性列表文件中。
对模型对象进行归档
在Cocoa世界中,归档(archiving)是指另一种形式的序列化,但它是任何对象都可以实现的更常规的类型。专门编写用于保存数据的任何模型对象都应该支持归档。使用对模型对象进行归档的技术可以轻松将复杂的对象写入文件,然后再从中读取它们。
只要在类中实现的每个属性都是标量(int.float)或是遵循NSCoding协议的某个类的实例,你就可以对整个对象进行完全的归档。由于大多数支持存储数据的Foundation和Cocoa Touch类都遵循NSCoding协议(除UIImage)。
还有一个协议应该和NSCoding协议一起实现,那就是NSCopying协议。后者允许复制对象,这使你在使用数据模型对象时具备了较大的灵活性。
遵循NSCoding协议
NSCoding协议声明了两个方法,这两个方法都是必需的。一个方法将对象编码到归档中,另一个方法对归档解码来创建一个新对象。这两个方法都传递一个NSCoder实例,使用方法和NSUserDefaults相似。也可以使用KVC对对象和原生数据类型(int,float)进行编码和解码。
对某个对象进行编码的方法可能看起来如下:
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:foo forKey:kFooKey];
[aCoder encodeObject:bar forKey:kBarKey];
[aCoder encodeInt:someInt forKey:kSomeIntKey];
[aCoder encodeFloat:someFloat forKey:kSomeFloat];
}
若要我们在项目中支持归档,必须使用正确的编码方法将所有实例变量编码成encoder。如果要子类化某个也遵循NSCoding的类,还需要确保对超类调用encodeWithCoder:方法,你的方法将如下所示:
- (void)encodeWithCoder:(NSCoder *)aCoder{
[super encodeWithCoder:aCoder];
[aCoder encodeObject:foo forKey:kFooKey];
[aCoder encodeObject:bar forKey:kBarKey];
[aCoder encodeInt:someInt forKey:kSomeIntKey];
[aCoder encodeFloat:someFloat forKey:kSomeFloat];
}
我们还需要实现一个通过NSCoder解码的对象初始化方法,恢复我们之前归档的对象。实现initWithCoder:方法比实现encodeWithCoder:方法稍微复杂一些。如果直接对NSObject进行子类化,或者对某些不遵循NSCoding的其他类进行子类化,则你的方法看起来如下
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
foo = [aDecoder decodeObjectForKey:kFooKey];
bar = [aDecoder decodeObjectForKey:kBarKey];
someInt = [aDecoder decodeObjectForKey:kSomeIntKey];
someFloat = [aDecoder decodeObjectForKey:kAgeKey];
}
return self;
}
该方法使用[super init]初始化对象实例,如果初始化成功,则它通过解码NSCoder的实例中传递的值来设置其属性。当为某个具有超类且遵循NSCoding的类实现NSCoding时,initWithCoder:方法应稍有不同。它不再对super调用init,而是调用initWithCoder,像这样:
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super initWithCoder:aDecoder]) {
foo = [aDecoder decodeObjectForKey:kFooKey];
bar = [aDecoder decodeObjectForKey:kBarKey];
someInt = [aDecoder decodeObjectForKey:kSomeIntKey];
someFloat = [aDecoder decodeObjectForKey:kAgeKey];
}
return self;
}
只要实现这两个方法,就可以对所有对象的属性进行编码和解码,然后便可以对对象进行归档,并且可以将其写入归档或者从归档中读取它们。
NSCopying协议
如前所述,遵循NSCopying对于任何数据模型对象来说都是非常好的事情。NSCopying有一个copyWithZone:方法,可用来复制对象。实现NSCopying和实现initWithCoder:非常相似,只需创建一个同一类的新实例,然后将该新实例的所有属性都设置为与该对象属性相同的值。此处的copyWithZone:方法的内容类似下:
- (instancetype)copyWithZone:(NSZone *)zone{
MyClass *copy = [[[self class] allocWithZone:zone] init];
copy.foo = [self.foo copyWithZone:zone];
...
return copy;
}
对数据对象进行归档和取消归档
从遵循NSCoding的一个或多个对象创建归档相对比较容易。首先创建一个NSMutableData实例,用于包含编码的数据。然后创建一个NSKeydArchiver实例,用于将对象归档到此NSMutableData实例中:
NSMutableData *data = [NSMutableData new];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
创建这两个实例后,我们使用KVC来对希望包含在归档中的所有对象进行归档,像这样:
[archiver encodeObject:myObject forKey:@"keyValueString"];
对所有要包含的对象进行编码之后,我们只需告知归档程序已经完成了这些操作。将NSMutableData实例写入文件系统:
[archiver finishEncoding];
BOOL success = [data writeToFile:@"/../.." atomically:YES];
写入文件时出现错误会将success设置为NO.如果success为YES,则数据已成功写入指定文件。从该归档创建的任何对象都将是过去写入该文件的对象的精确副本。
从归档重组对象的步骤类似。从归档文件创建一个NSData实例,并创建一个NSKeyedUnarchiver以对数据进行解码:
NSData *data = [[NSData alloc] initWithContentsOfFile:@"/.../.."];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
然后,使用之前对对象进行归档的同一个键从解压程序中读取对象。
self.object = [unarchiver decodeObjectForKey:@"key"];
最后,告知归档程序完成了该操作:
[unarchiver finishDecoding];
在某个需要归档的类中:
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
self.lines = [aDecoder decodeObjectForKey:kLinesKey];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:self.lines forKey:kLinesKey];
}
- (instancetype)copyWithZone:(NSZone *)zone{
BIDFourLines *copy = [[[self class] allocWithZone:zone] init];
NSMutableArray *linesCopy = [NSMutableArray new];
for (id line in self.lines) {
linesCopy addObject:[line copyWithZone:zone];
}
copy.lines = linesCopy;
return copy;
}
我们刚才实现了遵循NSCoding和NSCopying所需的所有方法。在encodeWithCoder:中对四个属性进行了编码,并在initWithCoder:中使用相同的4个键值对对这些属性进行解码。在copyWithZone:中,我们创建了一个新的BIDFourLines对象,并将四个字符串复制到其中。
然后更改ViewController。
- (void)viewDidLoad {
[super viewDidLoad];
NSString *filePath = [self dataFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSData *data = [[NSMutableData alloc] initWithContentsOfFile:filePath];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
BIDFourLines *fourlines = [unarchiver decodeObjectForKey:kRootKey];
[unarchiver finishDecoding];
for (int i = 0; i < 4; i++) {
UITextField *theField = self.lineFields[i];
theField.text = fourlines.lines[i];
}
}
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:app];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)applicationWillResignActive:(NSNotification *)notification{
NSString *filePath = [self dataFilePath];
//对outlet集合中每个元素提取相同的属性(KVC)
BIDFourLines *fourLines = [[BIDFourLines alloc] init];
fourLines.lines = [self.lineFields valueForKey:@"text"];
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:fourLines forKey:kRootKey];
[archiver finishEncoding];
[data writeToFile:filePath atomically:YES];
}
使用内嵌的SQLite3
首先在Targets的build phase中引入sqlite3动态库
SQLite3在存储和检索大量数据方面非常有效。他还能够对数据进行复杂的聚合,与使用对象执行这些操作比,获得结果的速度更快。
SQLite3可以不需要将所有对象加载到内存中。
SQLite3使用SQL(Structured Query Language, 结构化查询语言)。SQL是与关系数据库交互的标准语言。
关系数据库和面向对象的编程语言使用完全不同的方法来存储和组织数据。这些方法差异很大,因而出现了在两者之间进行转换的各种技术以及很多库和工具。这些技术统称为ORM(Object-Relational Mapping,对象关系映射)。目前有很多ORM工具可用于Cocoa touch.实际上,Apple提供的Core Data就是一种。
创建和打开数据库
使用SQLite3前,必须打开数据库。用于执行此操作的命令是sqlite3_open().这样将打开一个现有数据库,如果指定位置上不存在数据库,则函数便会创建一个新的数据库,下面是打开新数据库的代码
sqlite3 *database;
int result = sqlite3_open("/path/to/database/file", &database);
如果result等于常量 SQLITE_OK,就表示数据库已经打开。此处你应该记住,数据库文件必须以C字符串而非NSString的形式进行传递。SQLite3是采用可移植的C而非OC编写的,它不知道什么是NSString.所幸,NSString有个方法,该方法能将NSString实例转化为C字符串。
const char *stringPath = [@"string" UTF8String];
对SQLite3数据库执行完所有操作后,调用一下内容来关闭数据库。
sqlite3_close(database);
数据库将所有数据存在表中。可以通过SQL的CREATE语句创建一个新表,并使用sqlite3_exec将其传递到打开的数据库,代码如下:
char *errorMsg;
const char *createSQL = "CREATE TABLE IF NOT EXISTS PEOPLE""(ID INTEGER PRIMARY KEY AUTOINCREMENT, FIELD_DATA TEXT)";
int result = sqlite3_exec(database, createSQL, NULL, NULL, &errorMsg);
如果里那个字符之间除了空白(包括换行符)之外没有其他分隔字符,那么这两个字符串会被连接成一个字符串.
如之前所做的一样,需要检查result是否等于SQLITE_OK以确保命令成功运行。如果命令未成功运行,errorMsg将对所发生的问题进行描述
函数sqlite3_exec()针对SQLite3运行任何不返回数据的命令。它用于执行:
- 更新
- 插入
- 删除
从数据库中检索数据有点复杂,必须首先向其输入SQL的SELECT命令来准备该语句
NSString *query = @"SELECT ID, FIELD_DATA FROM FIELDS ORDER BY ROW";
sqlite3_stmt *statement;
int result = sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil);
所有接受字符串的SQLite3函数都要求使用旧式的C字符串。在实例中,我们可以创建并传递一个C字符串,也可以创建一个NSString并通过它的方法UTF8String派生一个C字符串。这两个方法都行。如果需要操纵字符串,则使用NSString或NSMutableString较为容易,但将NSString转换为C字符串会导致一些额外的开销。
如果result等于SQLITE_OK,则语句准备成功,可以开始遍历结果集。下面的例子将遍历结果集从数据库中搜索int 和 NSString。
while (sqlite3_step(statement) == SQLITE_ROW) {
int rowNum = sqlite3_column_int(statement, 0);
char *rowData = (char *)sqlite3_column_text(statement, 1);
NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
//do something with the data here
}
sqlite3_finalize(statement);
绑定变量
虽然可以通过创建SQL字符串来插入值,但常用的方法是使用绑定变量(bind variable)来执行数据库插入操作。正确处理字符串并确保他们没有无效字符(以及引号处理过的属性)是非常繁琐的事情。借助绑定变量,这些问题都迎刃而解。
需使用绑定变量插入值,只需按正常方式创建SQL语句,但要在SQL字符串中添加一个问号.每个问号都表示一个需要在语句执行之前进行绑定的变量。然后准备好SQL语句,将值绑定到各个变量并执行命令。
下面这个示例使用两个绑定变量预处理SQL语句,它将int绑定到第一个变量,将字符串绑定到第二个变量,然后执行查询语句:
char *sql = "insert into foo values (?, ?);";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, sql, -1, &stmt, nil) == SQLITE_OK) {
sqlite3_bind_int(stmt, 1, 235);
sqlite3_bind_text(stmt, 2, "Bar", -1, NULL);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
NSLog(@"This should be real error checking");
}
sqlite3_finalize(stmt);
根据希望使用的数据类型,可以选择不同的绑定语句。大部分绑定函数都只有3个参数。
- 无论针对哪种数据类型,任何绑定函数的第一个参数都指向之前在sqlite3_prepare_v2()调用中使用的sqlite3_stmt.
- 第二个参数是所绑定的变量的索引。它是一个有序索引的值,这表示SQL语句中的第一个问号索引是1,而其后每个问号的索引值都依次加1
- 第三个参数始终表示应该替换问号的值。有些绑定函数(比如说用于绑定文本和二进制数据的绑定函数)拥有另外两个参数。
- 一个参数是在上面的第三个参数中传递的数据的长度。对于C字符串,可以传递-1来代替字符串的长度,则函数将使用整个字符串,对于所有其他情况,需要指定所传递数据的长度。
- 另一个参数是可选的函数callback,用于在语句执行后完成内存清理工作。通过整个函数使用malloc()释放已分配的内存。
- 绑定函数后面的语法似乎看起来有点奇怪,因为我们执行了一个插入操作。当使用绑定常量时,会将相同语法同时用于查询和更新。如果SQL字符串包含了一个SQL查询(而不是更新),我们需要多次调用sqlite3_step(),直到它返回SQLITE_DONE。因为这里是更新,所以仅调用一次。
SQLite3的应用
#import "ViewController.h"
#import <sqlite3.h>
@interface ViewController ()
@property (copy, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
sqlite3 *database;
if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"Failed to open database");
}
NSString *createSQL = @"CREATE TABLE IF NOT EXISTS FIELDS""(ROW INTEGER PRIMARY KEY, FIELD_DATA TEXT)";
char *errorMsg;
if (sqlite3_exec(database, [createSQL UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"Error creating table: %s", errorMsg);
}
NSString *query = @"SELECT ROW, FIELD_DATA FROM FIELDS ORDER BY ROW";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, [query UTF8String], -1, &stmt, nil) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
int row = sqlite3_column_int(stmt, 0);
char *rowData = (char *)sqlite3_column_text(stmt, 1);
NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
UITextField *field = self.lineFields[row];
field.text = fieldValue;
}
sqlite3_finalize(stmt);
}
sqlite3_close(database);
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:app];
}
- (void)applicationWillResignActive:(NSNotification *)notification{
sqlite3 *database;
if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"Failed to open database");
}
for (int i = 0; i < 4; i++) {
UITextField *field = self.lineFields[i];
char *update = "INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA""VALUES (?, ?);";
char *errorMsg = NULL;
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, update, -1, &stmt, nil) == SQLITE_OK) {
sqlite3_bind_int(stmt, 1, i);
sqlite3_bind_text(stmt, 2, [field.text UTF8String], -1, NULL);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
NSAssert(0, @"Error updating table: %s", errorMsg);
}
sqlite3_finalize(stmt);
}
sqlite3_close(database);
}
- (NSString *)dataFilePath{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = paths[0];
return [path stringByAppendingPathComponent:@"data.sqlite"];
}
@end
首先打开数据库,如果在打开时遇到了问题,则关闭它并抛出一个断言错误。
接下来,需要确保有一个表来保存我们的数据。可以使用CREATE TABLE来完成此任务。通过指定IF NOT EXISTS,可以防止数据库覆盖现有数据。如果已有一个具有相同名称的表,此命令会直接退出,不执行任何操作,所以可以在应用每次启动时安全地调用它,无需显式检查表是否存在。
最后需要加载数据。为此,使用SELECT语句。在这个简单例子中,我们创建了一个SELECT来从数据库请求所有行,并要求SQLite3准备我们的SELECT。要告诉SQLite3按行号排序各行,以便我们总是以相同顺序获取它们。否则,SQLite3将按内部存储顺序返回各行。
最后关闭数据库连接,所有操作到此结束。
请注意,我们在创建表和加载它所包含的所有数据后立即关闭了数据库连接,而不是在应用运行的整个过程中保持打开状态。这是管理连接最简单的方式,对于整个小应用,我们可以在需要连接时再打开它。在其他需要频繁使用数据库的应用中,可能有必要始终打开连接。
其他更改是在applicationWillResignActive:方法中进行的,我们需要把应用数据保存在这里。由于数据库中的数据存储在一个表中,存储后应用的数据看起来跟下面相似:
行 | FIELD_DATA
------------- | -------------
0 | text1
1 | text2
2 | text3
3 | text4
applicationWillResignActive:方法会首先再次打开数据库。然后保存数据,在4个字段中进行循环,生成4条独立命令来更新数据库中的每一行。
我们在循环中要做的第一件事就是创建一个字段名称,以便可以检索到正确的文本框输出口。记住,使用valueForKey:可以根据名称检索属性。同时为错误消息声明一个指针,在出现错误时使用。
我们设计了一条带两个绑定变量的 INSERT OR REPLACE的SQL语句。第一个变量代表所存储的行,第二个变量代表要存储的实际内容字符串值。使用INSERT OR REPLACE而不是更标准的INSERT,就不必担心某个行是否存在。
接下来声明一个指向语句的指针,然后为语句添加绑定变量,并将值绑定到两个绑定变量
然后调用sqlite3_step()来执行更新,检查并确定其运行正常,然后完成语句,结束循环。
注意,此处使用了一个断言来检查错误条件。之所以会使用断言,而不使用异常或手动错误检查,是因为这种情况只有在开发人员出错的情况下才会出现。使用此断言宏有助于我们调试代码,并且可以脱离最终应用。如果某个错误条件是用户正常情况下可能遇到的条件,则应该使用其他形式的错误检查。
有一个条件可能导致前面的SQLite代码出现错误,而不是程序员错误。如果设备的存储区已满,SQLite无法将其更改保存到数据库,那么这里也会发生错误。但是这种情况很少见,并可能为用户带来更深层次的问题,不过这已超出了应用数据的范围。
完成循环后,关闭数据库。
Core Data
Core Data是一款稳定,功能全面的持久化工具。
你不需要创建类,而是先在数据模型编辑器中创建一些实体(entity),然后在代码中为这些实体创建托管对象.(managed object)
实体表示对对象的描述,而托管对象表示在运行时创建的该实体的具体实例,
因此,在数据模型编辑器中,你将创建实体,而在代码中,你将创建并检索托管对象。实体和托管对象之间的差异类似于类和类的实例。
实体由属性组成,属性分为3种类型。
- 特性(attribute) 特性在Core Data实体中的作用与实例变量在Objective-C类中的作用完全相同,它们都用于保存数据。
- 关系--顾名思义,关系用于定义实体之间的关系。举例来说,假设要定义一个Person实体,你可能首先会定义一些特性,比如hairColor,eyeColor,height和weight.你还可以定义地址特性,比如说state和zipCode.或者,可以将他们嵌入到单独的HomeAddress实体中。时候后面这种方法,你可能还希望在Person和HomeAddress之间创建一个关系。关系可以1对1或1对多。从Person到HomeAddress的关系可以是1对1,因为大多数人都只有一个家庭地址。从HomeAddress到Person的关系可以是1对多,因为可能多个person住在同一个HomeAddress
- 提取属性(fetched property):提取属性是关系的备选方法。用提取属性可以创建一个可在提取时被评估的查询,从而确定哪些对象属于这个关系。沿用刚才的例子,一个Person对象可以拥有一个名为Neighbors的提取属性,该属性查找数据存储中与这个Person的HomeAddress拥有相同邮政编码的所有HomeAddress对象。由于提取属性的结构和使用方式,它们通常是1对1的关系。提取属性也是唯一一种能让你跨越多个数据存储的关系。
通常,特性,关系和提取属性都是使用Xcode的数据模型编辑器定义的.
键值编码
我们的代码中不再使用存取方法和修改方法,而是使用键值编码来设置属性或检索它们的已有值。
在操作托管对象时,用于设置和检索属性值得键就是希望设置的特性的名称。因此要从托管对象中检索在name特性中的值,需要调用以下方法:
NSString *name = [myManagedObject valueForKey:@"name"];
同样,要为托管对象的属性设置新值,可以执行一下操作:
[myManagedObject setValue:@"daf" forKey:@"dajif"];
在上下文中结合它们
那么,托管对象的活动区域在哪里呢?它们位于所谓的持久存储中,有时也成为支持存储(backing store)。持久存储可以采用多种不同的形式。默认情况下,Core Data应用将支持存储实现为在应用Documents目录中的SQLite数据库。虽然数据是通过SQLite存储的,但Core Data框架中的类将完成与加载和保存数据相关的所有工作。如果使用Core Data,则不需要编写任何SQL语句。你只需要操作对象,而内部的工作就由Core Data完成。
除了SQLite之外,支持存储还可以作为二进制文件实现,甚至以XML格式存储。还有一种选择是创建一个内存库,编写缓存机制时可以采取这种方法,但它在当前回话结束后无法保存数据。在几乎所有情况下,你都应该采用默认的设置,并使用SQLite作为持久存储。
虽然大多数应用都只有一个持久存储,但也可以在同一应用中使用多个持久存储。
除了创建它之外(通常在应用委托中实现),我们通常不会直接操作持久存储,而是使用所谓的托管对象上下文(context)。上下文协调对持久存储的访问,同时保存自上次保存对象以来修改过的属性的信息。上下文还能通过撤销管理器来取消所有更改,这意味着你可以撤销单个操作或是回滚到上次保存的数据。(有点类似git哦)
可以将多个上下文指向相同的持久存储,但大多数ios应用只使用一个。
许多核心数据调用都需要NSManagedObjectContext作为参数,或者需要在上下文中执行。除了一些更加复杂、多线程的iOS应用外,应用委托都可以只使用managedObjectContext属性--它是Xcode项目魔板自动为应用创建的默认上下文。
你可能会发现,除了委托对象上下文和持久存储协调者之外,所提供的应用委托还包含一个NSManagedObjectModel实例。该类负责在运行时加载和表示使用Xcode中的数据模型编辑器创建的数据模型。通常,你不需要直接与该类交互。该类由其他Core Data类在后台使用,因此它们可以确定数据模型中定义了哪些实体和属性。只要使用所提供的文件创建数据模型,就完全不需要担心这个类。‘
创建托管对象
创建托管对象的新实例非常简单,但没有alloc和init使用起来简单。这里使用NSEntityDescription类中的insertNewObjectForEntityForName:inManagedObjectContext:工厂方法。NSEntityDescription的工作是跟踪在应用的数据模型中定义的所有实体并能够让你创建这些实体的实例。此方法创建并返回一个实例,表示内存中的单个实体。它返回使用该特定实体的正常属性设置的NSManagedObject实例,或者如果将实体配置为使用NSManagedObject的特定子类实现,则返回该类的实例。请记住,实体类似于类。实体是对象的描述,用于定义特定的实体具有哪些属性。
创建新对象的方法如下:
NSManagedObject *thing = [NSEntityDescription insertNewObjectForEntityForName:@"Thing" inManagedObjectContext:context];
这个方法的名称为insertNewObjectForEntityForName:inManagedObjectContext:,因为除了创建新对象外,它还将此新对象插入到上下文,并返回这个对象。调用结束后,对象存在于上下文中,但还不是持久存储的一部分。下一次托管对象上下文的save:方法被调用时,这个对象将被添加到持久存储。
获取托管对象
要从持久存储中获取托管对象,可以使用获取请求(fetch request),这是Core Data处理预定义的查询的方式。例如,可以要求“返回所有eyeColor为蓝色的Person"
首次创建获取请求后,为它提供一个NSEntityDescription,指定希望检索的一个或多个对象实体。下面是一个创建获取请求的例子:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entityDescr = [NSEntityDescription entityForName:@"Thing" inManagedObjectContext:context];
[request setEntity:entityDescr];
也可以使用NSPredicate类为获取请求指定条件。Predicate类似于SQL的WHERE子句,可定义条件让获取请求得出结果。下面是一个简单的示例:
NSPredicate *pre = [NSPredicate predicateWithFormat:@"(name = %@)", nameThing];
[request setPredicate:pre];
第一行代码创建的predicate告诉获取请求,无需获取指定实体的所有托管对象,它应仅获取那些name属性被设置为当前存储在nameThinng变量中的值的托管对象。所以,如果nameThing是一个包含值@"Bob"的NSString,则会告诉获取请求仅返回其name属性为"Bob"的托管对象。这是一个简单的例子,predicate复杂很多。
创建了获取请求并为它提供实体描述后(可以选择为它指定一个predicate),使用NSManagedObjectContext中的实例方法来执行获取请求:
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
//handle error
}
executeFetchRequest:error:将从持久存储中加载特定对象,并在一个数组中返回它们。如果遇到错误,则会获得一个nil数组,并且你提供的错误指针将指向描述特定问题的NSError对象。如果没有遇到错误,则会获得一个有效的数组,但其中可能没有任何对象,因为可能没有任何对象满足指定标准。此后,context(对它执行了请求)将跟踪对该数组中返回的托管对象的所有更改。向该上下文发送一条save:信息可保存更改。
Core Data的应用
#import "ViewController.h"
#import "AppDelegate.h"
static NSString * const kLineEntityName = @"Line";
static NSString * const kLineNumberKey = @"lineNumber";
static NSString * const kLineTextKey = @"lineText";
@interface ViewController ()
@property (strong, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:kLineEntityName];
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
NSLog(@"There was a error!");
//handle error
}
for (NSManagedObject *oneObject in objects) {
int lineNum = [[oneObject valueForKey:kLineNumberKey] intValue];
NSString *lineText = [oneObject valueForKey:kLineTextKey];
UITextField *theField = self.lineFields[lineNum];
theField.text = lineText;
}
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:app];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)applicationWillResignActive:(NSNotification *)notification{
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
NSManagedObjectContext *context = appDelegate.managedObjectContext;
NSError *error;
for (int i = 0; i < 4; i++) {
UITextField *textFiled = self.lineFields[i];
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:kLineEntityName];
NSPredicate *pred = [NSPredicate predicateWithFormat:@"(%s = %d)", kLineNumberKey, i];
request.predicate = pred;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
NSLog(@"There is an error!");
//handle error
}
NSManagedObject *theLine = nil;
if (objects.count > 0) {
theLine = objects[i];
}else {
theLine = [NSEntityDescription insertNewObjectForEntityForName:kLineEntityName inManagedObjectContext:context];
}
[theLine setValue:[NSNumber numberWithInt:i] forKey:kLineNumberKey];
[theLine setValue:textFiled.text forKey:kLineTextKey];
}
[appDelegate saveContext];
}
来看一下viewDidLoad方法。我们需要确定持久存储中是否已经存在数据,如果有则加载数据并使用它填充字段。该方法首先获取对应委托的引用,我们将使用这个引用获得为我们创建的托管对象上下文。
下一个步骤是创建一个获取请求并将实体描述传递给它,以便请求知道要检索的对象类型
由于我们希望检索持久存储中的所有line对象,因此没有创建predicate.通过执行没有predicate的请求,上下文将返回库中的每一个line对象。确保返回的是有效的数组,如果不是则记录对应的日志。
接下来,我们使用快速枚举遍历已获取托管对象的数组,从中提取每个托管对象的lineNumber和lineText值,并使用该信息更新用户界面上的一个文本框。
然后,我们需要在应用即将终止(无论是转到后台还是完全退出)的时候获取通知,以便能够保存用户对数据作出的任何更改。
我们接下来讨论applicationWillResignActive:。这里使用的方法和前面一样,先获取对应用委托的引用,然后使用此引用获取应用的默认上下文指针。
然后使用循环语句为每个标签执行一次,获得每个字段对应的索引。
接下来,为line实体创建获取请求。需要确认持久存储中是否已经有一个与这个字段对应的托管对象,因此创建一个predicate,用于为字段标识正确的对象。
此时在上下文中执行获取请求并且检查objects是否为nil.如果为nil,则表示遇到错误,我们应该为应用执行合适的错误处理。
现在声明一个指向NSManagedObject的指针并将它设为nil.执行此操作的原因是,我们还不知道是要从持久存储里加载对象还是创建新的托管对象。因此,可以检查与条件匹配的返回对象。如果返回了有效对象,就加载,否则就创建一个新的托管对象来保存这个字段的文本。
接着,使用键值编码来设置行号和此托管对象的文本。
完成循环后,通知context保存更改。