iOS数据的持久化

因为豺狼也是在学习阶段, 可能会有一些纰漏, 还请各位看官无情指出, 如多少有些助益的话, 也请点个红心, 非常感谢~下面让我们开始吧!

沙盒

先理解一下沙盒机制, 简单说就是除了APP自己的目录外, 不允许你在其他地方存取数据. 整个沙盒目录下有三个子目录:Documents Library tmp.
Documents目录下的数据在连接iTunes时会进行同步, 适合存储重要数据, 如用户信息啥的.
Library目录下又有两个子目录Caches Preferences, 一个是缓存, 一个是应用设置信息.
Library/Caches目录存储体积大且不需要备份的数据, Library/Preferences目录保存的设置信息会在iTunes连接时同步.
tmp用来保存临时数据, 在应用关闭时候就自动删除掉了.
具体调取路径的代码如下:

NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *libPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;
NSString *cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString *preferPath = NSSearchPathForDirectoriesInDomains(NSPreferencePanesDirectory, NSUserDomainMask, YES).firstObject;
NSString *tmpPath = NSTemporaryDirectory();
NSLog(@"\ndocPath : %@\nlibPath : %@\ncachesPath : %@\npreferPath : %@\ntmpPath : %@", docPath, libPath, cachesPath, preferPath, tmpPath);

地址的输出结果

另外说一句, 这些都是保存在硬盘Data目录中, 和应用程序本身的资源文件和可执行文件是分开的, 后者在Bundle目录下.
代码如下:

NSString *path = [[NSBundle mainBundle] bundlePath];
NSLog(@"path : %@", path);
应用本身地址

数据持久化

所谓数据持久化就是将数据保存到硬盘, 以便于下次进入应用时快速调用. 一般的方法有如下几种:

  • property list (属性列表)
  • preference (偏好设置)
  • NSKeyedArchiver (加密形式)
  • SQLite3/FMDB (嵌入式数据库)
  • CoreData (面向对象的嵌入式数据库)
property list

plist文件是将特定对象, 通过XML方式保存到目录中. 可以被序列化的类型只有OC中的对象类型(String Array Dictionary Data Number).

// 创建地址
NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *plistPath = [docPath stringByAppendingPathComponent:@"PlistFile.plist"];
NSDictionary *dict = @{@"key1":@"value1", @"key2":@"value2"};
// 存储
BOOL ret = [dict writeToFile:plistPath atomically:YES];
if (!ret) {
    NSLog(@"写入失败");
}
// 读取
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
NSLog(@"\nresult : %@", result);
  • 在write方法里, 为了安全, atomically一般情况都是YES. 它表示是否需要先写入一个辅助文件, 再把辅助文件拷贝到目标文件地址.
  • 能够进行writeToFile的只有ArrayDictionary类型, 反序列化同样是调用arrayWithContentsOfFiledictionaryWithContentsOfFile.
  • XML序列化缺点也很明显, 一点是操作的对象有限, 另一点也是最为重要的一点, 保存方式为明文保存, 千万不要用来保存账号密码<手动滑稽>!

preference

NSUserDefaults, 简单好用却很low的方法,豺狼刚入坑时最爱用的方法...代码如下:

// 获取偏好设置文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
// 存储
[userDefaults setObject:@"this is object" forKey:@"object"];
[userDefaults setBool:YES forKey:@"man"];
[userDefaults setObject:@[@"1", @"2", @"3"] forKey:@"array"];
// 立即保存
[userDefaults synchronize];
// 读取
NSString *object = [userDefaults objectForKey:@"object"];
BOOL man = [userDefaults boolForKey:@"man"];
NSArray *array = [userDefaults objectForKey:@"array"];
NSLog(@"\nobject : %@\nman : %@\narray : %@", object, man?@"YES":@"NO", array);

另外一个NSUserDefaults比较少用却更直观没用过, 所以也列出来的方法:

  • 创建一个偏好设置文件:


    创建一个SettingBundle
  • 设置其中的Root.plist文件
    对偏好设置进行自定义
  • 在手机设置的最下面找到应用的偏好设置
