iOS 数据持久化存储

2,530 阅读9分钟
原文链接: www.jianshu.com

在iOS开发中,所谓的数据持久化存储,就是将数据保存在硬盘中,这样即使我们的应用关闭后重启,仍然是可以继续访问之前保存的数据. 在iOS开发中,最常见的就是以下几种存储方案.

常见的几种存储方案
1, plist(属性列表)文件
2, Preference(偏好设置)
3, NSKetedArchiver(归档)
4, FMDB(基于SQLite3 封装的一套OC的API库)
5, CoreData

一、 沙盒结构介绍

首先需要了解一下Bundle和沙盒(sandbox)之间的区别:
Bundle: 应用程序在手机中的安装路径
sandbox(沙盒):专门来存储当前APP自己的数据的路径

  • 沙盒机制:在iOS中每个APP都拥有自己的沙盒,APP只能访问对应沙盒中存储的数据, iOS是不允许跨越沙盒去访问数据的,所有的数据都是保存在该沙盒的三个子目录下:

    Document
    Library(Library/Caches, Library/Preference)
    temp

    • Document: 一般在该目录下保存一些比较重要的数据,比如:游戏相关的数据, 当连接iTunes后会自动同步数据

      注意点: 如果将数据资源保存到该目录.上架可能会被拒绝,(解决方案:直接设置该文件夹不被iTunes备份),总之:不能保存从网上下载的数据,否则不能上架

    • Library: 存储应用设置或者状态信息等,在该目录下还有两个子目录:Caches和Preference

      Library/Caches: 存放缓存文件,iTunes不会备份,因此文件不会因APP退出而删除(一般使用SDWebImage的缓存资源都是保存到这来)
      Library/Preference: 保存应用的所有偏好设置,iOS的Setting(设置)会在该目录查找该应用的设置信息,iTunes会同步数据

    • temp: 临时文件, iTunes不会备份该文件夹中的数据, 这个文件夹中的数据,会因为应用的关闭而删除.
  • 如何获取到对应目录下的路径

    参数1: 指定搜索的路径名称:是个枚举值:
    NSDocumentDirectory: 表示在Document中寻找数据
    NSCachesDirectory: 表示在caches中寻找
    参数2: 也是个枚举值:
    NSUserDomainMask: 表示在用户的主目录
    NSLocalDomainMask: 表示在当前的机器
    NSNetworkDomainMask: 表示网络中可见的主机
    NSSystemDomainMask: 表示系统目录,不可修改
    NSAllDomainsMask: 表示所有
    参数3, 是否展开波浪线
    NSString *cachesPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory(参数1), NSSearchPathDomainMask(参数2), BOOL(参数3))

//2.主目录
NSString *home = NSHomeDirectory();

//3.文档目录
//3.1拼接字符串
NSString *docDir1 = [NSString stringWithFormat:@"%@/%@",home,@"Documents"];

//3.2拼接路径(一般处理路径拼接建议使用此方法)
NSString *docDir2 = [home stringByAppendingPathComponent:@"Documents"];

NSString *docDir3 = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

//这个目录是在Library目录下的,需要自己手动创建文件夹(一般不会存在这个下面)
NSString *docError = [NSSearchPathForDirectoriesInDomains(NSDocumentationDirectory, NSUserDomainMask, YES) lastObject];

//4.缓存目录
NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];

//5.临时目录
NSString *tempDir = NSTemporaryDirectory();
在iOS开发中,苹果提供了4中数据持久化方案供我们选择,分别是: 属性列表(plist), 数据归档(NSKeyedValueArchiver/NSUserDefaults), 数据库(splite)以及coreData.
 plist文件存储:
 plist是一种明文的轻量级存储方式,最常用的格式是XML格式,比如:新疆一个项目是,系统会提供一个info.plist文件,这种方式的安全性几乎为0,所提plist主要是用于存储少了并且不重要的数据. plist只能读取数组(NSArray)或者字典(NSDictonary).

 数据归档/序列化(NSKeyedArchiver):
 NSKeyedArchiver是一种轻量级存储的持久化方案,数据化归档时经过加密处理的,所以安全性远高于plist,数据归档可以存储一些复杂的对象,数据保存前会经过二进制处理.注意:使用这种方案是需要前提的, 使用前必须要遵守NSCoding协议并且实现协议中的两个方法.

 数据库(splite):
 splite是一个轻量级,跨平台的小型数据库,可移植性比较高,有着和MySpl几乎相同的数据库语句,以及无需服务器即可使用的优点:
 数据库的优点:
 1, 该方案可以存储大量的数据,存储和检索的速度非常快.
 2, 能对数据进行大量的聚合,这样比起使用对象来讲操作要快.
 数据库的缺点:
 1, 它没有提供数据库的创建方式
 2, 它的底层是基于C语言框架设计的, 没有面向对象的API, 用起来非常麻烦
 3, 发杂的数据模型的数据建表,非常麻烦
