iOS:持久化(数据存储)

Page content

合理的记忆方式可以深刻而高效,计算机也是这样

沙盒

概念

  1. 每个iOS应用都有自己的应用沙盒(应用沙盒就是文件系统目录)与其他文件系统隔离,应用必须待在自己的沙盒里,其他应用不能访问该沙盒。
  2. 模拟器应用沙盒的根路径在: /Users/用户名/Library/Application Support/iPhone Simulator/6.0(模拟器版本)/Applications 或者: /Users/用户名/资源库/Application Support/iPhone Simulator/6.1/Applications
  3. 注意:默认情况下,模拟器的目录是隐藏的,要想显示出来,需要在Mac终端输入下面的命令: 显示Mac隐藏文件的命令:defaults write com.apple.finder AppleShowAllFiles YES 隐藏Mac隐藏文件的命令:defaults write com.apple.finder AppleShowAllFiles NO

沙盒类型

  1. Documents:保存应用运行时生成的需要持久化的数据,iTunes同步设备时会备份该目录,例如游戏应用可将游戏存档保存在该目录。
  2. temp:保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统也可能会清除该目录下的文件,iTunes同步设备时不会备份该目录。
  3. Library/Caches:保存应用运行时生成的需要持久化的数据,iTunes同步设备时不会备份该目录。一般存储体积大,不需要备份的非重要数据。
  4. Library/Preference:保存应用的所有偏好设置,iOS的setting(设置)应用会在该目录中查找应用的设置信息。iTunes同步设备时会备份该目录.

实例

获取沙盒目录:

~~~objective-c
		/// Documents
 		NSString * documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
 		/// Caches
    NSString * cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
    /// Library
    NSString * libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES)[0];
    /// Temp
    NSString * tmpPath = NSTemporaryDirectory();
~~~

获取到沙盒目录后,选择对应的路径,拼接新的路径,即可进行写入存储操作:

~~~objective-c
NSString * savePath = [cachesPath stringByAppendingString:@"savedArray"];
~~~

属性列表

概念

  1. 属性列表是一种XML格式的文件,拓展名为plist。
  2. 如果对象是NSString, NSDictionary, NSArray, NSData, NSNumber等类型,就可以使用:writeToFile:atomiclly:方法直接将对象写到属性列表文件中。

实例

属性列表-归档NSArray到一个plist属性列表中:

~~~objective-c
/**
* 属性列表write写入沙盒中的缓存目录
*/
- (IBAction)saveDataToFile:(id)sender {    
    NSString * cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
    NSString * savePath = [cachesPath stringByAppendingString:@"array"];
    if ([[NSFileManager defaultManager] fileExistsAtPath:savePath]) {
        [[NSFileManager defaultManager] removeItemAtPath:savePath error:nil];
    }
    NSArray * array = @[@{@"1":@"11"},@{@"2":@"22"},@{@"3":@"33"}];
    [array writeToFile:savePath atomically:YES];
}
~~~

读取:

~~~objective-c
/**
* 获取该目录下的属性列表信息
*/
- (void)getArraySaved {
    NSString * cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
    NSString * savePath = [cachesPath stringByAppendingString:@"array1"];
    NSArray * array = [NSArray arrayWithContentsOfFile:savePath];
    if (array) {
        for (NSDictionary * dict in array){
            NSLog(@"%@",dict);
        }
    }
}
~~~

Preference

概念

  1. 很多iOS应用都支持偏好设置,比如保存UserId,城市区域zoneId,字体大小,是否自动登录等设置;
  2. iOS提供了一套标准的解决方案来为应用加入偏好设置功能;
  3. 每个应用都有个NSUserDefaults实例,通过它来存取偏好设置。

实例

以下为存储是否自动登录(仅为举例,不出于安全考虑):

~~~objective-c
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:YES forKey:@"key_auto_login"];
[defaults synchornize];
~~~

读取上次保存的设置:

~~~objective-c
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL autoLogin = [defaults boolForKey:@"key_auto_login"];
~~~

注:NSUserDefaults设置数据时,不是立即写入,而是根据时间戳定时地把缓存中的数据写入本地磁盘;

所以调用了set方法之后数据有可能还没有写入磁盘应用程序就终止了,出现此问题可以通过调用synchornize方法强制写入。

NSKeyedArchiver

概念

  1. 归档是针对对象进行序列化存储的一种方案;
  2. 如果对象是NSString, NSDictionary, NSArray, NSData, NSNumber等类型,就可以直接使用NSKeyedArchiver进行归档和恢复;
  3. 如果是自定义的类的对象进行归档,则要实现NSCoding的协议。

注:如果父类也遵守了NSCoding协议,那么:

  1. 在encodeWithCoder:中调用 [super encodeWithCode:encode];确保继承的实例变量也能被编码;
  2. 在initWithCoder:中调用 self = [super initWithCoder:decoder];确保继承的实例变量也能被解码。

实例-基础对象

路径:

~~~objective-c
NSString * cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
NSString * savePath = [cachesPath stringByAppendingString:@"array"];
~~~

归档:

~~~objective-c
/**
* 归档
*/
- (void)arrayArchive {
    NSArray *array = [NSArray arrayWithObjects:@"1",@"2",nil];
    [NSKeyedArchiver archiveRootObject:array toFile:savePath];
}
~~~

