• 【iOS】—— 持久化


    一、数据持久化的目的

    1.快速展示,提升体验

    • 已经加载过的数据,用户下次查看时,不需要再次从网络(磁盘)加载,直接展示给用户

    2.节省用户流量(节省服务器资源)

    • 对于较大的资源数据进行缓存,下次展示无需下载消耗流量
    • 同时降低了服务器的访问次数,节约服务器资源。(图片)

    3.离线使用。

    • 用户浏览过的数据无需联网,可以再次查看。
    • 部分功能使用解除对网络的依赖。(百度离线地图、图书阅读器)
    • 无网络时,允许用户进行操作,等到下次联网时同步到服务端。

    4.记录用户操作

    • 草稿:对于用户需要花费较大成本进行的操作,对用户的每个步骤进行缓存,用户中断操作后,下次用户操作时直接继续上次的操作。
    • 已读内容标记缓存,帮助用户识别哪些已读。
    • 搜索记录缓存

    二、iOS中数据持久化方案

    • NSUserDefault 简单数据快速读写
    • Property list (属性列表)文件存储
    • Archiver (归档)
    • SQLite 本地数据库
    • CoreData

    三、数据持有化方式分类

    在移动端的数据持有化方式总体有两类:

    内存缓存

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

    内存指当前程序的运行空间,缓存速度快容量小,是临时存储文件用的,供CPU直接读写。打开一个程序,他是在内存中存储,关闭程序后内存就又回到原来的空间空间。

    • 案例:
      • iOS系统图片加载 —— [UIImage imageNamed:@“imageName”]
      • 网络图片加载三方库 SDWebImage

    磁盘缓存

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

    磁盘是程序的存储空间,缓存容量大、速度慢、可持有化。与内存不同的是磁盘是永久存储东西的。

    • 案例:
      • 用户输入内容草稿缓存
      • 搜索历史缓存
      • 网络图片加载三方库 SDWebImage

    四、沙盒机制介绍

    iOS中的沙盒机制是一种安全体系。为了保证系统安全,iOS每个应用程序在安装时,会创建属于自己的沙盒文件(存储空间)。应用程序只能访问自身的沙盒文件,不能访问其他应用程序的沙盒文件,当应用程序需要向外部请求或接收数据时,都需要经过权限认证,否则,无法获取到数据。所有的非代码文件都要保存在此,例如属性文件plist、文本文件、图像、图标、媒体资源等,其原理是通过重定向技术,把程序生成和修改的文件定向到自身文件夹中。

    五、沙盒的目录结构

    每个APP的沙盒下面都有相似目录结构,如下图:
    4324324

    获取应用程序的沙盒路径

    // 获取沙盒根目录路径
    NSString *path = NSHomeDirectory();
    
    • 1
    • 2

    注意: 每次编译代码会生成新的沙盒路径,注意是编译不是启动,所以模拟机或者真机运行,每次运行所得到的沙盒路径都是不一样的,线上版本app真机不会生成新的沙盒路径。

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

    访问沙盒目录常用C函数介绍

    //文件路径搜索
    FOUNDATION_EXPORT NSArray<NSString *> *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);
    
    • 1
    • 2

    该方法返回值为一个数组,在iphone中由于只有一个唯一路径,所以直接取数组第一个元素即可。

    • 参数一 NSSearchPathDirectory directory:指定搜索的目录名称,比如这里用NSDocumentDirectory表明我们要搜索的是Documents目录。如果我们将其换成NSCachesDirectory就表示我们搜索的是Library/Caches目录。
    • 参数二 NSSearchPathDomainMask domainMask:搜索主目录的位置,NSUserDomainMask表示搜索的范围限制于当前应用的沙盒目录。还可以写成NSLocalDomainMask(表示/Library)、NSNetworkDomainMask(表示/Network)等。
    • 参数三 BOOL expandTilde:是否获取完整的路径,我们知道在iOS中的全写形式是/User/userName,该值为YES即表示写成全写形式,为NO就表示直接写成“~”。

      该值为NO:Caches目录路径为~/Library/Caches
      该值为YES:Caches目录路径为/var/mobile/Containers/Data/Application/E7B438D4-0AB3-49D0-9C2C-B84AF67C752B/Library/Caches

    下面给出上述两个参数的枚举值:

    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)
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    沙盒目录的获取方式

    //获取沙盒根路径
    NSString *path = NSHomeDirectory();
    NSLog(@"沙盒根路径:%@", path);
    //Document路径
    NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    NSLog(@"Document目录路径:%@", docDir);
    // 获取Library的目录路径
    NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
    NSLog(@"Libarary目录路径:%@", libDir);
    // 获取Caches目录路径
    NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
    NSLog(@"Cacheas目录路径:%@", cachesDir);
    // library Preference
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSLog(@"偏好设置目录路径:%@", defaults);
    // 获取tmp目录路径
    NSString *tmpDir =  NSTemporaryDirectory();
    NSLog(@"tmp目录路径:%@", tmpDir);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    输出结果:
    4324234

    沙盒目录介绍

    • Documents:保存持久化数据,会备份。一般用来存储需要持久化的数据。
      一般我们在项目中,我们会把一些用户的登录信息以及搜索历史记录等一些关键数据存储到这里。
      // 获取Documents目录路径
      NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
      
      • 1
      • 2
      此文件夹是默认备份的,备份到iCloud。
      注:iCloud的备份,会通过Wi-Fi每天自动备份用户iOS设备。
      我们可以在获取到的路径结尾加一个字符串来创建一个文件名:
      NSString *filename = [docDir stringByAppendingPathComponent:@"data.txt"];
      
      • 1
      这样我们的这个filename就是一个完整的.txt类型文件的目录了。
    • Library:默认存放设置和其他状态信息,除了caches子目录之外其他目录都会被iclude同步。
      • Application Support:此目录包含应用程序用来运行但应对用户隐藏的文件,如游戏的新关卡等文件。
      • Caches:保存应用运行时生成的需要持久化的数据,一般存储体积大、不需要备份的非重要数据,如网络请求的音视频与图片等的缓存。在 iOS 5.0 及以后版本中,Caches 当系统磁盘空间非常低时,系统可能会在极少数情况下该删除目录(APP 正在运行时不会发生),所以尽量保证该路径的文件在 APP 在重新运行时可以得到重新创建。
      • Cooikes:系统会自动将App中网络请求的cookie保存为文件。
      • Preferences:保存应用的所有偏好设置。UserDefaults 生成的 plist 文件就会保存该目录下。
      • SplashBoard:存储启动屏缓存,缓存文件格式为 ktx,本质上就是图片,如果启动屏不生效的问题可以考虑从删除该路径下相关缓存文件这个角度解决。
    • SystemData:存放系统数据,无对外暴露的接口。
    • tmp:临时文件夹(系统会不定期删除里面的文件)。

    六、持久化数据存储方式

    1.XML属性列表

    属性列表是一种XML格式的文件,拓展名为plist

    如果对象是NSStringNSDictionaryNSArrayNSDataNSNumber等类型,就可以使用
    writeToFile:atomically:方法直接将对象写到属性列表文件中,举例说明:

    // 获取 Document 文件目录
    NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    // 在 Document 目录下新建一个 test.plist 文件
    NSString * filePath = [docPath stringByAppendingPathComponent:@"test.plist"];
    
    // 存字典,将字典数据存到刚才的 test.plist 文件
    NSDictionary* dict = @{ @"name" :@"xiyouedc", @"age" : @"18" };
    [dict writeToFile:filePath atomically:YES];
    
    // 取字典,从刚才的 test.plist 文件中取出字典数据
    NSDictionary* dictA = [NSDictionary dictionaryWithContentsOfFile:filePath];
    NSLog(@"%@", dictA);
    
    // 存数组
    NSArray* array = @[@"xiyouedc", @"18"];
    [array writeToFile:filePath atomically:YES];
    
    // 取数组
    NSArray* arrayA = [NSArray arrayWithContentsOfFile:filePath];
    NSLog(@"%@", arrayA);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输出结果:
    435345
    同时进入该项目的沙盒目录中可以看到 Document 目录下多了我们刚才创建的 test.plist 文件:
    4324234
    因为我们最后是存储的 NSArray 数据类型的,所以他这里也就是 NSArray 类型的数据。
    312323534

    2.Preferences偏好设置(UserDefaults)

    很多iOS应用都支持偏好设置,提供了一套标准的解决方案来为应用加入偏好设置功能,比如保存用户名,字体大小,密码,是否自动登录等。

    每个应用都有个NSUserDefaults实例,可以通过它来存取偏好设置,不需要路径。其本身的创建类似于单例模式,我们在后面用不同的属性名再次申请创建,会覆盖之前的数据。

    NSUserDefaults:简单数据快速读写,不能存储自定义类型。
    UserDefaults设置数据时,不是立即写入,而是根据时间戳定时地把缓存中的数据写入本地磁盘。所以调用了set方法之后数据有可能还没有写入磁盘应用程序就终止了。出现以上问题,可以通过调用synchornize方法[defaults synchornize];强制写入。

    偏好设置存储的优点:

    • 不需要关心文件名,系统会自动帮你生成一个文件名。
    • 快速做键值对的存储。

    我们使用UserDefaults注册一个账号密码试一下:

    // 获取偏好设置对象
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    
    //存储数据
    [defaults setObject:@"xiyouedc" forKey:@"name"];
    [defaults setObject:@"1234567890" forKey:@"password"];
    
    // 同步调用,立刻写到文件中,不写这个方法会异步,有延迟
    [defaults synchronize];
    
    // 需要验证账号密码的地方,获取偏好设置对象
    NSUserDefaults *defaultsA = [NSUserDefaults standardUserDefaults];
    NSString *name = [defaultsA objectForKey:@"name"];
    NSString *password = [defaultsA objectForKey:@"password"];
    NSLog(@"name:%@ password:%@", name, password);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    输出结果:
    34234234
    同时我们可以看到在沙盒中的偏好设置中出现了一个新的文件,里边存放的是我们刚才写入的数据:
    4234234234

    3.NSKeyedArchiver归档和解档

    NSKeyedArchiver(归档):归档一般都是保存自定义对象的时候,使用归档。因为plist文件不能够保存自定义对象。如果一个字典中保存有自定义对象,如果把这个对象写入到文件当中,它是不会生成 plist文件的。如果对象是NSStringNSDictionaryNSArrayNSDataNSNumber等类型,可以直接用NSKeyedArchiver进行归档和恢复。

    但是在我们使用归档之前,我们必须得遵守NSSecureCoding协议才行,老版本只需要遵循NSCoding实现其归档和解档的方法就行,但是iOS13更新之后就不行了,我们就必须的遵守NSSecureCoding协议,NSSecureCoding协议也遵循了原来NSCoding这个协议,不过我们还需要遵循它的一个supportsSecureCoding方法,这样我们才能归档成功。

    因为NSSecureCoding协议也遵循了原来NSCoding这个协议,所以他也就有了- (void)encodeWithCoder:(NSCoder *)coder方法和- (id)initWithCoder:(NSCoder *)coder方法:

    • - (void)encodeWithCoder:(NSCoder *)coder方法

      每次归档对象时,都会调用这个方法。一般在这个方法里面指定如何归档对象中的每个实例变量。可以使用encodeObject:forKey:方法归档实例变量。

    • - (id)initWithCoder:(NSCoder *)coder方法

      每次从文件中会恢复(解码)对象时,都会调用这个方法。一般在这个方法里面指定如何解码文件中的数据为对象的实例变量,可以使用decodeObject:forKey方法解码实例变量。

    除了这两个方法之外,NSSecureCoding协议还有+ (BOOL)supportsSecureCoding方法,只有当这个方法为YES的时候,才可以归档成功。

    举例说明:

    // person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject<NSSecureCoding>
    @property (nonatomic, strong) NSString *name;
    @property int age;
    @end
    // person.m
    #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;
    }
    
    + (BOOL)supportsSecureCoding {
        return YES;
    }
    @end
    
    // 归档:自定义对象想要存储到沙盒(文件夹),必须通过归档。
    - (void)save {
        // 1.新的模型对象
        Person *person = [[Person alloc] init];
        person.name = @"xiyouedc";
        person.age = 18;
        NSError *error;
        // 2.归档模型对象
        // 3.获得 Documents 的全路径
        NSString *docu = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
        // 4.获得新文件的全路径,即新建一个 person.data 文件来存储我们要归档的数据
        NSString *path = [docu stringByAppendingString:@"/person.data"];
        // 5.将对象封装为 Data 数据并归档
        NSData *data = [NSKeyedArchiver archivedDataWithRootObject:person requiringSecureCoding:YES error:&error];
        if (error) {
            NSLog(@"sodufosuf%@", error);
        }
        [data writeToFile:path atomically:YES];
    }
    
    // 读档,将数据从文件中读出
    - (void)read {
       NSError *error;
       // 1.获得 Documents 的全路径
       NSString *docu = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
       // 2.获得文件的全路径,即获取我们要解档文件的路径
       NSString *path = [docu stringByAppendingString:@"/person.data"];
       // 3.从 path 路径中获取 Data 数据
       NSData *unData = [NSData dataWithContentsOfFile:path];
       // 4.从文件中读取Person对象
       Person *person = (Person *)[NSKeyedUnarchiver unarchivedObjectOfClass:[Person class] fromData:unData error:&error];
       // 打印结果
       NSLog(@"%@--%d",person.name, person.age);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    输出结果:
    543534534
    同时 Documents 文件下也出现了我们创建的 person.data 文件:
    423423423

    注意: 我们一定要遵守NSSecureCoding协议,并实现它的归档解档方法,以及+ (BOOL)supportsSecureCoding方法!!!

    4.数据库存储

    • SQLite
      是目前主流的嵌入式关系型数据库,其最主要的特点就是轻量级、跨平台,当前很多嵌入式操作系统都将其作为数据库首选。
    • CoreData
      CoreData是iOS5之后才出现的一个框架,本质上是对SQLite的一个封装,它提供了对象-关系映射(ORM)的功能,即能够将OC对象转化成数据,保存在SQLite数据库文件中,也能够将保存在数据库中的数据还原成OC对象,在这个过程中不需要手动编写任何SQL语句,CoreData封装了数据库的操作过程,以及数据库中数据和OC对象的转换过程。通过CoreData管理应用程序的数据模型,可以极大程度减少需要编写的代码数量。
    • FMDB
      是一个处理数据存储的第三方框架,框架是对sqlite的封装,整个框架非常轻量级但又不失灵活性,而且更加面向对象。

    SQLiteCoreData的区别:

    • CoreData可以在一个对象更新时,其关联的对象也会随着更新,相当于你更新一张表时,其关联的其他表的也会随着更新。
    • CoreData供更简单的性能管理机制,可以限制查询记录的总数,这个类会自动更新其缓存。
    • 多表查询方面,CoreData没有SQL直观,没有类似外连接,左连接等操作。

    什么是序列化和反序列化,用来做什么?

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

    如果觉得不够深入可以自行了解:iOS数据持久化设计iOS 数据存储(持久化存储、缓存)

  • 相关阅读:
    static关键字超详细总结
    C#编程学习
    深度|谁在为OpenAI和Anthropic的AI编程竞赛提供“军火”?已赚得盆满钵满
    LeetCode 75 part 07 队列
    sessionStorage、localStorage、cookie的缓存
    瑞吉外卖03-新增员工
    C#和西门子PLC使用Udp通信
    HandlerAdapter接口类的简介说明
    基于JSP的图书销售管理系统
    航空数据链协议解析与仿真测试
  • 原文地址:https://blog.csdn.net/m0_55124878/article/details/126043947