在实际开发中我们都是使用的是FMDB第三方开源的数据库,该数据库是基于splite封装的面向对象的框架.

 coreData:
 coreData是苹果官方在iOS5之后推出的综合性数据库,其使用了对象关系映射技术,将对象转换成数据,将数据存储在本地的数据库中
 coreData为了提高效率,需要将数据存储在不同的数据库中,比如:在使用的时候,最好是将本地的数据保存到内存中,这样的目的是访问
 速度比较快.
  • 数据化存储方案的各自使用场景

    plist文件: 主要是用于不用加密的数据存储,比如: 在添加地址中使用的pickerView中的cities.plist文件
    数据归档/数据序列化: 当我们想要存储一些更加复杂的自定义数据时,NSUserDefault无法提供更强大的帮助,这时候就需要选择其他的存储方法,如果我们的程序此时不需要执行查询数据,数据迁移等操作,或者是并非所有的数据都有发杂的关系图,这时候数据的归档方案是最适合的.使用archive格式的文件将归档的数据存储在沙盒目录下,这种格式读取出来的是二进制,,需要用NSKeyedUnarchiver类对该数据进行解档(反序列化)
    小总结: 虽然苹果官方给我们提供了4中不同的方式存储数据.但是它并没有要求我们在什么情况下使用对应的数据存储方案.我们只能在不同的开发场景下,选择最适合的持久化方案.

  • plist文件存储

    • 一般使用plist文件存储的数据都是不需要加密,存储少量,简单的数据,plist文件能存储字典和数组,不能存储对象。
/**
 *  Plist文件
 */

- (void)setUpPlist {

    NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [path stringByAppendingPathComponent:@"Person.plist"];

    NSDictionary *dict = @{
                           @"name" : @"William",
                           @"age" : @18,
                           @"height" : @1.75f
                           };

    // 将数据写入Plist
    [dict writeToFile:filePath atomically:YES];
    NSLog(@"%@", filePath);

    // 读取plist中的数据
    NSDictionary *dicts = [NSDictionary dictionaryWithContentsOfFile:filePath];
    NSLog(@"dict = %@",dicts);
    NSString *str = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"str = %@", str);
}
  • 偏好设置
    • iOS开发中也是会用到(Preference)偏好设置来存储数据,比如: 保存用户的用户名,密码,字体大小等设置。通过NSUserDefaults来存取偏好设置
/**
 *  偏好设置
 */
// 不能保存自定义的对象
- (void)setUpSaveUserDefaults {

    // 获取NSUsersDefault对象(是个单利)
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSLog(@"%@", defaults);
    // 保存数据(注意: 如果设置的数据没有同步, 那么会在将来的某个时刻自动将数据保存到Preference文件夹下面)
    [defaults setObject:@"Mr.William" forKey:@"name"];
    [defaults setInteger:23 forKey:@"age"];
    [defaults setDouble:1.75f forKey:@"height"];
    [defaults setObject:@"man" forKey:@"gender"];

    // 强制让数据立刻保存
    [defaults synchronize];
}

- (void)setUpReadUserDefaults {

    // 同样获取NSUserDefault对像
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    // 读取数据
    NSString *name = [defaults objectForKey:@"name"];
    NSString *gender = [defaults objectForKey:@"gender"];
    NSInteger age = [defaults integerForKey:@"age"];
    double height = [defaults doubleForKey:@"height"];

    NSLog(@"name=%@,gender=%@,age=%ld,height=%.1f",name,gender,age,height);
}

重点说明:
(1)偏好设置是专门用来保存应用程序的配置信息的, 一般情况不要在偏好设置中保存其他数据。如果利用系统的偏好设置来存储数据, 默认就是存储在Preferences文件夹下面的,偏好设置会将所有的数据都保存到同一个文件中。
(2)使用偏好设置对数据进行保存之后, 它保存到系统的时间是不确定的,会在将来某一时间点自动将数据保存到Preferences文件夹下面,如果需要即刻将数据存储,可以使用[defaults synchronize];
(3)注意点:所有的信息都写在一个文件中,对比简单的plist可以保存和读取基本的数据类型。
(4)步骤:获取NSuserDefaults,保存(读取)数据

  • 归档(序列化)、解档(反序列化)

    1, 将给种类型的对象存储到文件中,不仅仅是字符串或者字典,还能实现对自定义类的对象进行归档.
    2, 序列化与反序列化:
    序列化: 将一个OC对象转换成NSData(二进制)的操作就叫做对象的序列化
    反序列化: 将本地的二进制数据转为一个OC对象的操作就叫做反序列化
    OC对象需要通过遵守NSCoding协议,并且实现协议中的两个方法,才能支持序列化和反序列化操作.