解档:

~~~objective-c
/**
* 解档
*/
- (void)arrayUnArchive {
    NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:savePath];
    NSLog(@"%@", array);
}
~~~

实例-自定义对象

自定义对象接口:

~~~objective-c
@interface Person : NSObject <NSCoding>
@property(nonatomic,copy) NSString *name;
@property(nonatomic,assign) int age;
@property(nonatomic,assign) float height;
@end
~~~

自定义对象实现:

~~~objective-c
@implementation Person
-(void)encodeWithCoder:(NSCoder*)encoder {
    [encoder encodeObject:self.name forKey:@"name"];
    [encoder encodeInt:self.age forKey:@"age"];
    [encoder encodeFloat:self.height forKey:@"height"];
}
-(id)initWithCoder:(NSCoder*)decoder {
    self.name= [decoder decodeObjectForKey:@"name"];
    self.age= [decoder decodeIntForKey:@"age"];
    self.height= [decoder decodeFloatForKey:@"height"];
    return self;
}
@end
~~~

对person实例归档解档:

~~~objective-c
NSString * cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
NSString * savePath = [cachesPath stringByAppendingString:@"Person"];
/// 归档(编码)
Person *person = [[Person alloc] init];
person.name = @"JN";
person.age = 22;
person.height = 1.63f;
[NSKeyedArchiver archiveRootObject:person toFile:savePath];
/// 恢复(解码)
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:savePath];
~~~

NSData在同一文件中归档多个对象

使用archiveRootObject:toFile:方法可以将一个对象直接写入到一个文件中,但有时候可能想将多个对象写入到同一个文件中,那么就要使用NSData来进行归档对象;

NSData可以为一些数据提供临时的存储空间,以便随后写入文件,或者存放从磁盘读取的文件内容。可以使用[NSMutableData data]创建可变数据空间。

以下是NSData归档2个Person对象到同一文件中:

~~~objective-c
NSString * cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
NSString * savePath = [cachesPath stringByAppendingString:@"Persons"];

// 新建一块可变数据区
NSMutableData *data = [NSMutableData data];
// 将数据区连接到一个NSKeyedArchiver对象
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
// 开始存档对象,存档的数据都会存储到NSMutableData中
[archiver encodeObject:person1 forKey:@"person1"];
[archiver encodeObject:person2 forKey:@"person2"];
// 存档完毕(一定要调用这个方法)
[archiver finishEncoding];
// 将存档的数据写入文件
[data writeToFile:savePath atomically:YES];
~~~

NSData从同一文件中解档2个Person对象:

~~~objective-c
// 从文件中读取数据
NSData *data = [NSData dataWithContentsOfFile:savePath];
// 根据数据,解析成一个NSKeyedUnarchiver对象
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
Person *person1 = [unarchiver decodeObjectForKey:@"person1"];
Person *person2 = [unarchiver decodeObjectForKey:@"person2"];
// 恢复完毕
[unarchiver finishDecoding];
~~~

利用归档实现深复制

对一个Person对象进行深复制:

~~~objective-c
/ 临时存储person1的数据
NSData * data = [NSKeyedArchiver archivedDataWithRootObject:person1];
// 解析data,生成一个新的Person对象
Person * person2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
 // 分别打印内存地址
NSLog(@"person1:%@", person1); // person1:0x8d3ed10>
NSLog(@"person2:%@", person2); // person2:0x8d3e2f0>
~~~

宏定义

~~~objective-c
/// NSUserDefault 存取数据
#define UNSaveObject(obj,key) \
if(obj && key){ \
[[NSUserDefaults standardUserDefaults] setObject:obj forKey:key]; \
[[NSUserDefaults standardUserDefaults] synchronize]; \
}
#define UNGetObject(key)    (key ? [[NSUserDefaults standardUserDefaults] objectForKey:key] : nil)
#define UNSaveInteger(obj,key) \
if(obj && key){ \
[[NSUserDefaults standardUserDefaults] setInteger:obj forKey:key]; \
[[NSUserDefaults standardUserDefaults] synchronize]; \
}
#define UNGetInteger(key)    (key ? [[NSUserDefaults standardUserDefaults] integerForKey:key] : 0)
/// NSUserDefault 序列化存取数据
#define UNSaveSerializedObject(obj,key) \
NSData *serialized = [NSKeyedArchiver archivedDataWithRootObject:obj];\
if(serialized){ \
[[NSUserDefaults standardUserDefaults] setObject:serialized forKey:key]; \
[[NSUserDefaults standardUserDefaults] synchronize]; \
}
#define UNGetSerializedObject(key)  key ? ([[NSUserDefaults standardUserDefaults] objectForKey:key] ? [NSKeyedUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] objectForKey:key]] : nil) : nil
/// NSUserDefault 删除数据
#define UNDeletedSerializedObject(key) { [[NSUserDefaults standardUserDefaults] removeObjectForKey:key];\
[[NSUserDefaults standardUserDefaults] synchronize]; \
}
~~~

总结

不同的数据存储API由于其特性,有不同的应用场景,选择合适的方式持久化数据是比较重要的;

本文所涉及的Foundation框架中的存储API,并不适合存储大量数据或者成记录的数据,这种情况就要考虑使用数据库sqlite来做持久化。