这几天在刚把项目做好,抽空来学习下本地化持久存储,下面是学习总结
1. iOS数据存储的几种方式
- Preference(偏好设置)
- plist存储
- 归档
- SQLite3
- CoreData
2. 应用沙盒
1. 每个iOS应用都有自己的应用沙盒(应用沙盒就是文件系统目录),与其他文件系统隔离。应用必须待在自己的沙盒里,其他应用不能访问该沙盒
2. 沙盒路径结构
-
Document
:适合存储重要的数据, iTunes同步应用时会同步该文件下的内容,(比如游戏中的存档) -
Library/Caches
:适合存储体积大,不需要备份的非重要数据,iTunes不会同步该文件 -
Library/Preferences
: 通常保存应用的设置信息, iTunes同步该应用时会同步此文件夹中的内容 -
tmp
:保存应用的临时文件,用完就删除,系统可能在应用没在运行时删除该目录下的文件,iTunes不会同步该文件
路径 | 保存数据特点 | iTunes是否同步 |
---|---|---|
Document | 适合存储重要的数据 | 同步 |
Library/Caches | 体积大,不需要备份 | 不同步 |
Library/Preferences | 应用的设置信息 | 同步 |
tmp | 临时文件 | 不同步 |
3. 获取沙盒路径
3.1 获取 Document / Caches
下的路径
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
- 第一个常量
NSDocumentDirectory
表示正在查找沙盒Document
目录的路径(如果参数为NSCachesDirectory
则表示沙盒Cache目录)
- 第二个常量
NSUserDomainMask
表明我们希望将搜索限制在应用的沙盒内
NSString *documentFilePath = paths.firstObject;
- 因为每一个应用只有一个Documents目录,所以这里取第一个和最后一个数据都是一样的
3.2 获取 tmp
下的路径
- (NSString *)getCacheFilePath
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return paths.firstObject;
}
3. 偏好设置(NSUserDefaults)
- 偏好设置是专门保存应用的配置信息的,如保存用户名、密码、字体大小、是否登陆等设置,一般不要在偏好设置保存其他数据
-
NSUserDefaults
适合存储轻量级的本地数据,支持的数据类型有:NSNumbe (NSInteger、float、double
),NSString
,NSDate
,NSArray
,NSDictionary
,BOOL
,NSData
使用
保存数组到NSUserDefaults
- (IBAction)saveUserDefults:(id)sender {
NSArray *arr = @[@"test"];
[[NSUserDefaults standardUserDefaults] setObject:arr forKey:@"arr"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
取出保存在NSUserDefaults
中的数组
- (IBAction)userDefault:(id)sender {
NSArray *arr = [[NSUserDefaults standardUserDefaults] objectForKey:@"arr"];
NSLog(@"%@", arr);
}
打印信息:
2017-04-05 19:13:21.147 SaveFile[17095:2509894] (
test
)
需要注意的是 NSUserDefaults
保存的数据都是不可变的,取出来的数据也是不可变类型
- (IBAction)userDefault:(id)sender {
//这里会崩溃 NSUserDefaults取出的数据都是不可变的
NSMutableArray *arr = [[NSUserDefaults standardUserDefaults] objectForKey:@"arr"];
[arr addObject:@"test1"];
NSLog(@"%@", arr);
}
log输出为
2017-04-05 19:29:57.739 SaveFile[17147:2576405] -[__NSSingleObjectArrayI addObject:]: unrecognized selector sent to instance 0x618000008a50
- 我们为一个不可变的数组添加了数据,所以会崩溃了
- 如果想要添加数据,可以把取出的不可变数组转为可变数组
NSUserDefaults
保存的是基本数据类型, 不能保存自定义数据对象
- (IBAction)saveUserDefults:(id)sender {
Person *p1 = [[Person alloc] init];
[[NSUserDefaults standardUserDefaults] setObject:p1 forKey:@"person1"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
这里会直接崩溃, log为
2017-04-05 19:36:56.455791 SaveFile[17189:2620826] [User Defaults] Attempt to set a non-property-list object <Person: 0x608000226200> as an NSUserDefaults/CFPreferences value for key person1
小结
NSUserDefaults
优点:
- .不需要关心文件名
- 快速进行键值对存储
- 直接存储基本数据类型
缺点:
- 不能存储自定义数据
- 取出的数据都是不可变的
4. plist
存储
plist
存储的不是数组就是字典
-
plist
不能存储自定义对象
使用plist
存储数据
- (IBAction)plist:(id)sender {
// 获取到Caches文件夹路径
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
// 拼接文件名
NSString *filePath = [cachePath stringByAppendingPathComponent:@"personInfo.plist"];
// 将数据封装成字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:@"100001" forKey:@"qq"];
[dict setObject:@"18" forKey:@"age"];
// 将字典持久化到沙盒文件中
[dict writeToFile:filePath atomically:YES];
}
运行之后可以查看 filePath
路径下的文件
点击打开查看数据
可以看到, 我们已经把数据存储到 plist
中了
如果对象是
NSString、NSDictionary、NSArray、NSData、NSNumber
等类型,就可以使用writeToFile:atomically:
方法直接将对象写到属性列表文件中
5. 归档解归档(存储自定义对象)
- 有的时候我们需要存储一些自定义数据对象,这时候用上面的方法就不合适了,
我们可以使用NSKeyedArchiver
的archiveRootObject: toFile:
方法直接归档一个对象,然后使用NSKeyedUnarchiver
的 unarchiveObjectWithFile:
解档对象
- (IBAction)archive:(id)sender {
Person *p1 = [[Person alloc] init];
p1.name = @"test";
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject ;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];
[NSKeyedArchiver archiveRootObject:p1 toFile:filePath];
NSLog(@"%@", p1.name);
}
解归档操作
#pragma mark - 解归档
- (IBAction)decode:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject ;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];
NSKeyedUnarchiver *unarchiver = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath] ;
Person *p1 = [unarchiver decodeObjectForKey:@"person1"];
NSLog(@"%@", p1.name);
}
运行点击按钮,发现会崩溃, log为
2017-04-05 20:49:31.505 SaveFile[17605:3101098] -[Person encodeWithCoder:]: unrecognized selector sent to instance 0x60800002fda0
那是因为我们在归档自定义对象的时候, 该对象必须遵守NSCoding
协议,并实现协议方法
- (void)encodeWithCoder:(NSCoder *)aCoder
方法的作用是告诉系统当前对象中那些属性需要归档, 当一个对象需要归档的时候会调用该方法
-
- (instancetype)initWithCoder:(NSCoder *)aDecoder
的作用是告诉系统哪些属性需要解档,在解析文件的时候会调用该方法
遵守协议
实现协议方法
//只要解析一个文件的时候就会调用
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init]) {
//要解归档的属性
_name = [aDecoder decodeObjectForKey:@"name"];
_age = [aDecoder decodeIntForKey:@"age"];
}
return self;
}
//当一个对象要归档的时候就会调用这个方法归档
- (void)encodeWithCoder:(NSCoder *)aCoder
{
//需要归档的属性
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.age forKey:@"age"];
}
这时候我们运行程序,点击按钮,可以发现能够保存数据到指定的路径下,并且能够获取到保存的对象
不过这种方法也有一个弊端, 那就是只能存储单个对象,当我们想要存储多个对象时这种方法就不行了,
举个例子:
#pragma mark - 归档
- (IBAction)archive:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject ;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];
Person *p1 = [[Person alloc] init];
p1.name = @"test";
Person *p2 = [[Person alloc] init];
p2.name = @"dev";
//将对象直接写入到文件中
[NSKeyedArchiver archiveRootObject:p1 toFile:filePath];
[NSKeyedArchiver archiveRootObject:p2 toFile:filePath];
NSLog(@"%@ %@", p1.name, p2.name);
}
这里我们存储了两个Person对象,这里通过解归档取出保存的数据
#pragma mark - 解归档
- (IBAction)decode:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject ;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];
Person *p1 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
Person *p2 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
NSLog(@"%@ %@", p1.name, p2.name);
}
log打印为
2017-04-06 15:57:19.752 SaveFile[7361:1750135] dev dev
- 可以看到我们取出的是最后一次归档的对象数据
当我们需要存储多个对象的时候需要使用下面的方式
#pragma mark - 归档
- (IBAction)archive:(id)sender {
//新建一块可变数据区(临时存储空间,以便随后写入文件,或者存放从磁盘读取的文件内容)
NSMutableData *data = [[NSMutableData alloc] init];
//将数据区连接到NSKeyedArchiver对象
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
Person *p1 = [[Person alloc] init];
p1.name = @"test";
//存档对象 存档的数据都会存储到NSMutableData中
[archiver encodeObject:p1 forKey:@"person1"];
Person *person2 = [[Person alloc] init];
person2.name = @"dev";
[archiver encodeObject:person2 forKey:@"person2"];
//结束存档
[archiver finishEncoding];
NSString *filePath = [[[WYFileManager shareFileManager] getDocumentFilePath] stringByAppendingPathComponent:@"personModel"];
//将存档的数据保存到本地
[data writeToFile:filePath atomically:YES];
}
我们在存档对象的时候会传过去一个key,随后我们会通过这个key来取得存档的数据
可以看出我们可以通过·NSKeyedArchiver
的属性方法来归档多种数据类型
根据key
来解归档对象
#pragma mark - 解归档
- (IBAction)decode:(id)sender {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentFilePath = paths.firstObject ;
NSString *filePath = [documentFilePath stringByAppendingPathComponent:@"personModel"];
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
//反归档对象 会调用对象的initWithCoder方法 所以需要实现该方法
Person *p1 = [unarchiver decodeObjectForKey:@"person1"];
Person *person2 = [unarchiver decodeObjectForKey:@"person2"];
//结束解归档
[unarchiver finishDecoding];
NSLog(@"%@ %@ ", p1.name, person2.name);
}
log输出为
2017-04-06 16:26:13.979 SaveFile[8526:1905284] test dev
- 可以看出通过这种方式可以保存多个对象到同一个文件中
小结:
-
NSKeyedArchiver
是用来归档对象的时候使用的类,NSKeyedUnarchiver
是用来解归档的时候使用,归档的时候会调用对象的- (void)encodeWithCoder:(NSCoder *)aCoder
方法,解归档的时候会调用对象的- (instancetype)initWithCoder:(NSCoder *)aDecoder
方法 - 我们应在对象的
- (void)encodeWithCoder:(NSCoder *)aCoder
方法中告诉系统要归档的属性,在- (instancetype)initWithCoder:(NSCoder *)aDecoder
方法中解归档属性 -
NSKeyedArchiver
和NSKeyedUnarchiver
总是成对出现的,调用了NSKeyedArchiver
的类方法或者属性方法进行了归档操作,那么在某处就会调用NSKeyedUnarchiver
的类方法或者属性方法进行解归档操作- 使用
[NSKeyedArchiver archiveRootObject:p1 toFile:filePath];
归档对象,相应的需要使用Person *p1 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
方法解归档对象
- 使用
NSKeyedArchiver
的属性方法[archiver encodeObject:person2 forKey:@"person2"];
来归档对象,解归档的时候就需要使用NSKeyedUnarchiver
的属性方法Person *p1 = [unarchiver decodeObjectForKey:@"person1"];
获取存储的对象数据
- 使用