Student.h

#import 
#import "Book.h"

@interface Student : NSObject 

/**姓名*/
@property(nonatomic, copy) NSString *name;

/**年龄*/
@property(nonatomic, assign) int age;

/**课本*/
@property(nonatomic, strong) Book *book;

@end

Student.m
#import "Student.h"

static NSString * const kStudentNameKey = @"kStudentNameKey";
static NSString * const kStudentAgeKey = @"kStudentAgeKey";
static NSString * const kBookKey = @"kBookKey";


@implementation Student

/**解档(反序列化)*/
- (instancetype)initWithCoder:(NSCoder *)aDecoder {

    if (self == [super init]) {

        self.name = [aDecoder decodeObjectForKey:kStudentNameKey];
        self.age = [aDecoder decodeIntForKey:kStudentAgeKey];
        self.book = [aDecoder decodeObjectForKey:kBookKey];
    }
    return self;
}

/**归档(序列化)*/
- (void)encodeWithCoder:(NSCoder *)aCoder {

    // 归档姓名(字符串对象)
    [aCoder encodeObject:self.name forKey:kStudentNameKey];

    // 归档年龄(注意:这是基本数据类型, 如果是其他的类型,直接调用对应类型的encode即可)
    [aCoder encodeInteger:self.age forKey:kStudentAgeKey];

    // 归档自定义类(Book)对象
    [aCoder encodeObject:self.book forKey:kBookKey];
}
@end

/***************************************/
viewController.m
// 保存
- (IBAction)saveDatas:(id)sender {

    // 创建对象
    Student *student = [[Student alloc] init];
    student.name = @"Alex";
    student.age = 15;
    student.book.bookName = @"毛泽东思想与概论";

    // 获取文件路径
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [path stringByAppendingPathComponent:@"student.archiver"];

    // 将自定义的对象保存到文件中,调用的是NSKeyedArchiver的类方法
    BOOL flag = [NSKeyedArchiver archiveRootObject:student toFile:filePath];
    if (flag) {

        NSLog(@"如果来到这里说明归档成功");
    }
   }

// 读取
- (IBAction)readDatas:(id)sender {

    // 获取文件保存的路径
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [path stringByAppendingPathComponent:@"student.archiver"];

    // 打印文件保存的路径
    NSLog(@"%@", filePath);

    // 从文件中读取对象, 解档对象就调用NSKeyedUnarchiver的一个类方法,unarchiveObjectWithFile: 即可

    Student *student = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];

    if (student) {

        // 来到这里说明读取成功
        NSLog(@"%@, %d, %@", student.name, student.age, student.book.bookName);
    }
   }
  • 归档与解档实例
  • 界面效果


    页面效果.png

  • 打印结果


    打印结果.png

Person.h
#import 

@interface Person : NSObject

@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) int age;
@end

Person.m
@implementation Person

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if (self == [super init]) {

        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.age = [aDecoder decodeIntForKey:@"age"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeInt:self.age forKey:@"age"];
}
@end

ViewController.m
- (IBAction)saveDatas:(id)sender {

    // 创建归档对象
    Person *per = [[Person alloc] init];
    per.name = @"Alex";
    per.age = 15;

    // 获取数据保存的路径
    NSString *path = NSHomeDirectory();
    NSString *filePath = [path stringByAppendingPathComponent:@"person.archive"];

    BOOL flag = [NSKeyedArchiver archiveRootObject:per toFile:filePath];
    if (flag) {

        NSLog(@"来到这里表示已经归档成功: %@",filePath);
    }
}

- (IBAction)updateDatas:(id)sender {

    NSString *path = NSHomeDirectory();
    NSString *file = [path stringByAppendingPathComponent:@"person.archive"];
    Person *persion = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
    if (persion) {
        persion.name = @"William";
        persion.age = 16;
    }

    // 再次归档
   BOOL flag =  [NSKeyedArchiver archiveRootObject:persion toFile:file];
    if (flag) {

        NSLog(@"file:%@", file);
        NSLog(@"name:%@, age: %d", persion.name, persion.age);
    }
}
- (IBAction)readDatas:(id)sender {

    // 获取数据保存的路径
    NSString *path = NSHomeDirectory();
    NSString *filePath = [path stringByAppendingPathComponent:@"person.archive"];

    Person *persion = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
    if (persion) {

        // 来到这里说明,解档成功
        NSLog(@"name: %@, age: %d", persion.name, persion.age);
    }
}