数据持久化的原因
在iOS开发中,数据持久化是一件非常重要的事情,它允许应用程序在不同的会话之间保存用户数据、设置、偏好等信息
1. 数据保存:
- 目的:将应用程序中的数据保存到非易失性存储中,以便在应用程序关闭或者重启之后能访问这些数据
- 示例:保存用户输入的数据、应用设置、游戏进度等
2. 状态恢复:
- 目的:在应用程序重新启动的时候恢复到之前的状态
- 示例:保存应用的最后状态,以便用户可以从中断的地方继续
3. 数据备份
- 目的:定期备份数据,防止数据丢失
- 示例:定期备份数据库到云端或者本地存储
4. 数据共享
- 目的:允许多个应用程序或者设备之间共享数据
- 示例:使用云服务同步数据,例如iCloud、Dropbox等
5. 离线访问
- 目的:即时在网络不可用的情况下也能去访问数据
- 示例:离线模式下的地图应用、阅读应用等
6. 历史记录
- 目的:记录用户的操作历史,以便用户可以回顾之前的活动
- 示例:浏览历史、购买记录
7. 数据分析
- 目的:收集用户行为数据,用于分析和改进产品
- 示例:收集用户使用频率,停留时间等数据
8. 安全性和合规性
- 目的:确保敏感数据的安全存储,遵守相关的法规要求
- 示例:加密存储个人身份信息、健康数据等
9. 安全性和合规性
- 目的:确保敏感数据的安全存储,遵守相关的法规要求
- 示例:加密存储个人身份信息、健康数据等
10. 性能优化
- 目的:通过缓存数据减少网络请求,提高应用程序的响应速度
- 示例:缓存图片、API数据等
数据持久化方式
NSUserDefault
:简单数据快速读写Property
:属性列表的文件存储Archiver
:归档SQLite
:本地数据库CoreData
:CoreData是基于sqlite的封装
数据存储区域
数据存储的区域在内存和磁盘
内存缓存
对于使用频率比较高的数据,从网络或者磁盘加载数据到内存以后,使用后并不马上去销毁的话,下次使用直接从内存加载
示例:iOS中的图片加载
磁盘缓存
将从网络中加载的用户操作产生的数据写入磁盘中,用户下次查看、继续操作的时候,直接从磁盘加载使用
示例:搜索历史的缓存、用户输入的内容草稿的缓存
沙盒机制
处于安全的原因,iOS应用在安装的时候,为每个APP分配了独立的目录,App只能对自己的目录进行操作,这个目录就被称为沙盒
应用程序只能访问自身的沙盒文件,不能访问其他应用程序的沙盒文件,当应用程序需要向外部请求或接收数据的时候,都需要经过权限认证,否则的话就没有办法获取到数据。所有的非代码文件都要保存于此
例如:属性文件plist、文本文件、图像、图标、媒体资源等,他们的原理都是通过重定向技术,将程序生成和修改的文件定向到自身文件夹中。
沙盒中主要包含4个目录:MyApp.app
、Documents
、Library
、Tmp
,目录结构如下所示:
MyApp.app:包含了所有的资源文件和可执行文件,上架前经过数字签名,上架后不可以更改
Documents:文档目录,要保存程序生成的数据,会自动被分到iCloud中。保存应用运行时生成的需要持久化的数据,iTunes同步设备时会备份该目录。例如:游戏应用可将游戏存档保存在这个目录中。
Library
- 用户偏好:使用
NSUserDefault
直接读写 - 如果想要数据及时写入磁盘中,需要调用一个同步方法
- 保存临时文件,“后续需要使用“,例如:缓存图片、离线数据(地图数据)
- 系统不会清理cache目录中的文件,要求程序开发的时候,”必须提供cache目录的清理解决方案“
- Caches:存放体积大又不需要备份的数据
- Preference:保存应用的所有偏好设置,iCloud会备份设置信息
- 用户偏好:使用
Tmp
临时文件,系统会自动清理。重新启动就会清理。保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录中删除。应用没有运行的时候,系统也可能会清除该目录下的文件。iTunes同步设备时不会备份该目录
- 存放临时文件,不会被备份,并且这个文件中的数据有可能随时被清除的可能
- 保存临时文件,“后续不需要使用”
- tmp目录中的文件,系统会自动清理
- 重新启动手机,tmp目录会被清理
- 系统磁盘空间不足的时候,系统也会自动清理
应用沙盒目录的常见获取方式
- 获取沙盒根目录的方法:
NSString* home = NSHomeDirectory()
Documents:
- 利用沙盒根目录拼接
Documents
字符串(这里不建议使用该方法,在新版本中的操作系统可能会更改目录名称)
NSString *home = NSHomeDirectory(); NSString *documents = [home stringByAppendingPathComponent:@"Documents"];
- 利用NSSearchPathForDirectoriesinDomains函数
// NSUserDomainMask 代表从用户文件夹下找 // YES 代表展开路径中的波浪字符“~” NSArray *array = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, NO); // 在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素 NSString *documents = [array objectAtIndex:0];
- 利用沙盒根目录拼接
tmp:
NSString* tmp = NSTemporaryDirectory();
Library/Cache(方法和Documents类似的两种方法)
- 利用沙盒根目录拼接”Caches”字符串
- 利用 NSSearchPathForDirectoriesInDomains 函数 (将函数的第2个参数改为:NSCachesDirectory即可)
Library/Preference:通过NSUserDefaults类存取该目录下的设置信息。
NSCache
这是苹果提供的一套缓存机制,用法和NSMutableDictionary类似,在AFNetWorking
、SDWebImage
、Kingfisher
中都有使用过
当内存不足的时候NSCache
会自动释放内存。NSCache
设置缓存对象数量和占用的内存大小,当内存超出了设置会自动释放内存。NSCache
是Key-Value
数据结构,其中key
是强引用,不实现NSCopying
协议,作为key的对象不会被拷贝
NSCache的属性
countLimit
:能够缓存对象的最大数量,默认值是0,没有限制
totalCostLimit
:设置缓存占用的内存大小evictsObjectsWithDiscardedContent
:是否回收废弃内容,默认YES
NSCache的方法
objectForKey
: 通过key获得缓存对象。setObject: forKey
: 缓存对象。setObject: forKey: cost
: 缓存对象,并指定key值对应的成本,用于计算缓存中所有对象的总成本。removeObjectForKey
: 删除指定对象。removeAllObjects
: 删除所有缓存对象。
NSCacheDelegate代理
willEvictObject
:缓存对象即将被清理的时候调用,一般开发者用来调试,不能在这个方法中修改缓存
在下面的场景中会被调用:
removeObjectForKey
- 缓存对象超过
NSCache
的countLimit
以及totalCostLimit
属性设置的限制 - App进入后台
- 系统发出内存警告
cache
这个实例的生命周期结束前
NSCache使用注意事项
- 当我们收到内存警告,我们调用
removeAllObjects
,则无法再继续往缓存中添加数据 - 不提供缓存总的大小,想知道
NSCache
占用的内存大小,只有通过添加缓存的cost自己计算 NSCache
自动释放内存的算法是不确定的,有时是按照LRU(最近最久未使用)释放,有时随机释放NSCache
中的数据在APP重启后会消失,这是由于NSCache
只是将数据保存在了内存之中
NSCache和NSMutableDictionary的区别:
NSCache
是线程安全的,不需要加线程锁,而NSMutableDictionary是线程不安全的
这里给一个使用的示例:
@interface ViewController () <NSCacheDelegate>
@property (nonatomic, strong) NSCache* myCache;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
self.myCache = [[NSCache alloc] init];
self.myCache.delegate = self;
for (int i = 0; i<10; i++) {
[self.myCache setObject:[NSString stringWithFormat:@"%d", i] forKey:@(i) cost: 1];
}
for (int i = 0; i<10; i++) {
NSLog(@"NSCache取出---%@", [self.myCache objectForKey:@(i)]);
}
/// 清除缓存
[self.myCache removeAllObjects];
/// 设置缓存限制
self.myCache.totalCostLimit = 5;
NSLog(@"设置缓存限制后=================");
for (int i = 0; i<10; i++) {
// 设置成本数为1
[self.myCache setObject:[NSString stringWithFormat:@"%d", i] forKey:@(i) cost: 1];
}
for (int i = 0; i<10; i++) {
NSLog(@"NSCache取出---%@", [self.myCache objectForKey:@(i)]);
}
/// 清除缓存
[self.myCache removeAllObjects];
NSLog(@"设置缓存限制后但未设置成本数cost=================");
for (int i = 0; i<10; i++) {
[self.myCache setObject:[NSString stringWithFormat:@"%d", i] forKey:@(i)];
}
for (int i = 0; i<10; i++) {
NSLog(@"NSCache取出---%@", [self.myCache objectForKey:@(i)]);
}
/// 清除缓存
[self.myCache removeAllObjects];
}
// 即将回收对象的时候进行调用,实现代理方法之前要遵守NSCacheDelegate协议。
- (void)cache:(NSCache *)cache willEvictObject:(id)obj{
NSLog(@"NSCache回收---%@", obj);
}
通过打印结果其实很明显的可以看出一些东西:
- 设置缓存限制并且知道缓存成本数的时候,超出是会自动回收的。但是设置缓存限制但不知道缓存成本数的时候不会自动回收
- 回收的时候会调用
willEvictObject
持久化存储方法
- Plist(NSArray/NSDictionary)仅仅存储数组、字典,但是数组和字典中不能有自定义对象
- 偏好设置(Preference/NSUserDefauls)同上并不能存储自定义对象
- 归档
NSCoding
(NSKeyedArchiver
/NSkeyedUnarchiver
)存储自定义对象(一次性读取和存储操作) - SQLite3:
- 操作数据比较快
- 可以局部读取
- 比较小型,占用的内存资源比较少
- Core Data
Plist
属性列表是一种XML格式的文件,拓展名为plist,如果是NSString、NSDiictionary、NSArray、NSData、NSNumber等类型,这里可以直接使用writeToFile:atomically
方法直接将对象写到属性列表文件中
属性列表-归档:NSDictionary
将一个NSDictionary
对象归档到一个plist属性列表中
NSMutableDictionary* dict = [NSMutableDictionary dictionary];
[dict setObject:@"张三" forKey:@"name"];
[dict setObject:@"155xxxxxxx" forKey:@"phone"];
[dict setObject:@"27" forKey:@"age"];
// 将字典持久化到Documents/stu.plist文件中
[dict writeToFile:path atomically:YES];
属性列表-恢复NSDictionary
读取属性列表,恢复NSDictionary
对象
// 读取Documents/stu.plist的内容,实例化
NSDictionaryNSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
NSLog(@"name:%@", [dict objectForKey:@"name"]);
NSLog(@"phone:%@", [dict objectForKey:@"phone"]);
NSLog(@"age:%@", [dict objectForKey:@"age"]);
这里使用一张表来理清过程
偏好设置
很多的iOS应用都支持偏好设置,例如保存用户名、密码、字体大小等设置,iOS提供了一套标准的解决方案来为应用加入偏好设置功能
每个应用都有个NSUserDefaults
实例,通过其来存取偏好设置
比如:保存用户名、字体大小、是否自动登录等:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"张三" forKey:@"username"];
[defaults setFloat:18.0f forKey:@"text_size"];
[defaults setBool:YES forKey:@"auto_login"];
读取上次保存的设置:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *username = [defaults stringForKey:@"username"];
float textSize = [defaults floatForKey:@"text_size"];
BOOL autoLogin = [defaults boolForKey:@"auto_login"];
注意:UserDeflauts
设置数据时,并不是立即写入,而是根据时间戳定时的将缓存中的数据写入本地磁盘中,所以调用了set方法之后数据可能还没有写入磁盘应用程序就终止掉了。出现以上问题,可以通过调用[defaults synchornize]
强制写入
归解档
NSKeyedArchiver
:如果对象是NSString、NSDictionary、NSArray、NSData、NSNumber等模型,可以直接使用 NSkeyedArchiver
进行归档和恢复。不是所有的对象都可以直接使用这种方式进行归档,只有遵守了NSCoding
协议的对象才可以。
NSCoding协议中的两个方法:
encodeWithCoder
:每次归档对象的时候,都会调用这个方法。一般在这个方法中指定如何归档对象中的每个实例变量,可以使用encodeObject:forKey:
方法进行归档实例设置initWithCoder
:每次从文件中恢复对象的时候都会调用这个方法,我们一般在这个方法中指定如何解码文件中的数据为对象的实例变量,可以使用decodeObjcet:forKey:
方法解码实例变量
归档一个NSArray对象到Documents/array.archive
NSArray *array = [NSArray arrayWithObjects:@”a”,@”b”,nil];
[NSKeyedArchiver archiveRootObject:array toFile:path];
NSKeyedArchiver——归档Person对象
@interface Person : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) float height;
@end
@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 *person = [[Person alloc] init];
person.name = @"xxx";
person.age = 27;
person.height = 1.83f;
[NSKeyedArchiver archiveRootObject:person toFile:path];
// 恢复(解码)
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
如果父类同时遵守了协议,那我们应该在encodeWithCoder
方法中加上[super encodeWithCode:encode]
确保继承的实例变量也能被编码,即也能被归档。应该在initWithCoder:
方法中加上self = [super initWithCoder:decoder]
确保继承的实例变量也能被解码,即也能被恢复
NSData —— 归档
在使用archiveRootObject:toFile:
方法可以将一个对象直接写入到一个文件中,有时候可能想讲多个对象写入到同一个文件中去,那么这个时候就需要使用NSData
进行归档对象。NSData可以为一些数据提供临时存储空间,以便随后写入文件,或者存放在磁盘读取的文件内容中,可以使用[NSMutableData data]
创建可变数据空间
// 新建一块可变数据区
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:path atomically:YES];
//解码
// 从文件中读取数据
NSData *data = [NSData dataWithContentsOfFile:path];
// 根据数据,解析成一个NSKeyedUnarchiver对象
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
Person *person1 = [unarchiver decodeObjectForKey:@"person1"];
Person *person2 = [unarchiver decodeObjectForKey:@"person2"];
// 恢复完毕
[unarchiver finishDecoding]
利用归档实现深复制,例如对一个Person对象进行深复制
// 临时存储person1的数据
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:person1];
// 解析data,生成一个新的Person对象
Student *person2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
// 分别打印内存地址
NSLog(@"person1:0x%x", person1); // person1:0x7177a60
NSLog(@"person2:0x%x", person2); // person2:0x7177cf0
数据库
Core Data
Core Data中的三个对象
NSManagedObject
只要定义一个类继承该类就会创建一张与之对应的表,也就是一个继承与该类的类就对应一张表,每一个通过继承该类创建出来的对象,都是该类对象的表的一条数据
NSManagedObjectContext
用于操作数据库,只要有类其就能对数据库的表进行增删改查
NSPersistenStoreCoordinator
决定存储的位置
Core Data多线程不安全
这本身并不是一个并发安全的架构,所以在多线程中实现Core Data
会有问题
- 原因:CoreData中的
NSManagedObjectContext
在多线程中不安全 - 如果想要多线程访问
CoreData
,最好的方法是一个线程一个NSManagedObjectContext
- 每个
NSManagedObjectContext
都可以使用同一个NSPersistentStoreCoordinator
实例,由于NSmanagedObjectContext
会在使用NSPersistentStoreCoordinator
前上锁
SQLite
这是一种轻量级的关系行数据库,适用于需要进行复杂数据查询和关系管理的场景,其具有更好的数据查询能力,可以通过SQL语句对数据进行灵活的筛选和排序。但是,SQLite的使用相对复杂。
序列化和反序列化
序列化:将对象转化为字节序列的过程
反序列化:将字节序列恢复成对象
作用:把对象写到文件中或者数据库中,并且读取出来
在iOS中怎么实现序列化?
在iOS实际开发中,我们也会使用到序列化,归档就是我们在iOS开发中使用序列化的场景
在iOS中一个自定义对象是无法直接存入到文件中的,必须先转化成二进制流才行。从对象到二进制数据的过程我们一般称为对象的序列化(Serialization),也称为归档(Archive)。同理,从二进制数据到对象的过程一般称为反序列化或者反归档(解档)。