009-数据持久化方案

数据持久化方案

iOS 默认情况下只能访问程序自己所在的目录,称为“沙盒”,沙盒结构的目录如下:

  • Application 应用程序包,存放资源文件和可执行文件,上架前经过数字签名,上架后不可修改

    NSString *path = [[NSBundle mainBundle] bundlePath];
    
  • Documents: iCloud 备份目录,存放数据,但不能存放缓存文件

    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    
  • Library/Caches: iCloud 不会备份此目录,适合大体积、不需要备份的文件

  • Library/Preferences: 保存应用设置信息,iCloud 会备份

  • tmp: 临时文件,随时可能被删除,iCloud 不会备份

数据持久化方案

  • plist文件(属性列表)
  • preference(偏好设置)
  • NSKeyedArchiver(归档)
  • SQLite 3
  • CoreData

1. plist 文件

plist 文件本质上是 xml 格式存储的结构化数据,支持以下数据结构

  • NSArray
  • NSMutableArray
  • NSDictionary
  • NSMutableDictionary
  • NSData
  • NSMutableData
  • NSString
  • NSMutableString
  • NSNumber
  • NSDate

获取路径

首先获取到存储路径和文件名称

    //NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *url = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
    NSString *fileName = [url.path stringByAppendingPathComponent:@"123.plist"];

这里获取文件路径有两种方式

NSArray<NSString *> *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);

此方法返回的是一个字符串数组,NSSearchPathDirectory 就是沙盒结构内的目录,NSSearchPathDomainMask 指定搜索范围,这里的NSUserDomainMask表示搜索的范围限制于当前应用的沙盒目录。还可以写成NSLocalDomainMask(表示/Library)、NSNetworkDomainMask(表示/Network)等。第三个参数表示是否将文件路径补全,例如下面的代码

    NSString *path = [@"~/Documents/test" stringByExpandingTildeInPath];
    NSLog(@"%@", path);

打印结果是

/var/mobile/Containers/Data/Application/7CCD2BE1-9092-4A8C-81C7-348ABC960013/Documents/test

另一种获取文件路径的方法是

- (NSArray<NSURL *> *)URLsForDirectory:(NSSearchPathDirectory)directory inDomains:(NSSearchPathDomainMask)domainMask NS_AVAILABLE(10_6, 4_0);

它是 NSFileManager 的方法,还有一个方法可以用于选择在目录不存在的情况下创建该目录

- (nullable NSURL *)URLForDirectory:(NSSearchPathDirectory)directory inDomain:(NSSearchPathDomainMask)domain appropriateForURL:(nullable NSURL *)url create:(BOOL)shouldCreate error:(NSError **)error NS_AVAILABLE(10_6, 4_0);

苹果官方文档推荐用 FileManager 的方法获取文件目录,获取到的是一个 NSURl 数组,拿到其中的 NSURL 后可以通过 url.path 获取到路径,然后补上文件名称

    NSString *fileName = [url.path stringByAppendingPathComponent:@"123.plist"];

存储数据

    NSArray *array = @[@"yasic", @"esir"];
    [array writeToFile:fileName atomically:YES];

读出数据

    NSArray *readArray = [NSArray arrayWithContentsOfFile:fileName];
    _resultLabel.text = [NSString stringWithFormat:@"%@ %@", readArray[0], readArray[1]];

2. preference

preference 本质上也是 xml 文件,存储的应用设置相关的数据。

    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setObject:@"Male" forKey:@"Yasic"];
    [userDefaults setObject:@"Famle" forKey:@"Esir"];
    [userDefaults synchronize];
    NSString *male = [userDefaults objectForKey:@"Yasic"];
    NSString *famale = [userDefaults objectForKey:@"Esir"];
    _resultLabel.text = [NSString stringWithFormat: @"yasic %@, esir %@", male, famale];

要注意 synchronize 方法是用来同步内存中的数据到文件里的,如果不手动调用则系统会在合适的时候进行写入操作。

3. NSKeyedArchiver

如果一个类遵循了 NSCoding 协议就可以进行归档和解档操作。

  • NSString
  • NSNumber
  • NSArray
  • NSDictionary
  • NSSet
  • NSData
  • UIColor
  • UIImage

首先定义一个遵循协议的类并实现协议的归档和解档方法

@interface Person : NSObject<NSCoding>

@property(strong, nonatomic) NSString *name;
@property(strong, nonatomic) NSString *gender;
@property(assign, nonatomic) NSInteger age;

@end

@implementation Person

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if ([super init])
    {
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.gender = [aDecoder decodeObjectForKey:@"gender"];
        self.age = [aDecoder decodeIntegerForKey:@"age"];
    }
    return self;
}

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

