【iOS】数据存储与持久化
前言
项目总结时,涉及了很多数据存储与持久化,这里做一个对比总结。
数据存储
数据存储的区域在内存和磁盘。
- 内存缓存:把经常使用的数据暂存在内存中,避免重复从磁盘或网络读取。
例如:SDWebImage图片内存缓存。 - 磁盘缓存:将从网络加载的、用户操作产生的数据写入到磁盘,用户下次查看或操作时,直接从磁盘加载使用。
例如:搜索历史、离线资源、用户本地配置等。
二者的区别在于:
- 内存是指当前程序的运行空间,缓存速度快、容量小,是临时存储文件用的,供CPU直接读取。
- 磁盘是程序的存储空间,缓存速度慢、容量大、可持久化,磁盘缓存是存在Library/Caches。
打开运行一个程序,它是存储在内存中,关闭后内存就又回到原来的空闲空间。而与内存不同的是,磁盘是永久存储东西的,只要里面存放东西,不管运行不运行,它都占用空间。
NSCache
NSCache是苹果官方提供的专门用于内存缓存的工具类,以键值对形式存储。其中key为强引用,不实现NSCopying协议,因此作为key的对象不会被拷贝。NSCache设置缓存对象数量和占有内存大小,当缓存超出时自动清理释放内存。NSCache是线程安全的。
objc
@interface NSCache <KeyType, ObjectType> : NSObject
- 属性
objc
@property NSUInteger totalCostLimit; // limits are imprecise/not strict // 设置缓存占用的内存大小能够缓存对象的最大数量,默认值是0,没有限制
@property NSUInteger countLimit; // limits are imprecise/not strict // 能够缓存对象的最大数量,默认值是0,没有限制
@property BOOL evictsObjectsWithDiscardedContent; // 是否回收废弃内容,默认YES
- 方法
objc
- (nullable ObjectType)objectForKey:(KeyType)key; // 通过key获得缓存对象
// 缓存对象
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g; // 指定key值对应的成本,用于计算缓存中所有对象的总成本
- (void)removeObjectForKey:(KeyType)key; // 删除指定对象
// 删除所有缓存对象
- (void)removeAllObjects;
- 代理
调用该方法的情况有:
- 超过限制自动淘汰
- removeObjectForKey
- removeAllObjects
objc
@protocol NSCacheDelegate <NSObject>
@optional
// 监听对象即将从缓存中被移除的事件
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end
使用时需注意:
- 收到内存警告又调用removeAllObjects时,会清空当前缓存。此时禁止立刻继续写入大量数据,避免内存刚刚释放,又重复占有。
- NSCache不提供缓存的总大小,需要自己通过cost计算。
- NSCache的自动释放算法不确定,有时使用LRU,有时随机释放。
- NSCache中的数据在App重启之后会消失。
具体示例看一下:
objc
- (void)test {
// 没有设置限制
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++) {
[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];
// 设置缓存限制后但未设置成本数cost
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];
}
- (void)cache:(NSCache *)cache willEvictObject:(id)obj {
NSLog(@"NSCache回收---%@", obj);
}


这个结果这里注意一下:
回收是发生在添加阶段,取空是发生在读取阶段。取空的是刚刚被回收的5个对象,然后剩下的没有超过限制,正常添加,removeAllObjects再回收。

数据持久化
数据持久化就是将应用运行在内存中的临时数据存储到硬盘等非易失性存储介质中,使得在应用程序关闭、重启后可以继续访问之前保存的数据。
常用的数据持久化方式有以下几种:
- plist 属性列表
- NSUserDefaults 偏好设置
- NSKeyedArchiver 归档解档(自定义对象存储)
- SQLite 原生数据库:目前主流的嵌入式关系型数据库,轻量级、跨平台。
- CoreData:苹果基于SQLite封装的ORM数据库,直接对象映射。
- FMDB、WCDB(第三方数据库封装):
- FMDB:iOS端使用最广的针对OC封装SQLite,支持队列操作。
- WCDB:微信技术团队开源的对SQLite操作的封装,支持对象和数据库映射,比FMDB更高效。
- KeyChain 钥匙串(加密持久化存储)
- File Storage 文件存储
数据持久化的意义在于以下几个方面:
- 数据保存:将应用程序中的数据保存到非易失性存储中,以便在应用程序关闭或重启后仍可以访问这些数据。例如:保存用户输入的数据、应用设置、游戏进度等。
- 状态恢复:在应用程序重新启动的时候恢复到之前的一个状态。例如:保存应用的最后状态,以便用户可以从中断的地方继续。
- 数据备份:定期备份数据,防止数据丢失。定期备份数据库到云端或本地存储。
- 数据共享:允许多个应用程序或者设备之间共享数据。(使用云服务同步数据)
- 离线访问:即使在网络不可用的情况下也能访问数据。例如:离线模式下的地图应用、阅读应用等。
- 历史记录:记录用户的操作记录,方便用户回顾之前的活动。例如:流览历史、购买记录等。
- 数据分析:收集用户行为数据(如用户使用频率、停留时间等数据),用于分析和改进产品。
- 用户个性化:根据用户的偏好设置来定制内容和服务。
- 性能优化:通过缓存数据减少网络请求,提高应用程序的响应速度。例如:缓存图片、API的数据等。
- 安全性与合规性:确保敏感数据的安全存储,并遵守相关法规要求。
iOS沙盒机制
iOS系统出于安全机制,所有数据持久化操作均依托App专属沙盒完成,是数据持久化的底层基础。App安装时,系统为每个应用分配独立、专属的沙盒目录,所有App仅能访问自身沙盒目录下的文件,无法跨应用读取数据。当应用程序需要像外部请求或者接收数据时,都需要经过权限验证,否则无法获取到数据。
所有的非代码文件(如属性文件plist、文本文件、图像、图标、媒体资源等)都保存在沙盒中。
其原理是通过重定向技术,把程序生成和修改的文件定向到自身文件夹中。
注意:开发调试阶段,编译可能使得Xcode重新安装App,进而导致每次生成新的沙盒路径;而线上正式版的App升级更新应用资源,不删除应用数据,不会生成新的沙盒路径。
Q:iOS沙盒什么时候创建?
A:App沙盒是在App安装到设备时由系统创建的,每个App拥有的唯一数据目录。
沙盒核心目录

