属性列表、 NSUserDefault、 归档archive、 数据库
属性列表
属性列表是一种明文的轻量级存储方式,其存储格式有多种,最常规格式为XML格式。在我们创建一个新的项目的时候,Xcode会自动生成一个info.plist文件用来存储项目的部分系统设置。plist只能用数组(NSArray)或者字典(NSDictionary)进行读取,由于属性列表本身不加密,所以安全性几乎可以说为零。因为,属性列表正常用于存储少量的并且不重要的数据。
在程序启动后,系统会自动创建一个NSUserDefaults的单例对象,我们可以获取这个单例来存储少量的数据,它会将输出存储在.plist格式的文件中。其优点是像字典一样的赋值方式方便简单,但缺点是无法存储自定义的数据。
数据归档/序列化
与属性列表相反,同样作为轻量级存储的持久化方案,数据归档是进行加密处理的,数据在经过归档处理会转换成二进制数据,所以安全性要远远高于属性列表。另外使用归档方式,我们可以将复杂的对象写入文件中,并且不管添加多少对象,将对象写入磁盘的方式都是一样的。
使用NSKeyedArchiver对自定义的数据进行序列化,并且保存在沙盒目录下。使用这种归档的前提是让存储的数据模型遵守NSCoding协议并且实现其两个协议方法。(当然,如果为了更加安全的存储,也可以遵守NSSecureCoding协议,这是iOS6之后新增的特性)
数据库
sqlite是一个轻量级、跨平台的小型数据库,其拥有可移植性高、有着和MySql几乎相同的数据库语句以及无需服务器即可使用的优点:
一、可以存储大量的数据,存储和检索的速度非常快;二、能对数据进行大量的聚合,这样比起使用对象来进行这些操作要快。
当然,它也具有明显的缺点:
一、它没有提供数据库的创建方式;
二、它基于C语言框架设计,没有面向对象的API,所以使用起来比较麻烦;
三、复杂的数据模型的数据建表相对而言比较麻烦。
当然,我们也可以使用基于sqlite封装的开源数据库FMDB来减少使用sqlite的工作量
coreData
coreData是苹果官方iOS5之后推出的综合型数据库,其使用了ORM(Object Relational Mapping)对象关系映射技术,将对象转换成数据,存储在本地数据库中。coreData为了提高效率,甚至将数据存储在不同的数据库中,且在使用的时候将本地数据放到内存中使得访问速度更快。我们可以选择coreData的数据存储方式,包括sqlite、xml等格式。但也正是coreData 是完全面向对象的,其在执行效率上比不上原生的数据库。除此之外,coreData拥有数据验证、undo等其他功能,在功能上是四种持久化方案最多的。
上面已经分别介绍了四种方案的优缺点,在开发中,并没有说哪种持久化方案是最好的,只能说在不同开发场景下,最适合使用的持久化方案。下面我们将用代码实战的方式对这些持久方案进行更加详细的了解
属性列表
在我们每次创建新的项目的时候,Xcode帮助我们生成了Info.plist文件,里面存储了关于项目名字、版本、bundle id等等关键信息,这个plist文件也是逆向工程(越狱)中获取app数据的重要文件。OK,那么什么情况下用plist存储呢?打个比方,最近在实现公司项目业务的时候,需要使用选择器(UIPickerView)给用户选择所在城市。对于城市数据,并没有加密的必要,而且这时候使用plist会达到更高一些的效率。既然已经知道需要的数据,那么很容易就得得出省-市这样的一对多的数据类型,我们的plist使用字典,将省份作为key,存储对应的城市的数组作为value
1、创建plist文件。New Files -> iOS -> Resource -> Property List -> Next
2、给这个plist文件命名cities,点击Create后创建好。然后我们选中Root,默认已经是字典的数据结构存储了,我们点击Root右边的加号添加一些键值对,然后修改左边的key的文字,并且将每一个key对应的value设置为Array数组
3、按照在字典中添加键值对的方式,在设置好每个key对应的类型之后,移动到每一行上面点击出现的+给每一个省份添加数组元素,并且赋值,最终效果图如下
当然,像这种城市的plist文件百度一下就可以找到,但是创建plist的方式相信大家看完之后也就明白了。从plist读取数据的方式也很简单,苹果把读取的方法封装在NSArray跟NSDictionary中,读取步骤分为两步:
1、获取plist文件路径:
NSString * filePath = [[NSBundle mainBundle] pathForResource: @"cities" ofType: @"plist"];
2、通过数组或者字典的构造器方法创建容器:
NSDictionary * dict = [NSDictionary dictionaryWithContentsOfFile: filePath];
//NSArray * array = [NSArray arrayWithContentsOfFile: filePath];
实现选择器的大概思路是用两个数组分别存储省份以及当前选中省份的城市数组,然后在滑动pickerView的回调事件中根据选中的省份更新城市数据源。
NSUserDefault
其支持的数据格式有:NSNumber(Integer、Float、Double等)、NSString、NSDate、NSArray(成员必须也是支持的格式类型)、NSDictionary(同NSArray)。其使用和读取也是非常的简单,像字典一样的存取方式
读取:NSString * str = [[NSUserDefaults standardUserDefaults] valueForKey: @"str"];
存储:[[NSUserDefaults standardUserDefaults] setValue: @"str" forKey: @"str"];
删除:[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"str"];
同样的也可以使用setObject:forKey:或者objectForKey:等字典存取方法
数据归档/数据序列化
更多时候,NSUserDefaults已经提供了存储简单变量的持久化方案。然而,当我们想要存储复杂的自定义数据时,NSUserDefaults无法为我们提供更多的帮助,这时候考虑的是另外的持久化方案。而并非所有的程序都需要查询数据、数据迁移这些操作,而且也并非所有的数据都有复杂的关系图。这时候,数据归档绝对是不二的选择。
我们使用archive格式的文件将归档化的数据存储在沙盒目录下,这种格式的文件读取出来是二进制数据(NSData),然后使用NSKeyedUnarchiver类对数据进行反序列化。假设当前需要进行持久化存储的是一款离线游戏,我需要存储游戏前十名的成绩、成绩持有者和记录创建时间这些数据,那么相对应的LXDGameRecord类声明如下,其必须遵循NSCoding或NSSecureCoding协议之一
@interface Model : NSObject
@property (nonatomic, strong) NSString *title;
@end
@implementation Model
- (instancetype)init{
self = [super init];
if (self) {
self.title = @"sssss";
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder{
self = [super init];
if (self) {
self.title = [coder decodeObjectForKey:@"title"];
}
return self;
}
- (void)encodeWithCoder:(nonnull NSCoder *)coder {
[coder encodeObject:self.title forKey:@"title"];
}
@end
1.NSKeyedArchiver归档
archiveRootObject:toFile:在iOS13中已弃用,替换为archivedDataWithRootObject:requiringSecureCoding:error:,可以归档任何类型。
Model *model = [[Model alloc] init];NSData *data = [NSKeyedArchiver archivedDataWithRootObject:model requiringSecureCoding:false error:nil];
2.NSKeyedUnarchiver解档
unarchiveObjectWithFile:同样在iOS13中被弃用,替换为unarchivedObjectOfClass:fromData:error:
Model *model0 = [NSKeyedUnarchiver unarchivedObjectOfClass:[Model class] fromData:data error:nil];
报错:
Error Domain=NSCocoaErrorDomain Code=4864 "This decoder will only decode classes that adopt NSSecureCoding. Class 'Model' does not adopt it." UserInfo={NSDebugDescription=This decoder will only decode classes that adopt NSSecureCoding. Class 'Model' does not adopt it.}
分析错误注意到另一个协议NSSecureCoding
@interface Model : NSObject @property (nonatomic, strong) NSString *title;@end
+ (BOOL)supportsSecureCoding { return YES;}
此处必须返回YES,如果返回NO同样会报错。
Error Domain=NSCocoaErrorDomain Code=4864 "Class 'Model' disallows secure coding. It must return YES from supportsSecureCoding." UserInfo={NSDebugDescription=Class 'Model' disallows secure coding. It must return YES from supportsSecureCoding.}
解档的大坑
如果其中存在NSArray等集合,那么解档同样会失败,不知原因。
@interface Model : NSObject @property (nonatomic, strong) NSArray *title;@end
报错:
Error Domain=NSCocoaErrorDomain Code=4864 "value for key 'title' was of unexpected class 'NSArray'. Allowed classes are '{( Model )}'." UserInfo={NSDebugDescription=value for key 'title' was of unexpected class 'NSArray'. Allowed classes are '{( Model )}'.}
@interface Model : NSObject @property (nonatomic, strong) NSDictionary *title;@end
报错:
Error Domain=NSCocoaErrorDomain Code=4864 "value for key 'title' was of unexpected class 'NSDictionary'. Allowed classes are '{( Model )}'." UserInfo={NSDebugDescription=value for key 'title' was of unexpected class 'NSDictionary'. Allowed classes are '{( Model )}'.}
解决方案
建议转化为JOSN字符串,再进行归档解档
使用其他本地化方案