@end

要注意的是如果归档类是某个自定义类的子类时,就需要在归档和解档之前先实现父类的归档和解档方法。即 [super encodeWithCoder:aCoder][super initWithCoder:aDecoder] 方法。

具体的存储和读写操作如下

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *url = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
    NSString *fileName = [url.path stringByAppendingPathComponent:@"person.data"];
    Person *yasic = [[Person alloc] init];
    yasic.name = @"Yasic";
    yasic.gender = @"Male";
    yasic.age = 22;
    [NSKeyedArchiver archiveRootObject:yasic toFile:fileName];
    
    Person *myYasic = [NSKeyedUnarchiver unarchiveObjectWithFile:fileName];
    _resultLabel.text = [NSString stringWithFormat:@"%@ %@ %ld", myYasic.name, myYasic.gender, myYasic.age];

主要是调用 (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path; 方法进行存储,调用 (nullable id)unarchiveObjectWithFile:(NSString *)path; 方法进行读取。

4. CoreData

CoreData 是基于 sqlite 的封装,最终将数据保存到一个数据库文件中,同时提供了 ORM 功能。数据最终的存储类型可以是:SQLite数据库,XML,二进制,内存里,或自定义数据类型。

其工作原理是

  • NSManagedObjectContext 临时数据库向 NSPersistentStoreCoordinator 持久化存储助理发送一个key(model 名字)
  • NSPersistentStoreCoordinator 通过这个 key 在 NSManagedObjectModel 数据模型中找到这个 model 对应的表
  • NSManagedObjectModel 将这个表名返回给 NSPersistentStoreCoordinator
  • NSPersistentStoreCoordinator 通过表名找到给表的 file 路径
  • NSPersistentStoreCoordinator 将这个路径返回给 NSManagedObjectContext
  • NSManagedObjectContext 对数据进行处理(增, 删 , 该, 查)

首先新建自己的 Data Model 文件,会生成一个 .xcdatamodeld 文件,在文件中加入 Entity,然后对 Entity 加入 attributes,设置名称和类型。然后要注意,在 Xcode8 中,默认的 NSManagedObject Subclass 是由类定义的,要自己定义需要把 Data Model Inspector 中的 Class Codegen 改成 none,然后选中 Data Model 文件,在 Editor 中选择 “Create NSManagedObject Subclass”,这样就将 Entity 变成了 oc 中的类,可以直接将类对应的对象与数据库中的表对应起来。

然后是对 context,coordinator 和 model 的初始化过程。

    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
    NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];//如果是nil则表示mainbundles
    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *fileName = [filePath stringByAppendingPathComponent:@"person.db"];
    [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:fileName] options:nil error:nil];
    context.persistentStoreCoordinator = coordinator;
    _manegeObjectContext = context;

context 直接初始化,model 会将应用程序包中的 model 合并起来,coordinator 依据 model 生成,然后加入数据库文件作为持久化库,还可以指定存储的类型。最后 context 持有 coordinator,coordinator 持有 model,就完成了初始化过程。

接下来是增删查改的具体步骤。

- (IBAction)add:(UIButton *)sender {
    Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:_manegeObjectContext];
    person.name = _nameField.text;
    person.gender = _genderField.text;
    person.age = [_ageField.text intValue];
    NSError *error = nil;
    [_manegeObjectContext save:&error];
    
    if (error)
    {
        NSLog(@"%@", error);
    }
}

这里 Person 就是通过 NSManagedObject Subclass 生成的类。

- (IBAction)delete:(UIButton *)sender {
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", _nameField.text];
    request.predicate = predicate;
    NSArray *results = [_manegeObjectContext executeFetchRequest:request error:nil];
    Person *resultPerson = results[0];
    [_manegeObjectContext deleteObject:resultPerson];
    NSError *error = nil;
    [_manegeObjectContext save:&error];
}

- (IBAction)query:(UIButton *)sender {
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", _nameField.text];
    request.predicate = predicate;
    NSError *error = nil;
    NSArray *results = [_manegeObjectContext executeFetchRequest:request error:&error];
    if (error)
    {
        NSLog(@"%@", error);
    }
    if ([results count] > 0)
    {
        Person *resultPerson = results[0];
        _resultLabel.text = [NSString stringWithFormat:@"%@ %@ %hd", resultPerson.name, resultPerson.gender, resultPerson.age];
    }
    else
    {
        _resultLabel.text = @"null";
    }
}

查询还支持模糊搜索和分页搜索。