沙盒根目录下主要包含以下几个核心文件夹:
- MyApp.app:应用资源目录。存放所有代码、静态资源和配置文件。上架前需要经过数字签名,上架后无法修改。
- Documents:核心持久化文档目录。存放用户生成、需要永久保存的数据,会自动同步到iCloud中。禁止存放网络下载资源,否则无法上架。
- Library:系统配置与缓存的总目录。包含多个子文件夹:
- Caches:缓存目录。存放体积大且无需备份的临时缓存文件,系统不会主动清理,需开发者手动实现清理逻辑。磁盘爆满时系统可能会自动清空。
- Preference:偏好设置目录。存储NSUserDefaults配置数据,iCloud会备份设置信息。
- SplashBoard:启动屏缓存目录。存储启动屏缓存,缓存文件格式为.ktx,本质为图片。启动屏异常可通过清理此目录缓存修复。
- Application Support:隐藏资源目录。用来存储应用程序运行但对用户隐藏的文件,如游戏的新关卡。
- Cookies:系统自动将App中网络请求的cookie数据存储为文件。
- Tmp:临时文件目录。存放App运行过程中的临时数据,使用完毕系统会在重新启动时自动清理,从该目录删除。存放的临时文件不会被备份,且有随时被清除的可能。
- SystemData:系统私有目录。无对外操作接口,开发者无需干预。
Q:为什么App沙盒路径改变,但Documents数据还在?
A:因为iOS将App Bundle Container(包容器)和Data Container(数据容器)分离管理。App Bundle存放程序资源,更新App时整个Bundle都会被系统替换,旧资源全部清除,因此Bundle路径可能会发生变化;而Data Container存放用户数据,系统通过Bundle Identifier关联应用数据,升级时会保留Data Container,因此Documents中的数据仍然存在。只有在删除App时,Data Container才会被删除。
沙盒路径获取
objc
// 获取沙盒根目录
NSString *homePath = NSHomeDirectory();
// 获取Documents目录
NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
// 获取Library目录
NSString *libPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
// 获取Caches缓存目录
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
// 获取Tmp临时目录
NSString *tmpPath = NSTemporaryDirectory();
其中访问目录的核心方法:
objc
FOUNDATION_EXPORT NSArray<NSString *> *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);
- directory:指定搜索的目录名称。
- domainMask:搜索主目录的位置。NSUserDomainMask表示搜索的范围仅限于当前应用的沙盒目录。有以下枚举值:
- NSUserDomainMask:当前用户目录
- NSLocalDomainMask:本机范围
- NSNetworkDomainMask:网络范围
- NSSystemDomainMask:系统范围
- NSAllDomainsMask:所有范围
- expandTilde:是否获取完整的路径。YES返回完整路径,NO返回
~简写路径。

数据持久化存储方式
这里具体对比一下几个数据持久化的存储方式。
plist属性列表
plist是xml格式的属性文件,仅支持OC基础数据类型:NSString、NSDictionary、NSArray、NSData、NSNumber,无法存储自定义对象。
OC基础对象可以直接通过writeToFile: atomically:方法直接将对象写入到属性列表文件中。
objc
// 获取Documents路径,拼接plist文件
NSString* docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString* filePath = [docPath stringByAppendingPathComponent:@"test.plist"];
NSLog(@"%@", filePath);
// 字典写入plist
NSDictionary* dict = @{@"name":@"lyd", @"age":@"18"};
[dict writeToFile:filePath atomically:YES];
NSDictionary* dicty = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSLog(@"%@", dicty);
// 数组写入plist
NSArray* array = @[@"lyd", @"18"];
[array writeToFile:filePath atomically:YES];
NSArray* arrayA = [NSArray arrayWithContentsOfFile:filePath];
NSLog(@"%@", arrayA);