屏幕快照 2016-02-14 16.01.42.png

用户在其中进行设置后, 相关设置的值可以通过预先定义好的Identifier来取到, 设置的内容也是保存在Library/Preferences目录下的. 代码如下:

NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
BOOL toggleSwitch = [userDefaults boolForKey:@"toggle_switch_ide"];
NSString *textfield = [userDefaults objectForKey:@"textfield_ide"];
CGFloat slider = [userDefaults floatForKey:@"slider_ide"];
NSLog(@"\ntogleSwitch : %@\ntextfield : %@\nslider : %f", toggleSwitch?@"YES":@"NO", textfield, slider);
  • 偏好设置一般是用来保存应用设置信息的, 最好不要在其中保存大量其他数据
  • 调用synchronize方法会进行立即保存, 否则系统会根据I/O不定时刻保存.
  • 偏好设置文件保存在Library/Preferences目录下, 以工程的Bundle Identifier为名的plist文件中.
  • 因为也属于XML序列化, 缺点同第一种方法.

NSKeyedArchiver

如果要针对更多的对象类型或者信息需要加密的话就需要使用NSKeyedArchiver归档, 它也是一种序列化的形式, 凡是遵守NSCoding协议的对象都可以进行NSKeyedArchiver归档. 代码如下:

创建自定义对象类型Person.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface Person : NSObject <NSCoding>

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

@end

Person.m
#import "Person.h"

@implementation Person

#pragma mark - NSCoding
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    // 解码
    if (self = [super init]) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.age = [aDecoder decodeIntegerForKey:@"age"];
        self.birthday = [aDecoder decodeObjectForKey:@"birthday"];
    }
    return self;
}

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

#pragma mark - description
- (NSString *)description {
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    formatter.dateFormat = @"yyyy-MM-dd";
    return [NSString stringWithFormat:@"\nname : %@\nage : %li\nbirthday : %@", self.name, self.age, [formatter stringFromDate:self.birthday]];
}

@end

自定对象建好后就是对其进行存储读取操作, 代码如下:

// 归档文件路径
NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
NSLog(@"path : %@", filePath);
// 实例化对象
Person *person = [[Person alloc] init];
person.name = @"韩梅梅";
person.age = 16;
person.birthday = [NSDate dateWithTimeIntervalSince1970:360000];
// 存储
[NSKeyedArchiver archiveRootObject:person toFile:filePath];
// 读取
Person *person2 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
NSLog(@"\nperson2 : %@", person2);
  • NSKeyedArchiver归档可以对多个对象进行归档, 在复杂归档时就需要针对不同对象设置对应Key. 不过用的并不多.
  • 保存文件的扩展名随意如: .torrent.
  • 归档对象必须遵守N�SCoding协议以及协议的initWithCoderencodeWithCoder方法.
  • 如果对自定义类的子类归档时, 需要先实现父类的编码解码方法, 即[super encodeWithCoder:aCoder][super initWithCoder:aDecoder].

SQLite3/FMDB

因为前几种方法属于覆盖式存储, 如果要改变其中某一条, 需要整体取出修改后再行归档. 相比较之前的几种方法, SQLite方便进行增删改查, 更适合存储读取大量数据内容.
因为入坑尚浅基础也弱, 豺狼对于使用C语言的SQLite3真的力不从心... 所以只研究了第三方数据库框架FMDB. 因为FMDB是用OC的方式对SQLite进行的封装, 所以相对于C语言而言, 更利于理解, 也更加轻便, 提升开发效率<手动滑稽>. GitHub-FMDB
因为FMDB是对SQLite的封装, 所以使用前先导入依赖库libsqlite3.0, 并将fmdb文件导入工程.

  • FMDatabase 执行SQL语句的主体.
  • FMResultSet 使用FMDatabase执行查询后的结果集.
  • FMDatabaseQueue 类似队列的作用.

具体使用代码如下:

