需求
将数据保存至本地以便后续的使用,在应用中非常的常见,例如资讯类应用、即时通讯类应用等。即时非上述应用,那么也避免不了本地化用户的偏好信息,登陆信息等等。
iOS 开发有多种本地化的手段,针对不同场景显示出不同的优缺点,你可以根据任务的情况进行选择。
存储方式
iOS 本地化存储方式大体分为下面几种。
- 文件存储,如
NSUserDefaults
- 归档
-
SQLite
数据库 Core Data
iOS 中的沙盒(sandbox)
每个 iOS 应用程序都有自己的应用沙盒,可以将沙盒简单的理解为应用的文件夹,每个应用的沙盒都是相互独立的,其他应用不能访问该沙盒,自己也不能访问其他应用的沙盒。一般情况下,想要将应用文件分享到其他应用需要借助系统,编写相应的扩展才能实现,这涉及到 iOS 8 新出的程序扩展类容,这里不做过多的讲解。
沙盒的结构与用途
可以借助 Xcode 工具将开发程序的沙盒数据下载到本地,右击显示包内容查看当前应用下的沙盒目录,以及已经存储数据信息。
- Documents:保存应用运行时生成的需要持久化的数据,iTunes同步设备时会备份该目录。重要数据
- Library/Caches:保存应用运行时生成的需要持久化的数据,iTunes同步设备不会备份该目录。非重要数据
- Library/Preference:保存应用的偏好设置,iTunes同步设备时会备份该目录。
- tmp:保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统在存储空间紧缺的情况下也可能会清除该目录下的文件。iTunes同步设备时不会备份该目录。
注:使用 NSUserDefaults
对象存储数据时,会默认创建名为 BundleIdentifier.plist 的文件来保存数据,就如上图展示。
沙盒目录的获取方式
Documents 文件夹的获取方式
// 利用沙盒根目录拼接字符串
NSString *homePath = NSHomeDirectory();
NSString *docPath = [homePath stringByAppendingString:@"/Documents"];
// 利用沙盒根目录拼接”Documents”字符串
NSString *homePath = NSHomeDirectory();
NSString *docPath = [homePath stringByAppendingPathComponent:@"Documents"];
推荐下面的方式来获取
// NSDocumentDirectory 要查找的文件
// NSUserDomainMask 代表从用户文件夹下找
// 在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [path stringByAppendingPathComponent:@"xxx.plist"];
这里的参数说明一下:
第一个参数:枚举类型,常用的有
- NSDocumentDirectory(Documents文件夹)
- NSCachesDirectory(Library/Caches)
第二个参数:NSUserDomainMask,表示从用户文件夹下找。
第三个参数:打印的路径会是这种形式~/Documents,我们一般都会用YES,这样可以获取完整路径字符串。
这个方法的返回值是一个数组,但在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素,所以我们取第一个元素
Library/Caches 文件夹的获取方式
NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [path stringByAppendingPathComponent:@"student.data"];
tmp 文件夹的获取方式
NSString *tmp= NSTemporaryDirectory();
文件形式存储
- NSUserDefaults
NSUserDefaults 类设计用来保存应用的偏好属性,例如WIFI情况下是否自动播放视频,是否缓存图像等等,又或者是用户相关的数据信息等。
可以存储的数据类型:NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary。如果要存储其他类型,如 UIImage,则需要转换为前面的类型,才能用 NSUserDefaults 存储。
使用示例一:
// 存储
NSString *passWord = @"1234567";
NSUserDefaults *user = [NSUserDefaults standardUserDefaults];
[user setObject:passWord forKey:@"userPassWord"];
// 获取
NSUserDefaults *user = [NSUserDefaults standardUserDefaults];
NSString *passWord = [ user objectForKey:@"userPassWord"];
使用示例二:
//存数据
UIImage *image=[[UIImage alloc]initWithContentsOfFile:@"photo.jpg"];
NSData *imageData = UIImageJPEGRepresentation(image, 100);//UIImage对象转换成NSData
[[NSUserDefaults standardUserDefaults] setObject:imageData forKey:@"image"]
[[NSUserDefaults standardUserDefaults] synchronize];//用synchronize方法把数据即时持久化到standardUserDefaults数据库中,因为NSUserDefaults存储数据不是及时的
//取数据
NSData *imageData = [[NSUserDefaults standardUserDefaults] dataForKey:@"image"];
UIImage *Image = [UIImage imageWithData:imageData];//NSData转换为UIImage
使用示例三:
自定义对象存储到本地,需要转换为 NSData 类型,再存储到本地。我们需要借助归档对象实现自定义对象到 NSData 的转换,并且对象需要遵守 NSCoding 协议,实现 encodeWithCoder
和 initWithCoder
方法,前者是将对象进行编码,后者用来解码。
以 Student 类为例。
首先 .h 文件中要继承 NSCoding
协议,.m文件如下:
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.name forKey:@"name"];
[coder encodeInteger:self.age forKey:@"age"];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super init]) {
self.age = [coder decodeIntegerForKey:@"age"];
self.name = [coder decodeObjectForKey:@"name"];
}
return self;
}
自定义对象完成了 Coding 协议,我们来将自定义对象数组存储到本地。
// 存
NSArray * array = [NSArray arrayWithArray:dataArray];
[[NSUserDefaults standardUserDefaults] setObject:array forKey:@"allStudent"];
// 取
NSdData *data = [[NSUserDefaults standardUserDefaults] objectForKey:@"allStudent"];
NSArray *allStudentArray = [NSKeyedUnarchiver unarchiveObjectWithData:data];
注:不要企图将自定义对象数组直接存储到本地。
一般来说,NSUserDefaults 是被设计用来存储一段简单的偏好设置的,就如他的命名一样。不要将大篇幅的数据存储到 NSUserDefaults 中,因为 NSUserDefaults 本质上是文件存储,它会再你第一次使用它时为你创建一份 plist 文件,你的数据越大意味着存取所话费的时间越久。
- 自定义文件
除了使用 NSUserDefaults 来存储信息,我们也可以将数组写到其他文件中。
可以存储的数据类型:NSArray、NSDictionary、NSString、NSData。
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [path stringByAppendingPathComponent:@"myfile.plist"];
// 存储
NSArray *arr = [[NSArray alloc] initWithObjects:@"1", @"2", nil];
[arr writeToFile:filePath atomically:YES];
// 获取
NSArray *arr = [NSArray arrayWithContentsOfFile:filePath];
注意事项:
使用自定义文件存储时,同样需要注意数据的合法性,例如自定义对象数组是无法写入的。另外,一份文件中通常只能存储同一种类型,并且写入数据默认是覆盖的,所以如果你是想要追加写入时,需要先将数据读取出来,追加数据之后再次写入。
因此 NSUserDefaults 类已经帮你封装好了,使用起来无需亲自创建文件,数据的添加、修改等,使用自定义文件存储,不如使用 NSUserDefaults。但是考虑到局限性,依旧不推荐存储大篇幅的数据。
自定义对象的存取
在之前有演示过自定义对象的存储,自定义对象需要遵守 NSCoding 协议。
@interface Possession:NSObject<NSCoding> {
NSString *name;//待归档类型
}
@implementation Possession
-(void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:name forKey:@"name"];
}
-(void)initWithCoder:(NSCoder *)aDecoder{
name=[[aDeCoder decodeObjectforKey:@"name"] retain];
}
NSKeyedArchiver 除了可以将对象转换为 NSData 类型外,还能直接将数据保存到指定文件。
[NSKeyedArchiver archiveRootObject:allPossessions toFile: path];
allPossessions = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
归档能够方便的存储自定义对象,但是缺点也是显而易见的 -- 只能一次性归档一次性解压,如果想要修改部分数据,需要解压整个数据,所以只能针对数据量小的数据,否则会消耗巨大的内存空间,操作数组对象速度也不够快。
本地数据库
存储并管理大量数据时,数据库都是你非常不错的选择,有了数据库,你可以处理数据的数量级非常大,并且速度非常快。SQLite
是一款中小型数据库,在进行数据操作时候,需要使用到 C 语言的函数,相对比较麻烦。市面上有很多针对 SQLite
进行封装的库,如 FMDB
、PlausibleDatabase
等,其中 FMDB
是一款使用非常广泛的数据库。
FMDB
是对 libsqlite3 框架的封装,用起来的步骤与 SQLite 使用类似,并且它对于多线程的并发操作进行了处理,所以是线程安全的。
FMDB 中几个重要的类:
-
FMDatabase
:数据库对象 -
FMResultSet
:结果集 -
FMDatabaseQueue
: 线程安全的操作队列
创建数据库
FMDB 能够接受处理 sql 语句,下面我们来演示一下数据库的创建和表的创建。
NSString *doc =[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES) lastObject];
NSString *fileName = [doc stringByAppendingPathComponent:@“student.sqlite”];
// 获得数据库对象
FMDatabase *db = [FMDatabase databaseWithPath:fileName];
if ([db open]){
// 创表
BOOL result = [db executeUpdate:@“CREATE TABLE IF NOT EXISTS t_student (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, age integer NOT NULL);”];
if (result){
NSLog(@“创建表成功”);
}
}
除了查询的 SQL 命令使用 -excuteQuery:
,绝大部分的修改操作都是使用 -executeUpdate:
方法。
查询数据和处理结果集。
//查询整个表
FMResultSet *resultSet = [self.db executeQuery:@“select * from t_student;”];
// 遍历结果集合
while ([resultSet next]){
NSString *name = [resultSet objectForColumn:@“name”];
int age = [resultSet intForColumn:@“age”];
}
线程安全问题
在多个线程中使用一个 FMDatabase 实例是不安全的,FMDatabaseQueue 提供了一个线程安全的队列,你应该使用该队列取操作你的数据库,而不是 FMDatabase,除非你非常的确定数据操作不会发生在多个线程中。
//创建队列
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
__block BOOL whoopsSomethingWrongHappened = true;
//任务包装到事务里
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
whoopsSomethingWrongHappened &= [db executeUpdate:@“INSERT INTO myTable VALUES (?)”,[NSNumber numberWith:1]];
whoopsSomethingWrongHappened &= [db executeUpdata:@“INSERT INTO myTable VALUES (?)”,[NSNumber numberWithInt:2]];
whoopsSomethingWrongHappened &= [db executeUpdata:@“INSERT INTO myTable VALUES (?)”[NSNumber numberWithInt:3]];
// 如果有错误 返回
if (!whoopsSomethingWrongHappened) {
*rollback = YES;
return;
}
}];
关于本地的数据库查看,你依然可以下载沙盒文件到本地,通过一些数据库工具打开查询当前的数据库信息。
Core Data
Core Data 是 iOS5 推出的框架,它提供了对象-关系映射(ORM)功能,能够将对象转换成数据保存到 SQLite 数据库中,也能够将数据取出还原成对象。
总结
本地化手段多样,但都各司其职,少量、偏好性质的数据可以使用 NSUserDefaults
方式本地保存,大批量、需频繁操作的数据最好采用数据库进行存取。