iOS数据的持久化

96
赤脊山的豺狼人
2016.02.16 16:19* 字数 2211

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

沙盒

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

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

iOS
Web note ad 1