// 数据库路径
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
// 在安全线程操作
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
[queue inDatabase:^(FMDatabase *db) {
    // 打开数据库
    if (![db open]) {
        NSLog(@"数据库打开失败");
    }
    // 创建表
    NSString *createSql = @"create table if not exists PersonList(id integer primary key autoincrement, name varchar, age interger)";
    if (![db executeUpdate:createSql]) {
        NSLog(@"创建表失败");
    }
    // 插入数据
    NSString *insertSql = @"insert into PersonList(name, age) values(?, ?)";
    // method1
    if (![db executeUpdate:insertSql, @"李雷", @14]) {
        NSLog(@"插入数据失败");
    }
    // method2
    if (![db executeUpdate:insertSql withArgumentsInArray:@[@"韩梅梅", @13]]) {
        NSLog(@"插入数据失败");
    }
    // 删除数据
    NSString *deleteSql = @"delete from PersonList where id%2 = 0";
    if (![db executeUpdate:deleteSql]) {
        NSLog(@"删除数据失败");
    }
    // 修改数据
    NSString *updateSql = @"update PersonList set name=?, age=? where id < 5";
    if (![db executeUpdate:updateSql, @"Jim", @25]) {
        NSLog(@"修改数据失败");
    }
    // 查询数据
    NSString *selectSql = @"select * from PersonList where id > 2";
    FMResultSet *result = [db executeQuery:selectSql];
    while (result.next) {
        NSString *name = [result stringForColumn:@"name"];
        NSInteger age = [result intForColumn:@"age"];
        NSLog(@"\nname:%@\nage:%li", name, age);
    }
    if (![db close]) {
        NSLog(@"关闭数据库失败");
    }
}];

在处理大量数据的时候, FMDB可以方便的添加事务, 代码如下:

NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
    // 打开数据库
    if (![db open]) {
        NSLog(@"数据库打开失败");
    }
    // 创建表
    NSString *createSql = @"create table if not exists PersonList(id integer primary key autoincrement, name varchar, age interger)";
    if (![db executeUpdate:createSql]) {
        NSLog(@"创建表失败");
    }
    NSString *insertSql = @"insert into PersonList(name, age) values(?, ?)";
    for (NSInteger i=0; i<500; i++) {
        if (![db executeUpdate:insertSql, [NSString stringWithFormat:@"name%li", i], @(1+i)]) {
            NSLog(@"添加数据失败");
            *rollback = YES;
        }
    }
    NSLog(@"添加数据成功");
}];
  • 所有的操作都是由FMDatabase来完成, 添加删除修改都是有executeUpdate方法完成, 查询由executeQuery方法完成, 为了数据安全最好在inDatabase里操作, 遇到大批量数据的时候在inTransaction里操作.
  • 在主线程处理大量数据的时候, 会造成堵塞, 最好使用多线程方法, 或者弹出一个alert显得更友好.
  • FMDB用到的闭包(block)需要注意强引用导致内存泄露.
  • 尽量使用FMDatabaseQueue, 防止多线程情况下造成的数据混乱.

至于SQL语句, 开发过程中移动端需要的逻辑本来就少, 豺狼也没用认真研究过, 反正单词别写错, 大多数情况都没啥问题~
另外说一句, 还是要花点时间研究SQLite3, 因为豺狼觉得它毕竟使用的是C语言, 在Swift中的迁移性好点.


CoreData

CoreData是iOS5之后苹果推出的, 继承了苹果API一如既往的繁琐难用...非常🐂B的一点是它的ORM(对象-关系映射)功能, 说简单点就是我们的Model和数据库可以互相转化了. CoreData本身不需要我们有任何SQL语法基础, 强大但是准备工作略繁琐.
本部分参考了MJ老师的《Core Data入门》
先来大概了解一下:

  • Data Model 模型文件, 描述应用中实体和实体属性, 文件类型为.xcdatamodeld.
    创建DataModel文件
  • Entity 实体, 相当于表, 点击Add Entity创建, 点击Attributes的'+'号创建实体属性, 点击Relationships的'+'号创建实体关联, 点击Fetched Properties的'+'创建抓取条件.
    创建实体和实体属性
  • NSManagedObject 数据库取出来的对象, 与NSDictionary类似, 使用KVC进行属性存取, 即setValue:forKey:valueForKey:, 一般我们创建它的子类文件进行具体的业务操作.
    创建NSManagerObject子类文件

    创建结果

