在开发通常需要我们把一些数据存储在本地,这篇我们来介绍iOS中数据持久化得方法。
1.属性列表
2.对象归档
3.偏好设置
4.嵌入式数据库(SQLite3)
5.苹果公司提供的持久化工具 Core Data
以下我们围绕这4种方式逐一介绍,在上一遍种我们介绍了“沙盒”和文件管理的基础知识,我们将以上一篇为基础进行介绍后面内容
属性列表(plist),指定应用的配置比如tabbar的状态,黑白名单,网络请求ATS等等。我们可以通过Xcode或者Property List Editor手动编辑。并且只要字典或数组包含特定可序列化对象就可以将NSDictionary和NSArray实例写入属性列表或者创建。
序列化对象(serialized object)是指可以被转换为字节流以便于存储到文件中或者通过网络进行传输的对象。
可以被序列化得Objective-C类:
NSArray;
NSMutableArray;
NSDictionary;
NSMutableDictionary;
NSData;
NSMutableData;
NSString;
NSMutableString;
NSNumber;
NSDate;
我们按照上一篇中的方法获取需要存取的目录然后执行保存和读取的操作
//获取路径
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *fileName = [path stringByAppendingPathComponent:@"test.plist"];
//保存数组
NSArray *array = @[@"Gavin", @"Alice", @"Lucy"];
[array writeToFile:fileName atomically:YES];
//读取信息
NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
atomically:参数让该方法将数据写入辅助文件,而不是写入指定位置。成功写入该文件之后,辅助文件将被复制到第一个参数指定的位置,这是更安全的写入文件的方法,如果应用在保存期间崩溃,则现有文件不会被破坏。
属性列表中只能将一小部分对象存储,我们来看看比较强大的方法。
我们先来介绍一些基础的概念:
归档(archiving):是另一种形式的序列化,它可以在任何对象进行序列化,编写用于保存数据,可以轻松的将复杂的对象写入文件,然后再从中读取他们。
实现对象归档,都应该遵循NSCoding协议同时还应该遵循NSCopying协议,NSCopying可以允许复制对象,由于决大多数支持存储数据的Foundation和Cocoa Touch类都遵循了NSCoding协议,因此,对于大多数类来说,归档相对而言还是比较容易实现的。
NSCoding协议声明了两个方法,也就是归档和解码。归档将一个对象编码到归档中,解码对归档解码来创建一个新对象。我们通过下面的例子来了解
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assgin) NSInteger age;
@end
//归档
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.age forKey:@"age"];
}
//解档
- (id)initWithCoder:(NSCoder *)aDecoder {
if ([super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [aDecoder decodeIntegerForKey:@"age"];
}
return self;
}
实现这两个方法,就可以对对象的属性进行编码和解码,然后便可以对对象进行归档,并且将其写入归档或从归档中读取他们。
遵循NSCopying协议可以实现对象的复制,NSCopying有一个方法copyWithZone:方法实现它既可
-(id)copyWithZone:(NSZone *)zone{
Person *obj = [[[self class] allocWithZone:zone]init];
obj.name = [self.name copyWithZone:zone];
obj.age = self.age;
return obj
}
NSZone:参数它是指向系统用于管理内存的struct,现在的iOS完全可以忽略。
首先创建一个NSMutableData实例,用于包含编码的数据,然后创建NSKeyedArchiver实例,用于将对象归档到NSMutableData实例中
//用于存放编码数据
NSMutableData *data = [[NSMutableData alloc] init];
//编码类
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
//创建Person类
Person *teach = [[Person alloc] init];
teach.name = @"Gavin";
teach.age = 25;
//归档
[archiver encodeObject:teach forKey:@"TeachPerson"];
//完成编码
[archiver finnishEncoding];
//获取路径
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *fileName = [path stringByAppendingPathComponent:@"test"];
//写入文件 成功返回YES 失败返回NO
BOOL success = [data writeToFile:fileName atomically:YES];
//解码
NSData *decoderData = [[NSData alloc] initWithContentsOfFile:fileName];
NSKeyedArchiver *unarchiver = [[NSKeyedArchiver alloc] initForReadingWithData:decoderData ];
Person *gavin = [Person alloc] init];
gavin = [unarchiver decodeObjectForKey:@"TeachPerson"];
[unarchiver finishDecoding];
/*
也可使用
归档[NSKeyedArchiver archiveRootObject:person toFile:fileName];
解码[NSKeyedUnarchiver unarchiveObjectWithFile:file];
*/
注意:如果需要归档的类是某个自定义类的子类时,就需要在归档和解档之前先实现父类的归档和解档方法。即 [super encodeWithCoder:aCoder] 和 [super initWithCoder:aDecoder] 方法;
偏好设置是专门用来保存应用程序的配置信息的,一般不要在偏好设置中保存其他数据。如果没有调用synchronize方法,系统会根据I/O情况不定时刻地保存到文件中。所以如果需要立即写入文件的就必须调用synchronize方法。偏好设置会将所有数据保存到同一个文件中。即preference目录下的一个以此应用包名来命名的plist文件。
//1.获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向文件中写入内容
[userDefaults setObject:@"teach" forKey:@"name"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];
//3.读取文件
NSString *name = [userDefaults objectForKey:@"name"];
NSInteger age = [userDefaults integerForKey:@"age"];
SQLite3是iOS中嵌入式SQL数据库,SQLite3在储存和检索数据方面非常有效,它能够对数据进行复杂的聚合,速度更快。
SQLite3使用SQL(Structured Query Language结构化查询语言),数据库基本介绍可以点击这里,下面我们来看数据库的使用。
我们要使用使用SQLite3需要连接libsqlite3.dylib的动态库
//访问路径方法方便我们获取目录
- (NSString *)dataFilePath{
//获取路径
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
return [path stringByAppendingPathComponent:@"data.sqlite"];
}
- (void)testSQL3{
//创建数据库
aqlite *database;
//因为是C语言库,不能直接使用NSString
const char *path = [self dataFilePath] UTF8String];
//sqlite_open(path, &database == SQLIE_OK说明打开数据库成功
if (sqlite_open(path, &database) != SQLIE_OK){
//创建数据库失败
sqlite_close(database);
NSAssert(0,@"Failed to open database");
}
//创建表
NSString *createSQL = @"CREATE TABLE IF NOT EXISTS t_person(id integer primary key autoincrement, name text, age integer)";
char *errorMsg;
if(sqlite3_exec(database,[createSQL UTF8String)], NULL, NUMM, &errorMsg) != SQLIE_OK){
//表创建失败
sqlite_close(database);
//输出错误信息
NSAssert(0,@"Error creating table:%s", errorMsg);
}
//插入数据
//我们上面的Person类创建实例存入
//SQL语句
NSString *sql = [NSString stringWithFormat:@"INSERT INTO t_person (name, age) VALUES('%@', '%ld')", teach.name, teach.age];
if (sqlite3_exec(database, sql.UTF8String, NULL, NULL, &errorMsg) != SQLIE_OK){
//插入失败
NSAssert(0,@"Error inster:%s", errorMsg);
}
//查询数据
NSString8 *query = @"select name, age from t_person;";
sqlite3_stmt *statement;
//sqlite3_prepare_v2:检查sql的合法性
if (sqlite3_prepare_v2(database, sql, -1, &statement, NULL) == SQLIE_OK){
//便利返回每一行
//sqlite3_step() : 逐行获取查询结果,不断重复,直到最后一条记录
while(sqlite3_step(statement) == SQLIE_OK){
//sqlite3_coloum_xxx() : 获取对应类型的内容,iCol对应的就是SQL语句中字段的顺序,从0开始。
//根据实际查询字段的属性,使用sqlite3_column_xxx取得对应的内容即可。
char *name = (char *)sqlite3_column_text(statement, 0);
NSInteger age = sqlite3_column_int(statement, 1);
Person *teach = [[Person alloc] init];
teach.name = [[NSString alloc] initWithUTF8String:name];
teach.age = age;
}
//关闭数据库连接
sqlite_finalize()
}
sqlite_close(database);
}
使用SQLite3是比较麻烦的可以使用现有的框架FMDB,可以再GitHub上搜索。
以下总结以下FMDB的一些方法方便查阅引用对FMDB的使用,也可以访问GitHub-FMDB。
FMDB有三个主要的类:
FMDatabase
一个FMDatabase对象就代表一个单独的SQLite数据库,用来执行SQL语句
FMResultSet
使用FMDatabase执行查询后的结果集
FMDatabaseQueue
用于在多线程中执行多个查询或更新,它是线程安全的
和c语言框架一样,FMDB通过指定SQLite数据库文件路径来创建FMDatabase对象,但FMDB更加容易理解,使用起来更容易,使用之前一样需要导入sqlite3.dylib。打开数据库方法如下:
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
FMDatabase *database = [FMDatabase databaseWithPath:path];
if (![database open]) {
NSLog(@"数据库打开失败!");
}
值得注意的是,Path的值可以传入以下三种情况:
具体文件路径,如果不存在会自动创建
空字符串@”“,会在临时目录创建一个空的数据库,当FMDatabase连接关闭时,数据库文件也被删除
nil,会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁
在FMDB中,除查询以外的所有操作,都称为“更新”, 如:create、drop、insert、update、delete等操作,使用executeUpdate:方法执行更新:
//常用方法有以下3种:
- (BOOL)executeUpdate:(NSString*)sql, ...
- (BOOL)executeUpdateWithFormat:(NSString*)format, ...
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments
//示例
[database executeUpdate:@"CREATE TABLE IF NOT EXISTS t_person(id integer primary key autoincrement, name text, age integer)"];
//或者
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES(?, ?)", @"Bourne", [NSNumber numberWithInt:42]];
- (FMResultSet *)executeQuery:(NSString*)sql, ...
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments
在多个线程中同时使用一个FMDatabase实例是不明智的。不要让多个线程分享同一个FMDatabase实例,它无法在多个线程中同时使用。 如果在多个线程中同时使用一个FMDatabase实例,会造成数据混乱等问题。所以,请使用 FMDatabaseQueue,它是线程安全的。以下是使用方法:
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
[queue inDatabase:^(FMDatabase *database) {
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_1", [NSNumber numberWithInt:1]];
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_2", [NSNumber numberWithInt:2]];
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_3", [NSNumber numberWithInt:3]];
FMResultSet *result = [database executeQuery:@"select * from t_person"];
while([result next]) {
}
}];
而且可以轻松地把简单任务包装到事务里:
[queue inTransaction:^(FMDatabase *database, BOOL *rollback) {
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_1", [NSNumber numberWithInt:1]];
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_2", [NSNumber numberWithInt:2]];
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_3", [NSNumber numberWithInt:3]];
FMResultSet *result = [database executeQuery:@"select * from t_person"];
while([result next]) {
}
//回滚
*rollback = YES;
}];
FMDatabaseQueue 后台会建立系列化的G-C-D队列,并执行你传给G-C-D队列的块。这意味着 你从多线程同时调用调用方法,GDC也会按它接收的块的顺序来执行。
Core Data是一款稳定、功能全面的持久化工具。我们先看代码实现上面的操作
- (void)test{
//获取app代理
AppDelegate *appDelegate = [UIApplication shareApplication].delegate;
//创建上下文
NSManageObjectContext *context = [appDelegate manaedObjectContext];
//创建请求
NSFetchRequest *request = [[NSFetchRequest alloc]initWithEntityName:@"test"];
//存储
NSManageObject *coreData= NSEntitydescription insertNewObjectForEntityForName:@"test" inManagedObjectContext:context];
[coreData setValue:teach.name forKey:@"name"];
[coreData setValue:teach.age forKey:@"age"];
//保存上下文
[appDelegate saveContext];
//上下文返回库中每一个对象
NSError *error;
NSArray *objects = [context executeFetchRequest:request error &error];
//便利数组查找
//我们只存了一条数据那么我们只去第一个
Person *teach = [[Person alloc] init];
teach.name = [objects[0] valueForkey:@"name"];
teach.age = [objects[0] valueForkey:@"age"];
}
需要注意的是:我们在数据操作的时候需要监控app的生命周期以保证数据的稳定。
Core Data 更加详细的用法大家可以查看官方文档,API介绍非常详细,在创建程序的记得勾选使用Core Data。
联系写了5个小时,也就是现在有时间。其中一些怕介绍不全,之后慢慢补充。发现有问题请评论