- (IBAction)Update:(UIButton *)sender {
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", _nameField.text];
    request.predicate = predicate;
    NSArray *results = [_manegeObjectContext executeFetchRequest:request error:nil];
    Person *resultPerson = results[0];
    NSError *error = nil;
    resultPerson.age = [_ageField.text intValue];
    resultPerson.gender = _genderField.text;
    [_manegeObjectContext save:&error];
}

当然也可以对查询的数据先进行排序再返回

    NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];
    request.sortDescriptors = @[sort];

最后注意,如果程序已经编译运行过,再修改 Data Model 文件会报错

NSPersistentStoreCoordinator has no persistent stores.

解决办法是重新安装一次应用,或者找到.sqlite将其删掉。

5. FMDB

FMDB 是 SQLite 数据库框架,是对 SQLite 的 C 语言 API 的封装,对多线程操作进行了处理,是线程安全的。使用之前需要先导入 sqlite3.dylib 文件。

在 Linked Frameworks and Libraries 中加入 libsqlite3.tbd。

导入 sqlite3 的头文件

#import "sqlite3.h"

FMDB 有三个重要的类

  • FMDatabase:一个 FMDatabase 对象就代表一个单独的 SQLite 数据库(注意并不是表),用来执行 SQL 语句
  • FMResultSet:使用 FMDatabase 执行查询后的结果集
  • FMDatabaseQueue:用于在多线程中执行多个查询或更新,它是线程安全的

FMDB 需要使用 pod 添加依赖。

关于数据库文件的文件路径

  • 具体文件路径,如果不存在会自动创建
  • 空字符串@"",会在临时目录创建一个空的数据库,当FMDatabase连接关闭时,数据库文件也被删除
  • nil,会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁

初始化和创建表

    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
    _db = [FMDatabase databaseWithPath:path];
    if (![_db open])
    {
        NSLog(@"fail!");
    }
    BOOL result = [_db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_person (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, age integer NOT NULL, gender text NOT NULL);"];
    if (result)
    {
        NSLog(@"create successful.");
    }
    else
    {
        NSLog(@"create fail");
    }

- (void)addItem
{
    NSString *name = _name.text;
    NSString *gender = _gender.text;
    NSInteger age = [_age.text intValue];
    BOOL result = [_db executeUpdate:@"INSERT INTO t_person (name, age, gender) VALUES (?,?,?)",name, @(age), gender];
    if (result)
    {
        NSLog(@"successful %@", NSStringFromSelector(_cmd));
    }
    else
    {
        NSLog(@"fail %@", NSStringFromSelector(_cmd));
    }
}

- (void)queryItem
{
    FMResultSet *set = [_db executeQuery:@"select * from t_person where name = ?", _name.text];
    NSString *result = @"";
    NSString *temp = @"";
    _resultLabel.text = @"";
    while ([set next]) {
        NSInteger id = [set intForColumn:@"id"];
        NSString *name = [set objectForColumn:@"name"];
        NSString *gender = [set objectForColumn:@"gender"];
        NSInteger age = [set intForColumn:@"age"];
        temp = [NSString stringWithFormat:@"%ld %@ %@ %ld", id, name, gender, age];
        _resultLabel.text = temp;
        result = [NSString stringWithFormat:@"%@\n%@", result, temp];
    }
}

- (void)updateItem
{
    BOOL result = [_db executeUpdate:@"update t_person set age = ?, gender = ? where name = ?", _age.text, _gender.text, _name.text];
    if (result)
    {
        NSLog(@"successful %@", NSStringFromSelector(_cmd));
    }
    else
    {
        NSLog(@"error %@", NSStringFromSelector(_cmd));
    }
}

- (void)deleteItem
{
    BOOL result = [_db executeUpdate:@"delete from t_person where name = ?", _name.text];
    if (result)
    {
        NSLog(@"successful %@", NSStringFromSelector(_cmd));
    }
    else
    {
        NSLog(@"error %@", NSStringFromSelector(_cmd));
    }
}

队列操作

- (void)queueUpdateItem
{
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
    FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
    [queue inDatabase:^(FMDatabase *database)
    {
        [database executeUpdate:@"update t_person set age = ? where name = ?", @(age), @"yasic1"];
        [database executeUpdate:@"update t_person set age = ? where name = ?", @(age), @"yasic2"];
        [database executeUpdate:@"update t_person set age = ? where name = ?", @(age), @"yasic3"];
    }];
}

必须注意,在所有 FMDB 方法中传递的参数必须是 objectivec 对象,最典型的 NSInteger 是非 oc 对象,会发生运行时错误。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348

推荐阅读更多精彩内容