使用CoreData前需要先导入CoreData.framework依赖库, 并#import <CoreData/CoreData.h>. 完成后进行具体操作, 首先搭建上下文环境, 代码如下:

// DataModel模型文件
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil]; // 传nil表示MainBundle
// 数据库地址
NSString *dbPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"CoreData.db"]; // 名字随意
NSURL *url = [NSURL fileURLWithPath:dbPath];
// 模型文件与数据库之间的持久化存储协调者
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// 数据库
NSError *error = nil;
NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error];
if (!store) {
    [NSException raise:@"添加数据库错误" format:@"%@", [error localizedDescription]];
}
// 操作的上下文
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
context.persistentStoreCoordinator = psc;

至此, 我们的数据库已经建好, 上下文环境也设置完成了.
NSPersistentStoreCoordinator作为协调者连接了模型文件和数据库, 是沟通的关键, 然后我们通过NSManagedObjectContext对这个协调者进行操作.
这里说一下, 在NSManagedObjectContext初始化的时候使用initWithConcurrencyType:, 单纯的init方法已经在iOS9中废弃了, 为了适配低于iOS9的系统, 参数使用NSPrivateQueueConcurrencyType, 另外两个NSPrivateQueueConcurrencyType表示私有线程, 不会阻塞主线程, NSMainQueueConcurrencyType表示主线程, 会堵塞主线程.

添加数据:

// 添加数据
// 实体对象赋值
Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:_context];
[person setValue:@"lilei" forKey:@"name"];
[person setValue:@15 forKey:@"age"];
Card *card = [NSEntityDescription insertNewObjectForEntityForName:@"Card" inManagedObjectContext:_context];
[card setValue:@"12345678910x" forKey:@"num"];
// 两表之间关联
[person setValue:card forKey:@"card"];
// 同步到数据库
NSError *error = nil;
if (![_context save:&error]) {
    [NSException raise:@"访问数据库错误" format:@"%@", [error localizedDescription]];
}

如果未建立NSManagedObject的子类, 则直接使用NSManagedObject即可.

查询与删除数据

// 查询数据
// 抓取请求
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:_context];
// 排列方式
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];
request.sortDescriptors = @[sort];
// 查询条件
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K < %@", @"age", @"20"];
request.predicate = predicate;
// 查询结果
NSError *error = nil;
NSArray *objs = [_context executeFetchRequest:request error:&error];
if (error) {
    [NSException raise:@"查询数据库错误" format:@"%@", [error localizedDescription]];
}
// 删除数据
// 输出结果
__weak typeof(self) weakSelf = self;
[objs enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    NSLog(@"name : %@, age : %@", [obj valueForKey:@"name"], [obj valueForKey:@"age"]);
    [weakSelf.context deleteObject:obj];
}];
if (![_context save:&error]) {
    [NSException raise:@"访问数据库错误" format:@"%@", [error localizedDescription]];
}

对于查询条件NSPredicate, %K是key path的替换值, %@是对应的值, 可以为数字日期等格式.
豺狼在网上找到了较为详细语法的介绍:

原帖: http://www.cocoachina.com/industry/20140321/8024.html

再介绍个从MJ老师那抄来的方法--打开CoreData的SQL语句输出开关:

1.打开Product,点击EditScheme...
2.点击Arguments,在ArgumentsPassed On Launch中添加2项
1> -com.apple.CoreData.SQLDebug
2> 1

会输出操作记录, 比较方便查看具体的操作.


至此, 豺狼花了三天时间学习整理的iOS数据持久化已经基本完成了, 参考了多位大神的教程, 查验了很多资料, 所以并非全部原创, 如果有侵权之处, 请及时联系我!
期间能想到的基本都写上了, 也许有些内容比较狭隘, 甚至有些地方理解的有误, 还请各位同仁多多交流指教! 如果对你有些许帮助请点个❤️, 关注一下豺狼, 感谢! <感激涕零>

关注豺狼的订阅号, 更新的新文章会第一时间收到通知~

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

推荐阅读更多精彩内容