属性列表与NSDictionary的写入与读取:

preference偏好设置
大部分iOS应用都支持偏好设置,如保存用户名、字体大小、密码、是否自动登录等。每个应用都有NSUserDefaults实例,通过它来存取偏好设置 。其本身的创建[NSUserDefaults standardUserDefaults]类似于单例模式,使用它设置数据的主要优点在于不需要关心文件名,系统自动生成,而且其底层类似NSDictionary,可以快速构建键值对。
preference偏好设置存储在App沙盒的Library目录中。
NSUserDefaults设置数据时不是立即写入,而是根据时间戳定时地将缓存中的数据写入本地磁盘中。因此调用set方法之后可能还未写入磁盘应用程序就终止了,导致写入失败。synchornize方法可以解决这个问题,实现同步强制写入。
objc
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"lyd" forKey:@"name"];
[defaults setObject:@"lyd123456" forKey:@"password"];
// 强制同步写入磁盘,解决异步写入延迟的问题
[defaults synchronize];
// 读取数据
NSString *name = [defaults objectForKey:@"name"];
NSKeyedArchiver归档解档
plist、UserDefaults均无法存储自定义模型对象,归档解档是iOS存储自定义对象的唯一文件方案。本质是对象序列化(对象转二进制)与反序列化(二进制转对象)。
要实现归档解档,在iOS13之前只需遵守NSCoding协议,而在iOS13之后,还需要遵守NSSecureCoding协议,同时实现supportsSecureCoding方法。其中NSSecureCoding协议也遵守NSCoding协议。
objc
// 支持安全归档
+ (BOOL)supportsSecureCoding {
return YES;
}
// 编码:对象转二进制(归档)
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.name forKey:@"name"];
[coder encodeInteger:self.age forKey:@"age"];
[coder encodeObject:self.city forKey:@"city"];
[coder encodeObject:self.hobbies forKey:@"hobbies"];
}
// 解码:二进制转对象(解档)
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
_name = [coder decodeObjectOfClass:[NSString class] forKey:@"name"] ?: @"";
_age = [coder decodeIntegerForKey:@"age"];
_city = [coder decodeObjectOfClass:[NSString class] forKey:@"city"] ?: @"";
NSSet *allowClass = [NSSet setWithObjects:[NSArray class], [NSString class], nil];
_hobbies = [coder decodeObjectOfClasses:allowClass forKey:@"hobbies"] ?: @[];
}
return self;
}
// 获取归档文件路径
+ (NSString *)archiveFilePath {
NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
return [docPath stringByAppendingPathComponent:@"UserArchive.data"];
}
// 写入归档文件
+ (BOOL)writeArchiveUserToFile {
LYDArchiver *user = [[LYDArchiver alloc] init];
user.name = @"lyd";
user.age = 18;
user.city = @"Shanghai";
user.hobbies = @[@"iOS", @"Objc"];
NSError *error = nil;
NSData *archiveData = [NSKeyedArchiver archivedDataWithRootObject:user requiringSecureCoding:YES error:&error];
if (error || !archiveData) {
NSLog(@"归档失败:%@", error.localizedDescription);
return NO;
}
return [archiveData writeToFile:[self archiveFilePath] atomically:YES];
}
// 读取解档文件
+ (instancetype)readArchiveUserFromFile {
NSData *data = [NSData dataWithContentsOfFile:[self archiveFilePath]];
if (!data.length) {
return nil;
}
NSError *error = nil;
LYDArchiver *user = [NSKeyedUnarchiver unarchivedObjectOfClass:[LYDArchiver class] fromData:data error:&error];
if (error) {
NSLog(@"解档失败:%@", error.localizedDescription);
return nil;
}
return user;
}
如果我们想将多对象统一归档解档,就可以使用NSData。
- 归档写入多个对象
objc
// iOS10以前写法:
NSMutableData *mData = [NSMutableData data];\
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:mData];
[archiver encodeObject:user1 forKey:@"user1"];
[archiver encodeObject:user2 forKey:@"user2"];
[archiver finishEncoding]; // 必须调用,否则返回空数据,归档失败
// iOS11后写法:
NSData *mdata = [NSKeyedArchiver archivedDataWithRootObject:user1 requiringSecureCoding:NO error:nil];
// 将存档的数据写入文件
NSString *path = [LYDArchiver archiveFilePath];
[mData writeToFile:path atomically:YES];
- 解档读取多个对象
objc
NSData *data = [NSData dataWithContentsOfFile:path];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:nil];
unarchiver.requiresSecureCoding = YES; // 开启安全编码校验
LYDArchiver *u1 = [unarchiver decodeObjectOfClass:[LYDArchiver class] forKey:@"user1"];
LYDArchiver *u2 = [unarchiver decodeObjectOfClass:[LYDArchiver class] forKey:@"user2"];
[unarchiver finishDecoding]; // 结束解码,释放unarchiver占有资源

总结
总结对比一下iOS主流数据持久化方案:
