问题:如何把一个包含自定义对象的数组序列化到磁盘?
涉及的知识点:iOS开发中的本地数据存储(持久化)
一、iOS开发中本地存储主要有三种形式
1、plist文件(属性列表)
2、preference(偏好设置)
3、NSKeyedArchiver(归档)
4、SQLite 3
5、CoreData
我们先来了解一下沙盒,每个应用的沙盒是相对独立。iOS本地化存储的数据保存在沙盒中。
Documents:iTunes会备份该目录。一般用来存储需要持久化的数据。
Library/Caches:缓存,iTunes不会备份该目录。内存不足时会被清除,应用没有运行时,可能会被清除,。一般存储体积大、不需要备份的非重要数据。
Library/Preference:iTunes同会备份该目录,可以用来存储一些偏好设置。
tmp: iTunes不会备份这个目录,用来保存临时数据,应用退出时会清除该目录下的数据。
1、plist文件
(1)简介
plist文件是将某些特定的类通过xml文件的方式保存在目录中。可以被序列化的类型一般为OC提供的基本类型,如下:NSArrasy(NSMutableArray)、NSDictionary(NSMutableDictionary)、NSDate(NSMutableDate)、NSString(NSMutableString)、NSNumber、NSDate。保存在沙盒的Documents中。
(2)使用方法
a.获得文件路径
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *fileName = [path stringByAppendingPathComponent:@ "123.plist" ];
b.存储
NSArray *array = @[@ "123" , @ "456" , @ "789" ];
[array writeToFile:fileName atomically:YES];
c.读取
NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@ "%@" , result);
(3)部分参数介绍
- 存储时使用**writeToFile: atomically:**方法。 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。这是更安全的写入文件方法,一般都写YES。
- 读取时使用**arrayWithContentsOfFile:**方法
2 、preference(偏好设置)
(1)介绍:
NSUserDefaults适合存储轻量级的本地数据,用来保存应用程序设置和属性、用户保存的数据,比如要保存一个登陆界面的数据,用户再次打开程序或开机后这些数据仍然存在。NSUserDefaults可以存储的数据类型包括:NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary。如果要存储其他类型,则需要转换为前面的类型,才能用 NSUserDefaults 存储,比如自定义对象,需要将对象转化为NSData在保存。
(2)使用实例
//将对NSUserDefault操作封装了两个方法,避免代码中到处是对NSUserDefault的操作
//存储数据
-(void)saveToUserDefaults:(NSString*)tosaveedString withKey:(NSString *)tosaveedKey
{
NSUserDefaults * tmp = [NSUserDefaults standardUserDefaults];
if (tmp) {
[tmp setObject:tosaveedString forKey:tosaveedKey];
[tmp synchronize];
}
}
//读出数据
-(NSString *)restoreFromUserDefaults:(NSString *)key
{
NSString * rtn = nil;
NSUserDefaults * tmp = [NSUserDefaults standardUserDefaults];
if (tmp) {
rtn = [tmp objectForKey:key];
}
return rtn;
}
//存入数据
[self saveToUserDefaults:textPass.text withKey:@"saveUserPass"];
//使用读出数据
NSString *isLoginCode=[self restoreFromUserDefaults:@"saveUserPass"];
注意
- 偏好设置是专门用来保存应用程序的配置信息的,一般不要在偏好设置中保存其他数据。
- 如果没有调用synchronize方法,系统会根据I/O情况不定时刻地保存到文件中。所以如果需要立即写入文件的就必须调用synchronize方法。
- 偏好设置会将所有数据保存到同一个文件中。即preference目录下的一个以此应用包名来命名的plist文件。
3、NSKeyedArchiver(归档)
(1)介绍
自定义对象不能直接写入文件,保存到磁盘,我们需要使用
NSKeyedArchiver将对象archive为NSData类型的数据,如果数据模型支持archive,那么数据模型类需要遵守NSCoding协议,并提供encodeWithCoder:和initWithCoder:方法。前一个方法告诉系统怎么对对象进行编码,而后一个方法则是告诉系统怎么对对象进行解码。
(2)实例:自定义对象序列化到磁盘
Possession类
@interface Possession:NSObject<NSCoding>{//遵守NSCoding协议
NSString *name;//待归档类型
}
@implementation Possession
-(void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:name forKey:@"name"];
}
-(void)initWithCoder:(NSCoder *)aDecoder{
name=[aDeCoder decodeObjectforKey:@"name"];
}
master类
@interface Master:NSObject<NSCoding>{//遵守NSCoding协议
Possession *possession;
NSString *position;
}
@implementation Possession
-(void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:possession forKey:@"possession"];//Possession类遵循NSCoding协议,可以归档
[aCoder encodeObject:position forKey:@"position"];
}
-(void)initWithCoder:(NSCoder *)aDecoder{
possession=[aDeCoder decodeObjectforKey:@"possession"];
position= [aDeCoder decodeObjectforKey:@"position"];
}
归档操作:
如果对Possession对象allPossession归档保存,只需要使用NSKeyedArchiver的方法archiveRootObject:toFile: 即可。
NSString *path = [self possessionArchivePath];
[NSKeyedArchiver archiveRootObject:allPossessions toFile: path ]
解档操作:
使用NSKeyedUnarchiver的方法unarchiveRootObject:toFile: 即可
allPossessions = [[NSKeyedUnarchiver unarchiveObjectWithFile:path] retain];
缺点:归档的形式来保存数据,只能一次性归档保存以及一次性解压。所以只能针对小量数据,而且对数据操作比较笨拙,即如果想改动数据的某一小部分,还是需要解压整个数据或者归档整个数据。
注意
(1)编码解码要同步:新版本增加一个可以key,那么要在 initWithCoder: 和 decodeWithCoder: 同时操作,处理好这个新的属性的归档、解档。
(2) 提供向后兼容性,即新、旧版本户型读取彼此的文档,实现向后兼容性就需要知道旧版本的编解码。然后按照找不到key的情况,处理新旧版本互读。
找不到key的情况:例如新版本增加一个属性,那么新版本在解码老版本的归档对象时,会出现找不到key的情况,此时系统会给一个默认值,如果默认值不满足需求可以使用 containsValueForKey 查看是否有对应的 key ,如果有正常解档该属性,没有的话可以按照自己的需求处理,如给其一个默认值或者不处理。
(3)提供向前兼容性,即对于未来可能更改的属性,给一个默认值。比如说某衣服现在有5种样式, 未来大有可能增加新的样式, 那么在解码时检测这个样式是否是当前允许的值, 如果不是, 给一个默认值, 这样在以后的扩展中 老版本仍可正常表现.
(4)Key 在当前对象的范围内应是唯一的,在子类使用的Key可能与其父类使用的Key 相冲突.因此, 在框架的公共类中, 应使用带前缀的字符串, 以避免冲突,前缀可以是类名或者包名。
(5)如果一个自定义的类 A,作为另一个自定义类 B 的一个属性存在;那么,如果要对 B 进行归档,那么,B 要实现 NSCoding 协议。并且,A 也要实现 NSCoding 协议。
(6)尽量减少编码和解码的对象数量, 这样写或读时的速度就会更快.
4、SQLite 3
之前的所有存储方法,都是覆盖存储,即后期想在文件中增加一条数据必须把整个文件读出来,修改完成后再把整个内容覆盖写入文件,这个特性使它们所以不适合存储大量的内容,存储大量数据时使用轻型数据库SQLite,它占用率资源低且处理速度快。
(1)字段类型
对于使用者而言,SQLite将数据分为以下几种类型:
- integer : 整数
- real : 实数(浮点数)
- text : 文本字符串
- blob : 二进制数据,比如文件,图片之类的
实际上SQLite是无类型的。即不管你在创表时指定的字段类型是什么,存储时依然可以存储任意类型的数据。而且在创表时也可以不指定字段类型。SQLite之所以设计字段类型是为了良好的编程规范和方便开发人员交流,所以平时在使用时最好设置正确的字段类型!主键必须设置成integer
(2)准备工作
导入依赖库啦,在iOS中要使用 SQLite3,需要添加库文件libsqlite3.dylib并导入主头文件。
(3)使用
- 创建数据库并打开
操作数据库之前必须先指定数据库文件和要操作的表,所以使用SQLite3,首先要打开数据库文件,然后指定或创建一张表。
//打开数据库并创建一个表
-(void)openDatabase {
//1.设置文件名
NSString *filename = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db" ];
//2.打开数据库文件,如果没有会自动创建一个文件
NSInteger result = sqlite3_open(filename.UTF8String, &_sqlite3);
if (result == SQLITE_OK)
{
NSLog(@ "打开数据库成功!" );
//3.创建一个数据库表
char *errmsg = NULL;
sqlite3_exec(_sqlite3, "CREATE TABLE IF NOT EXISTS t_person(id integer primary key autoincrement, name text, age integer)" , NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@ "错误:%s" , errmsg);
} else {
NSLog(@ "创表成功!" );
}
}else{
NSLog(@ "打开数据库失败!" );
}
}
- 执行指令
使用 sqlite3_exec() 方法可以执行任何SQL语句,比如创表、更新、插入和删除操作。但是一般不用它执行查询语句,因为它不会返回查询到的数据。
//往表中插入1000条数据
-(void)insertData {
NSString *nameStr;
NSInteger age;
for (NSInteger i = 0; i < 1000; i++) {
nameStr = [NSString stringWithFormat:@ "Bourne-%d" , arc4random_uniform(10000)];
age = arc4random_uniform(80) + 20;
NSString *sql = [NSString stringWithFormat:@ "INSERT INTO t_person (name, age) VALUES('%@', '%ld')" , nameStr, age];
char *errmsg = NULL;
sqlite3_exec(_sqlite3, sql.UTF8String, NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@ "错误:%s" , errmsg);
}
}
NSLog(@ "插入完毕!" );
}
- 查询指令
查询相对比较麻烦,为得到查询数据必须要获得查询结果,需使用sqlite3的下面函数- sqlite3_prepare_v2() : 检查sql的合法性。
- sqlite3_step() : 逐行获取查询结果,不断重复,直到最后一条记录,
- sqlite3_coloum_xxx() : 获取对应类型的内容,iCol对应的就是SQL语句中字段的顺序,从0开始。根据实际查询字段的属性,使用sqlite3_column_xxx取得对应的内容即可
- sqlite3_finalize() : 释放stmt
//从表中读取数据到数组中
-(void)readData {
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1000];
char *sql = "select name, age from t_person;" ;
sqlite3_stmt *stmt;
NSInteger result = sqlite3_prepare_v2(_sqlite3, sql, -1, &stmt, NULL);//检查sql合法性
if (result == SQLITE_OK)
{ //while循环,使用sqlite3_step()逐行查询
while (sqlite3_step(stmt) == SQLITE_ROW) {
char *name = (char *)sqlite3_column_text(stmt, 0);//sqlite3_column_xx获取结果
NSInteger age = sqlite3_column_int(stmt, 1);
//创建对象
Person *person = [Person personWithName:[NSString stringWithUTF8String:name] Age:age];
[mArray addObject:person];
}
self.dataList = mArray;
}
sqlite3_finalize(stmt);
}