「iOS」————持久化

iOS学习


IOS------持久化

为什么持久化?

数据保存

  • 目的:将应用程序中的数据保存到非易失性存储中,以便在应用程序关闭或重启后仍能访问这些数据。

  • 示例:保存用户输入的数据、应用设置、游戏进度等。

状态恢复:

  • 目的:在应用程序重新启动时恢复到之前的状态。
  • 示例:保存应用的最后状态,以便用户可以从中断的地方继续。

数据备份

  • 目的:定期备份数据,以防数据丢失。
  • 示例:定期备份数据库到云端或本地存储。

数据共享:

  • 目的:允许多个应用程序或设备之间共享数据。
  • 示例:使用云服务同步数据,如 iCloud、Dropbox 等。

离线访问:

  • 目的:即使在网络不可用的情况下也能访问数据。
  • 示例:离线模式下的地图应用、阅读应用等。

历史记录:

  • 目的:记录用户的操作历史,以便用户可以回顾之前的活动。
  • 示例:浏览历史、购买记录等。

数据分析:

  • 目的:收集用户行为数据,用于分析和改进产品。
  • 示例:收集用户使用频率、停留时间等数据。

用户个性化:

  • 目的:根据用户的偏好定制内容和服务。
  • 示例:保存用户喜好、推荐设置等。

安全性与合规性:

  • 目的:确保敏感数据的安全存储,并遵守相关法规要求。
  • 示例:加密存储个人身份信息、健康数据等。

性能优化:

  • 目的:通过缓存数据减少网络请求,提高应用程序的响应速度。
  • 示例:缓存图片、API 数据等。

数据持久化方式

  • NSUserDefault:简单数据快速读写。

  • Property list: 属性列表的文件存储

  • Archiver:归档。

  • SQLite:本地数据库。

  • CoreData:CoreData 是基于 sqlite 的封装。

数据存储区域

数据存储的区域在内存和磁盘

内存缓存

对于使用频率比较高的数据,从网络或磁盘加载数据到内存以后,使用后并不马上销毁,下次使用直接从内存加载

例如:iOS的图片加载

磁盘缓存

将从网络加载的,用户操作产生的数据写入到磁盘,用户下次查看、继续操作时,直接从磁盘加载使用

例如搜索历史的缓存、用户输入内容草稿的缓存,

对比项 内存缓存 磁盘缓存
速度 极快 较慢
容量 小(受内存限制) 大(受磁盘限制)
持久性 易失(进程结束即丢失) 持久(重启后仍在)
适用场景 热点、临时、频繁访问数据 历史、草稿、长期数据
实现难度 简单

因此实际开发中,通常采取二级缓存的方法:

  • 先查内存缓存,内存没有再查磁盘缓存,磁盘没有再查网络或重新生成。

沙盒机制

出于安全的原因,iOS 应用在安装时,为每个 App 分配了独立的目录,App 只能对自己的目录进行操作,这个目录就被称为沙盒。

应用程序只能访问自身的沙盒文件,不能访问其他应用程序的沙盒文件,当应用程序需要向外部请求或接收数据时,都需要经过权限认证,否则,无法获取到数据。所有的非代码文件都要保存在此。

例如属性文件plist、文本文件、图像、图标、媒体资源扽等,其原理是通过重定向技术,把程序生成和修改的文件定向到自身文件夹中。

沙盒中主要包含4个目录:MyApp.appDocumentsLibraryTmp,目录结构如下:

  • MyApp.app:包含了所有的资源文件和和可执行文件,上架前经过数字签名,上架后不可修改。

  • Documents:文档目录,要保存程序生成的数据,会自动被分到iCloud中。保存应用运行时生成的需要持久化的数据,iTunes同步设备时会备份该目录。例如,游戏应用可将游戏存档保存在该目录。(注意点:不要保存从网络上下载的文件,否则会无法上架!)

  • Library

    • 用户偏好,使用 NSUserDefault 直接读写!

    • 如果要想数据及时写入磁盘,还需要调用一个同步方法

    • 保存临时文件,"后续需要使用",例如:缓存图片,离线数据(地图数据

    • 系统不会清理 cache 目录中的文件,就要求程序开发时,"必须提供 cache 目录的清理解决方案"

      • Caches:存放体积大又不需要备份的数据

      • Preference:保存应用的所有偏好设置,iCloud会备份设置信息

  • Tmp:临时文件,系统会自动清理。重新启动就会清理。保存应用程序运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统也可能会清除该目录下的文件。iTunes同步设备时不会备份该目录。

    • 重新启动手机,系统磁盘空间不足时,该目录会被系统自动清空
    • 存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除。
    • 保存临时文件,后续不需要使用的文件。
应用沙盒目录的常见获取方式

获取沙盒的根目录:

objectivec 复制代码
NSString *home = NSHomeDirectory();

每次编译代码会生成新的沙盒路径,是在编译的时候,所以模拟机和真机每次的沙盒路径都是不一样的。

上面的代码得到的就是当前应用程序目录的路径,该目录下就是应用程序的沙盒,在该目录下有4个文件夹:DocumentsLibrarySystemDatatmp当前应用程序只能访问该目录下的文件。

利用 NSSearchPathForDirectoriesInDomains 函数获取沙盒目录

objectivec 复制代码
//文件路径搜索
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就表示直接写成"~"。

上述两个参数的枚举值:

objectivec 复制代码
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)
};

