「iOS」————持久化
iOS学习
- IOS——持久化
- 为什么持久化?
- 数据持久化方式
- **数据存储区域**
- 内存缓存
- 磁盘缓存
- 沙盒机制
- 应用沙盒目录的常见获取方式
- NSCache
- NSCache属性
- NSCache的方法
- NSCacheDelegate代理:
- 注意
- 持久化数据存储方式
- Plist(属性列表)
- Preference 偏好设置(UserDefaults)
- NSKeyedArchiver 归档解档
- 数据库存储:
- 序列化与反序列化
IOS——持久化
为什么持久化?
数据保存:
-
目的:将应用程序中的数据保存到非易失性存储中,以便在应用程序关闭或重启后仍能访问这些数据。
-
示例:保存用户输入的数据、应用设置、游戏进度等。
状态恢复:
- 目的:在应用程序重新启动时恢复到之前的状态。
- 示例:保存应用的最后状态,以便用户可以从中断的地方继续。
数据备份:
- 目的:定期备份数据,以防数据丢失。
- 示例:定期备份数据库到云端或本地存储。
数据共享:
- 目的:允许多个应用程序或设备之间共享数据。
- 示例:使用云服务同步数据,如 iCloud、Dropbox 等。
离线访问:
- 目的:即使在网络不可用的情况下也能访问数据。
- 示例:离线模式下的地图应用、阅读应用等。
历史记录:
- 目的:记录用户的操作历史,以便用户可以回顾之前的活动。
- 示例:浏览历史、购买记录等。
数据分析:
- 目的:收集用户行为数据,用于分析和改进产品。
- 示例:收集用户使用频率、停留时间等数据。
用户个性化:
- 目的:根据用户的偏好定制内容和服务。
- 示例:保存用户喜好、推荐设置等。
安全性与合规性:
- 目的:确保敏感数据的安全存储,并遵守相关法规要求。
- 示例:加密存储个人身份信息、健康数据等。
性能优化:
- 目的:通过缓存数据减少网络请求,提高应用程序的响应速度。
- 示例:缓存图片、API 数据等。
数据持久化方式
-
NSUserDefault
:简单数据快速读写。 -
Property list
: 属性列表的文件存储 -
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同步设备时不会备份该目录。
- 重新启动手机,系统磁盘空间不足时,该目录会被系统自动清空
- 存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除。
- 保存临时文件,后续不需要使用的文件。
应用沙盒目录的常见获取方式
获取沙盒的根目录:
NSString *home = NSHomeDirectory();
每次编译代码会生成新的沙盒路径,是在编译的时候,所以模拟机和真机每次的沙盒路径都是不一样的。
上面的代码得到的就是当前应用程序目录的路径,该目录下就是应用程序的沙盒,在该目录下有4个文件夹:Documents
、Library
、SystemData
、tmp
,当前应用程序只能访问该目录下的文件。
利用 NSSearchPathForDirectoriesInDomains 函数获取沙盒目录
//文件路径搜索
FOUNDATION_EXPORT NSArray<NSString *> *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);
该方法返回值为一个数组,在iphone中由于只有一个唯一路径,所以直接取数组第一个元素即可。
-
参数一 NSSearchPathDirectory directory:指定搜索的目录名称,比如这里用NSDocumentDirectory表明我们要搜索的是Documents目录。如果我们将其换成NSCachesDirectory就表示我们搜索的是Library/Caches目录。
-
参数二 NSSearchPathDomainMask domainMask:搜索主目录的位置,NSUserDomainMask表示搜索的范围限制于当前应用的沙盒目录。还可以写成NSLocalDomainMask(表示/Library)、NSNetworkDomainMask(表示/Network)等。
-
参数三 BOOL expandTilde:是否获取完整的路径,该值为YES即表示写成全写形式,为NO就表示直接写成“~”。
上述两个参数的枚举值:
typedef NS_OPTIONS(NSUInteger, NSSearchPathDomainMask) {NSUserDomainMask = 1, // 用户目录 - 基本上就用这个。 NSLocalDomainMask = 2, // 本地NSNetworkDomainMask = 4, // 网络 NSSystemDomainMask = 8, // 系统NSAllDomainsMask = 0x0ffff // 所有
};//常用的NSSearchPathDirectory枚举值
typedef NS_ENUM(NSUInteger, NSSearchPathDirectory) {NSApplicationDirectory = 1, // supported applications (Applications)NSDemoApplicationDirectory, // unsupported applications, demonstration versions (Demos)NSAdminApplicationDirectory, // system and network administration applications (Administration)NSLibraryDirectory, // various documentation, support, and configuration files, resources (Library)NSUserDirectory, // user home directories (Users)NSDocumentationDirectory, // Library 下的(Documentation)模拟器上没有创建NSDocumentDirectory, // documents (Documents)
};
获取示例:
// 搜索 Document 目录NSArray<NSString *> *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);NSString *documentDirectory = [documentPaths firstObject];NSLog(@"Document Directory: %@", documentDirectory);// 搜索 Document 目录 NONSArray<NSString *> *documentPathsNO = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, NO);NSString *documentDirectoryNO = [documentPathsNO firstObject];NSLog(@"Document Directory NO: %@", documentDirectoryNO);
NSCache
NSCache是苹果提供的一套缓存机制,用法和NSMutableDictionary类似,在AFNetworking,SDWebImage,Kingfisher中都有使用。
当内存不足时NSCache会自动释放内存。 NSCache设置缓存对象数量和占用的内存大小,当缓存超出了设置会自动释放内存。
NSCache是Key-Value数据结构,其中key是强引用,不实现NSCoping协议,作为key的对象不会被拷贝。
NSCache属性
countLimit
: 能够缓存对象的最大数量,默认值是0,没有限制。
totalCostLimit
: 设置缓存占用的内存大小 evictsObjectsWithDiscardedContent
: 是否回收废弃内容,默认YES
NSCache的方法
objectForKey
: 通过key获得缓存对象。
setObject: forKey
: 缓存对象。
setObject: forKey: cost
: 缓存对象,并指定key值对应的成本,用于计算缓存中所有对象的总成本。
removeObjectForKey
: 删除指定对象。
removeAllObjects
: 删除所有缓存对象。
NSCacheDelegate代理:
willEvictObject
: 缓存对象即将被清理时调用,一般开发者用来调试,不能在此方法中修改缓存
在以下场景中会被调用:
- removeObjectForKey
- 缓存对象超过NSCache的countLimit和otalCostLimit属性设置的限制
- App进入后台
- 系统发出内存警告
- cache这个实例的生命周期结束前
注意
- 当收到内存警告,而我们又调用removeAllObjects,则无法再继续往内存中添加数据。
- 不提供缓存总的大小,想知道NSCache占用的内存大小,只有通过添加缓存的cost自己计算。
- NSCache自动释放内存的算法是不确定的,有时是按照LRU(最近最久未使用)释放,有时随机释放。
- NSCache中的数据在APP重启后会消失,因为NSCache只是将数据保存在内存 的。
NSCache和NSMutableDictionary的区别
NSCache是线程安全的,不需要加线程锁,而NSMutableDictionary线程不安全。
我们来做个代码演示:
- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view.self.view.backgroundColor = [UIColor whiteColor];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);
}
从打印结果可以看看出:
- 设置缓存限制且知道缓存成本数时,超出是会自动回收。但是设置缓存限制但不知道缓存成本数时不会自动回收。
- 使用
removeAllObjects
回收时会调用willEvictObject
的代理方法。
持久化数据存储方式
Plist(属性列表)
属性列表是一种XML格式的文件,拓展名为plist如果对是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,就可以使用 writeToFile:atomically:
方法直接将对象写到属性列表文件中。
属性列表与NSDictionary的存储和读取过程:
我们用代码学习一下:
- (void)directorfile {// 获取 Document 目录NSArray<NSString *> *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);NSString *documentDirectory = [documentPaths firstObject];// 在 Document 目录下新建一个 test.plist 文件NSString *documentfileName = [documentDirectory stringByAppendingPathComponent:@"data.plist"];NSLog(@"%@", documentfileName);// // 存字典,将字典数据存到刚才的 test.plist 文件NSDictionary *dict = @{@"name": @"GuiCi", @"boom": @"666"};[dict writeToFile:documentfileName atomically:YES];// 取字典NSDictionary* msgDict = [NSDictionary dictionaryWithContentsOfFile:documentfileName];NSLog(@"%@", msgDict);
}
Preference 偏好设置(UserDefaults)
大部分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"];
UserDefaults设置数据时,不是立即写入,而是根据时间戳定时地把缓存中的数据写入本地磁盘。所以调用了set方法之后数据有可能还没有写入磁盘应用程序就终止了。出现以上问题,可以通过调用synchornize方法
[defaults synchornize];
强制写入。
偏好设置存储的优点:
-
不需要关心文件名,系统会自动帮你生成一个文件名。
-
快速做键值对的存储。
NSKeyedArchiver 归档解档
之前对于数组的完全深拷贝,我们知道可以使用归档进行,现在我们来真正了解归档吧:
NSKeyedArchiver(归档):归档一般都是保存自定义对象的时候,使用归档。因为plist文件不能够保存自定义对象。如果一个字典中保存有自定义对象,如果把这个对象写入到文件当中,它是不会生成 plist文件的。如果对象是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,可以直接用NSKeyedArchiver进行归档和恢复。
但是在我们使用归档之前,我们必须得遵守NSSecureCoding协议才行,老版本只需要遵循NSCoding实现其归档和解档的方法就行,但是iOS13更新之后就不行了,我们就必须的遵守NSSecureCoding协议,NSSecureCoding协议也遵循了原来NSCoding这个协议,不过我们还需要遵循它的一个supportsSecureCoding方法,这样我们才能归档成功。
也就是说我们一定要实现下面三个方法,才可以完成一次归档和解档
- (void)encodeWithCoder:(NSCoder *)coder; // 解码
- (nullable instancetype)initWithCoder:(NSCoder *)coder; // 编码
+ (BOOL)supportsSecureCoding; // 是否支持
// Person.m
#import "Person.h"@implementation Person- (instancetype)initWithName:(NSString *)name age:(NSInteger)age address:(NSString *)address {self = [super init];if (self) {_name = [name copy];_age = age;_address = [address copy];}return self;
}#pragma mark - NSSecureCoding
//一定要返回YES才可以成功归档
+ (BOOL)supportsSecureCoding {return YES;
}- (void)encodeWithCoder:(NSCoder *)coder {[coder encodeObject:_name forKey:@"name"];[coder encodeInteger:_age forKey:@"age"];[coder encodeObject:_address forKey:@"address"];
}- (instancetype)initWithCoder:(NSCoder *)coder {self = [super init];if (self) {_name = [coder decodeObjectOfClass:[NSString class] forKey:@"name"];_age = [coder decodeIntegerForKey:@"age"];_address = [coder decodeObjectOfClass:[NSString class] forKey:@"address"];}return self;
}- (NSString *)description {return [NSString stringWithFormat:@"Name: %@, Age: %ld, Address: %@", _name, (long)_age, _address];
}// 获取沙盒Documents目录路径
NSString* getDocumentsPath() {NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);return [paths firstObject];
}// 归档(保存)Person对象到指定文件路径
BOOL archivePerson(Person *person, NSString *filePath) {NSError *archiveError = nil;if (@available(iOS 11.0, macOS 10.13, *)) {NSData *data = [NSKeyedArchiver archivedDataWithRootObject:person requiringSecureCoding:YES error:&archiveError];if (archiveError) {NSLog(@"归档失败: %@", archiveError);return NO;} else {BOOL success = [data writeToFile:filePath atomically:YES];NSLog(@"归档%@", success ? @"成功" : @"失败");return success;}} else {BOOL success = [NSKeyedArchiver archiveRootObject:person toFile:filePath];NSLog(@"归档%@", success ? @"成功" : @"失败");return success;}
}// 解档(读取)Person对象从指定文件路径
Person* unarchivePerson(NSString *filePath) {NSError *unarchiveError = nil;if (@available(iOS 11.0, macOS 10.13, *)) {NSData *data = [NSData dataWithContentsOfFile:filePath];if (data) {Person *person = [NSKeyedUnarchiver unarchivedObjectOfClass:[Person class] fromData:data error:&unarchiveError];if (unarchiveError) {NSLog(@"解档失败: %@", unarchiveError);return nil;} else {NSLog(@"解档成功: %@", person);return person;}}return nil;} else {Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];NSLog(@"解档%@%@", person ? @"成功: " : @"失败: ", person);return person;}
}@end
以下简单理解:
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface Person : NSObject<NSSecureCoding>
@property (nonatomic, strong) NSString *name;
@property (assign) int age;
- (void)saveData;
- (void)readData;@end#import "Person.h"@implementation Person
- (void)encodeWithCoder:(NSCoder *)coder {[coder encodeObject:_name forKey:@"name"];[coder encodeInt:_age forKey:@"age"];
}
- (id)initWithCoder:(NSCoder *)coder {if (self = [super init]) {_name = [coder decodeObjectForKey:@"name"];_age = [coder decodeIntForKey:@"age"];}return self;
}
// 返回YES才能成功归档
+ (BOOL)supportsSecureCoding {return YES;
}- (void)saveData {Person *p = [[Person alloc] init];p.name = @"clearlove";p.age = 24;NSError *error;// 2.归档模型对象// 3.获得 Documents 的全路径NSString *docu = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];// 4.获得新文件的全路径,即新建一个 person.data 文件来存储我们要归档的数据NSString *path = [docu stringByAppendingString:@"/person.plist"];NSLog(@"%@", path);// 5.将对象封装为 Data 数据并归档NSData *data = [NSKeyedArchiver archivedDataWithRootObject:p requiringSecureCoding:YES error:&error];if (error) {NSLog(@"sodufosuf%@", error);}[data writeToFile:path atomically:YES];
}
// 读档,将数据从文件中读出
- (void)readData {NSError *error;// 1.获得 Documents 的全路径NSString *docu = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];// 2.获得文件的全路径,即获取我们要解档文件的路径NSString *path = [docu stringByAppendingString:@"/person.plist"];// 3.从 path 路径中获取 Data 数据NSData *unData = [NSData dataWithContentsOfFile:path];// 4.从文件中读取Person对象Person *person = (Person *)[NSKeyedUnarchiver unarchivedObjectOfClass:[Person class] fromData:unData error:&error];// 打印结果NSLog(@"name: %@ age: %d",person.name, person.age);
}
@end
如果父类也遵守了NSCoding协议,请注意: 应该在encodeWithCoder:
方法中加上一句[super encodeWithCode:encode];
确保继承的实例变量也能被编码,即也能被归档。应该在initWithCoder:
方法中加上一句self = [super initWithCoder:decoder];
确保继承的实例变量也能被解码,即也能被恢复。
有时候可能想将多个对象写入到同一个文件中,那么就要使用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];
数据库存储:
-
SQLite:是目前主流的嵌入式关系型数据库,其最主要的特点就是轻量级、跨平台,当前很多嵌入式操作系统都将其作为数据库首选。
-
CoreData:苹果基于sqlite封装的ORM(Object Relational Mapping)的数据库,直接对象映射.其中有三个对象
- NSManagedObject
只要定义一个类继承该类就会创建一张与之对应的表,也就是一个继承与该类的类就对应一张表,每一个通过继承该类创建出来的对象,都是该类对应的表中的一条数据。 - NSManagedObjectContext
用于操作数据库,只要有类它就能对数据库的表进行增删改查 - NSPersistentStoreCoordinator
决定存储的位置 - 注意: 该数据库多线程并不安全。最好一个线程创建一个NSManagedObjectContext
- NSManagedObject
-
FMDB:iOS端github使用最广的针对OC对sqlite的封装,支持队列操作
-
WCDB:微信技术团队开源的对sqlite操作的封装,支持对象和数据库映射,ORM数据库的一种实现,比FMDB更高效
序列化与反序列化
序列化:把对象转化为字节序列的过程
反序列化:把字节序列恢复成对象
作用:把对象写到文件或者数据库中,并且读取出来
iOS中怎么实现序列化?
在iOS中就是采用我们的一个归档的方式来实现一个序列化的.在iOS中一个自定义对象是无法直接存入到文件中的,必须先转化成二进制流才行。从对象到二进制数据的过程我们一般称为对象的序列化(Serialization),也称为归档(Archive)。同理,从二进制数据到对象的过程一般称为反序列化或者反归档(解档)。