iOS数据持久化属性列表及对象归档的封装

文章目录
  1. 使用及注意
    1. NSUserDefaults
      1. 存取自定义类型
    2. NSKeyedArchiver
  2. 封装
    1. 任意类归档原理
    2. 封装实现
      1. 沙盒
      2. 文件操作

在iOS上的数据持久化的解决方案总的来说有四种。分别是NSUserDefaults属性列表、NSKeyedArchiver对象归档、SQLite3数据库以及苹果官方提供的基于SQLite3进行二次封装的Core Data。

对于以上四种方案的优劣以及用途比较,之前已经看过很多文章有所讲解,其中有一篇文章对于这四种方案的使用讲解的非常透彻清晰,如果有需要的朋友可以移步​iOS中几种数据持久化方案:我要永远地记住你!

今天我们来讨论一下关于NSUserDefaults属性列表与NSKeyedArchiver对象归档两种方式的使用以及封装。

使用及注意

NSUserDefaults属性列表和NSKeyedArchiver对象归档是iOS中两种非常常用的数据持久化方案。这两者均由系统提供,所以用起来非常简单方便。下面分别就两者的使用和注意事项作简单介绍。

NSUserDefaults

NSUserDefaults是一个极好的方式,用于存储轻量级数据,比如用户习惯、用户清单等等。这个类存在一个单例模式的实例对象standardUserDefaults,我们可以利用这个实例对象来保存程序启动默认设置等。

NSUserDefaults所支持的数据类型较为传统、典型,包括NSStringNSDateNSURLNSArrayNSDictionaryBOOL以及NSIntegerfloatdouble

在通常情况下,我们使用+ (NSUserDefaults *)standardUserDefaults;来获取一个单例模式的NSUserDefaults。当然苹果也提供了另外一个方法- (nullable instancetype)initWithSuiteName:(nullable NSString *)suitename;,使我们能够开辟一个不同于standardUserDefaults的实例。这个方法在iOS 7之前的前身是- (nullable id)initWithUser:(NSString *)username;

由此可见,我们每次使用单例模式的userDefaults,都可以比较简单的完成。

特别注意:

  • NSUserDefaults无法处理可变类型数据,如NSMutableStringNSMutableArray等,想要存储可变对象,可利用不可变数据类型实例对象做桥接。
  • 归档数据所需的key必须是唯一的,多次对同一个key设置对象会导致之前设置的对象被覆盖掉。

存取自定义类型

对于非前文所述数据类型,我们同样可以利用NSUserDefaults处理它们,这得益于NSUserDefaults提供了针对任意对象处理的方法:

1
2
3
- (nullable id)objectForKey:(NSString *)defaultName;
- (void)setObject:(nullable id)value forKey:(NSString *)defaultName;
- (void)removeObjectForKey:(NSString *)defaultName;

假设我们定义如下的数据模型:

1
2
3
4
5
Dog *snoopy = [[Dog alloc] init];
snoopy.name = @"Snoopy";
snoopy.sex = 1;
snoopy.age = 8;
snoopy.skinColor = @"white";

显然想要吧snoopy归档,我们肯定不能直接在setObject: forKey:方法中直接对snoopy对象进行操作,直接操作的后果就是crash。但我们可以利用NSKeyedArchiverNSData对象进行桥接。

Dog类实例对象转化为NSData,然后将其归档至NSUserDefaults

1
2
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:snoopy];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"mydog"];

读取对象的时候,过程是是一样的,也是先取出NSData类对象,然后再转化为Dog类。

1
2
NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:@"mydog"];
Dog *snoopy = [NSKeyedUnarchiver unarchiveObjectWithData:data];

NSKeyedArchiver

刚刚前文介绍的,通过把自定义数据类模型转化为NSData再归档至NSUserDefaults的方法,其实已经用到了NSKeyedArchiver的编解码方法。我们能对NSData对象这样归档的原因正是NSData类已经实现了NSCoding协议。所有实现了NSCoding协议的类,都可以直接通过NSKeyedArchiver进行归档。

而具体的实现方式,其实是和NSUserDefaults没有太大区别的,在此不表。

封装

任意类归档原理

刚刚提到了,对于任意实现了NSCoding协议的类,都可以直接归档。那么这个协议如何实现?

1
2
3
4
5
6
@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
@end

显然,一个作为编码一个作为解码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)dencoder:(NSCoder *)decoder {
unsigned count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i =0; i<count; i++) {
Ivar var = ivars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithFormat:@"%s",name];
id value = [decoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
}
-(void)encoder:(NSCoder*)encoder {
unsigned count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i =0; i<count; i++) {
Ivar var = ivars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithFormat:@"%s",name];
id value = [self valueForKeyPath:key];
[encoder encodeObject:value forKey:key];
}
}

实现以上编解码方法,并将其作为NSObject的扩展,即可完成对任意对象归档的准备工作(NSObject作为一切类的根类)。

封装实现

沙盒

沙盒是iOS中用于存储应用数据的位置,每个应用都存在独立的沙盒,所以不会互相影响,更不会影响系统文件。

获取沙盒路径:

1
NSString *documentsPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];

文件操作

归档文件、读取文件、删除单个文件:

1
2
3
[NSKeyedArchiver archiveRootObject:object toFile:path];
[NSKeyedUnarchiver unarchiveObjectWithFile:path];
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];

path是沙盒路径后接文件名的字符串,比如我这里的demo是”/var/mobile/Containers/Data/Application/4CBD28A1-126C-4BC0-A5CD-FAC43B413271/Documents/fileName.plist“

开个脑洞,若想设备沙盒中所有文件怎么办?

同样的,获取沙盒路径,之后通过方法- (nullable NSArray<NSString *> *)contentsOfDirectoryAtPath:(NSString *)path error:(NSError **)error获得来自该路径下的所有文件数组,再对其进行遍历,对文件进行筛选,对需要删除的文件调用remove方法即可。完整的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+ (void)removeAllFiles {
NSString *documentsPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
NSError *error;
NSArray<NSString *> *dir = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:documentsPath error:&error];
if (!dir || error) {
NSLog(@"filePathError:%@", error.description);
return;
}
for (NSString *fileName in dir) {
NSString *filePath = [NSString stringWithFormat:@"%@/%@", documentsPath, fileName];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
NSLog(@"Delete File:%@", filePath);
}
}