获取示例:

objectivec 复制代码
 // 搜索 Document 目录
    NSArray<NSString *> *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentDirectory = [documentPaths firstObject];
    NSLog(@"Document Directory: %@", documentDirectory);
    // 搜索 Document 目录 NO
    
    NSArray<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: 缓存对象即将被清理时调用,一般开发者用来调试,不能在此方法中修改缓存

在以下场景中会被调用:

  1. removeObjectForKey
  2. 缓存对象超过NSCache的countLimit和otalCostLimit属性设置的限制
  3. App进入后台
  4. 系统发出内存警告
  5. cache这个实例的生命周期结束前
注意
  • 当收到内存警告,而我们又调用removeAllObjects,则无法再继续往内存中添加数据。
  • 不提供缓存总的大小,想知道NSCache占用的内存大小,只有通过添加缓存的cost自己计算。
  • NSCache自动释放内存的算法是不确定的,有时是按照LRU(最近最久未使用)释放,有时随机释放。
  • NSCache中的数据在APP重启后会消失,因为NSCache只是将数据保存在内存 的。

NSCache和NSMutableDictionary的区别

NSCache是线程安全的,不需要加线程锁,而NSMutableDictionary线程不安全。

我们来做个代码演示:

objectivec 复制代码
- (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);
}



从打印结果可以看看出:

  1. 设置缓存限制且知道缓存成本数时,超出是会自动回收。但是设置缓存限制但不知道缓存成本数时不会自动回收。
  2. 使用 removeAllObjects 回收时会调用 willEvictObject 的代理方法。

持久化数据存储方式

Plist(属性列表)

属性列表是一种XML格式的文件,拓展名为plist如果对是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,就可以使用 writeToFile:atomically: 方法直接将对象写到属性列表文件中。

属性列表与NSDictionary的存储和读取过程:

我们用代码学习一下:

objectivec 复制代码
- (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实例,通过它来存取偏好设置。

比如:如果我们要保存用户名,字体大小,是否自动登录:

objectivec 复制代码
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@"张三" forKey:@"username"];
[defaults setFloat:18.0f forKey:@"text_size"];
[defaults setBool:YES forKey:@"auto_login"];

当我们需要使用时,读取上次保存的位置:

objectivec 复制代码
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方法,这样我们才能归档成功。

也就是说我们一定要实现下面三个方法,才可以完成一次归档和解档

objectivec 复制代码
- (void)encodeWithCoder:(NSCoder *)coder; // 解码
- (nullable instancetype)initWithCoder:(NSCoder *)coder; // 编码
+ (BOOL)supportsSecureCoding; // 是否支持
objectivec 复制代码
// 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

以下简单理解:

objectivec 复制代码
#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]创建可变数据空间。

objectivec 复制代码
// 新建一块可变数据区
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
  • FMDB:iOS端github使用最广的针对OC对sqlite的封装,支持队列操作

  • WCDB:微信技术团队开源的对sqlite操作的封装,支持对象和数据库映射,ORM数据库的一种实现,比FMDB更高效

序列化与反序列化

序列化 :把对象转化为字节序列的过程
反序列化 :把字节序列恢复成对象
作用:把对象写到文件或者数据库中,并且读取出来

iOS中怎么实现序列化?

在iOS中就是采用我们的一个归档的方式来实现一个序列化的.在iOS中一个自定义对象是无法直接存入到文件中的,必须先转化成二进制流才行。从对象到二进制数据的过程我们一般称为对象的序列化(Serialization),也称为归档(Archive)。同理,从二进制数据到对象的过程一般称为反序列化或者反归档(解档)。