data-persistence

Sandbox(沙盒机制)

iOS中得沙盒机制(sandbox)是一种安全体系,它规定了应用程序只能在为该应用程序创建的文件夹内读取文件,不可以访问其他地方的内容。所有的非代码文件都保存在这个地方,如图片,声音,属性列表和文本文件等。

  • 每个应用程序都在自己的沙盒内
  • 不能随意跨越自己的沙盒去访问别的应用程序的沙盒内容
  • 应用程序向外请求或接受数据都需要经过权限认证

    一个沙盒中包含四部分

    • .app文件,即可运行的应用文件;
    • Document,苹果建议将程序创建或程序浏览的文件数据保存在该目录下,iTunes备份和恢复时会包括该目录;
    • Library,存储程序的默认设置或其它状态信息;
    • Library/Caches,存放缓存文件,iTunes不会备份此目录,此目录下的文件不会在应用退出删除;
    • tmp,创建和存放临时文件的地方,iTu不会备份此目录。

代码获取沙盒路径的方法

  1. 获取根目录

    NSString *homePath = NSHomeDirectory();

  2. 获取Document目录
    NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES)[0];

  3. 获取Cache目录
    NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask, YES)[0];

  4. 获取Library目录
    NSString *libPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask, YES)[0];

  5. 获取tmp目录
    NSString *tmpPath = NSTemporaryDirectory();

iOS数据持久化技术

数据持久化既是,能将内存中的数据模型转换为存储模型,并能在将来需要时将存储模型还原为数据模型的一种机制。

说明 : 通俗的讲,也就是将数据保存在非易失性的设备中,并且能在需要时恢复。针对苹果设备来说,就是从闪存到内存的过程。

iOS开发中数据持久化的方法

  • Row File APIs(C语言的文件操作,iOS的NSFilemanager)
  • NSUserDefaults (默认保存文件在对应的程序包sandbox的目录下的library/Preferences)
  • Plist(属性列表)
  • NSCoding + Archiver&Unarchiver (对象归档)
  • SQLite (数据库)
  • FMDB (对SQLite的封装)

@property (nonatomic, strong) NSString *filePath;
@property (nonatomic, strong) UITextField *textField;

#define kFileName                 @”test.txt”


ROW APIs

C语言文件操作### ROW APIs

  1. 创建文件路径
1
2
3
4
5
6
//创建文件存放路径(一般需要保存的文件存放在sandbox的Document目录下)
- (void)setupPath
{
NSString *documentDirectory = NSSearchPathForDirectorieInDomains(NSDocumentDirectory,NSUserDomainMask,YES)[0];
self.filePath = [documentDirectory stringByAppendingPathComponent:kFileName];
}
  1. 文件的写入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)saveData
{
// oc文件路径转化为c
const char *filePath = [_filePath UTF8String];
// 打开文件
FILE *fp = fopen(filePath, "w+");
if (NULL == fp) {
perror("fopen");
return;
}
// 将_textFiled的内容写到文件
const char *content = [_textField.text UTF8String];
size_t size = fwrite(content, BUFSIZE, 1, fp);
fclose(fp);
if (size > 0) {
 NSLog(@"Saved data successfully");
}
}
  1. 文件的读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)loadData
{
// 文件路径
const char *filePath = [_path UTF8String];
NSLog(@"%s", filePath);
// 打开文件
FILE *fp = fopen(filePath, "r");
if (fp == NULL) {
perror("fopen");
return;
}
//读取文件内容到内存
char buf[BUSIZ] = {0};
//获取文件大小
fseek(fp,SEEK_END);
long size = ftell(fp);
fread(fp,size,1,buf);
//赋值给_textField
NSString *str = [NSString stringWithUTFString:str];
if(str != NULL && ![str isEqualToString:@""]);{
_textField.text = str;
}
fclose(fp);
}

OC NSFileManager文件管理器操作

  1. 创建文件路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//创建文件存放路径(一般需要保存的文件存放在sandbox的Document目录下)
- (void)setupPath
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *documentDirectory = NSSearchPathForDirectorieInDomains(NSDocumentDirectory,NSUserDomainMask,YES)[0];
//创建文件目录
NSString *test = [documentDirectory stringByAppendingPathComponent:@"test"];
[fileManager createDirectoryAtPath:test withIntermediateDirectories:YES attributes:nil error:nil];
self.filePath = [documentDirectory stringByAppendingPathComponent:kFileName];
NSString *content = nil;
if(![fileManager fileExistsAtPath:self.filePAth]){
[fileManager createFileAtPath:self.filePath contents:[content dataUsingEncoding:NSUTF*String] attributes:nil];
}
}
  1. 文件的写入
1
2
3
4
5
6
7
8
9
10
- (void)saveData
{
NSError *error;
[self.textField.text writeToFile:self.filePath atomically:YE];
if(error){
NSLog(@"Error : %@",error);
return;
}
NSLog(@"save data successfully");
}
  1. 文件的读取
1
2
3
4
5
6
7
8
9
10
11
- (void)loadData
{
NSError *error;
NSString *content = [[NSString alloc]initWithContentsOfFile:self.filePath encoding:NSUTF*String error:&error];
if(error){
NSLog(@"Error : %@",error);
return;
}
self.textField.text = content;
NSLog(@"load data successfully");
}

NSUserDefaults

>

  • 直接使用原始的文件操作API,不管是C语言的还是OC的都不太方便
  • Cocoa会为每个app自动创建一个数据库,用来存储App本身的偏好设置,如:开关
    值,音量值之类的少量信息

  • NSUserDefaults使用时用 [NSUserDefaults standardUserDefaults] 接口获取单例对象

  • NSUserDefaults本质上是以Key-Value形式存成plist文件,放在App的 Library/Preferences目录下
  • 这个文件是不安全的,所以千万不要用NSUserDefaults来存储密码之类的敏感信息,用户名和密码应该使用KeyChains来存储
  1. 文件的写入
1
2
3
4
5
6
7
8
9
10
11
12
- (void)saveData
{
NSUserDefaults *userDefaults = [NSUserDefaults
standardUserDefaults];
float progress = [self.progressTextField.text floatValue];
[userDefaults setFloat:progress forKey:@"progress"];
[userDefaults setObject:self.inputTextField.text
forKey:@"input"];
database
// keeps the in-memory cache in sync with a user’s defaults
[userDefaults synchronize];
 }
  1. 文件的读取
1
2
3
4
5
6
7
8
9
10
11
- (void)loadConfig
{
NSUserDefaults *userDefaults = [NSUserDefaults
standardUserDefaults];
self.toggle.on = [userDefaults boolForKey:@"toggle"];
self.progressView.progress = [userDefaults
floatForKey:@"progress"];
self.progressTextField.text = [NSString stringWithFormat:@"%.2f",
self.progressView.progress];
self.inputTextField.text = [userDefaults stringForKey:@"input"];
}
**说明:** *对NSUserDefaults单例对象的操作,实质上还是对PList文件 (Library/Preferences/<Application BundleIdentifier>.plist)的读写,只是Apple帮我们封装好了 读写方法。*

Plist

>

  • NSUserDefaults只能读写Library/Preferences/.plist这个 文件
  • PList文件是XML格式的,只能存放固定数据格式的对象
  • PList文件支持的数据格式有NSString, NSNumber, Boolean, NSDate, NSData, NSArray,和NSDictionary。其中,Boolean格式事实上以[NSNumber numberOfBool:YES/NO];这样的形式表示。NSNumber支持float和int两种格式。
  1. 创建文件路径
1
2
3
4
5
6
- (void)setUpPlist
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *documentDirectory = NSSearchPathForDirectorieInDomains(NSDocumentDirectory,NSUserDomainMask,YES)[0];
self.filePath = [documentDirectory stringByAppendingPathComponent:@"test.plist"];
}
  1. 写入plist文件
1
2
3
4
5
6
7
8
9
- (void)saveData
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[@"textField"] = _textField.text;
if (![dict writeToFile:_path atomically:YES]) {
NSLog(@"Error!!!");
return;
}
}
  1. plist文件的读取
1
2
3
4
5
6
7
8
9
- (void)loadData
{
NSMutableDictionary *dict = [NSMutableDictionary
dictionaryWithContentsOfFile:_path];
NSString *content = dict[@"textField"];
 if (content && content.length > 0) {
_textField.text = content;
 }
}

Archiver&Unarchiver

>

  • NSUserDefaults和Plist文件支持常用数据类型,但是不支持自定义的数据对象
  • Cocoa提供了NSCoding和NSKeyArchiver两个工具类,可以把我们自定义的对象编码 成二进制数据流,然后存进文件里面
  1. NSCoding协议


1
2
3
4
5
6
7
8
9
10
11
//解档,解码。解档之后会生成一个该类的对象(解码后对模型的属性赋值)
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
self.name = [aDecoder decodeObjectForKey:kNameKey];
self.age = [aDecoder decodeIntForKey:kAgeKey];
self.studyID = [aDecoder decodeObjectForKey:kStudyIDKey];
}
return self;
}
1
2
3
4
5
6
7
8
9
10
11
//归档,编码 (将模型的属性编码)
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
self.name = [aDecoder decodeObjectForKey:kNameKey];
self.age = [aDecoder decodeIntForKey:kAgeKey];
self.studyID = [aDecoder decodeObjectForKey:kStudyIDKey];
}
return self;
}
----
  1. 保存数据
1
2
3
4
5
6
7
8
9
- (void)saveData{
_student = [[YMStudent alloc] init];
_student.name = _name.text;
_student.age = [_age.text intValue];
_student.studyID = _studyID.text;
if ([NSKeyedArchiver archiveRootObject:_student toFile:self.filePath]) {
 NSLog(@"Archive successfully!");
}
}
  1. 读取数据
1
2
3
4
5
6
7
8
9
- (void)loadData{
_student = [NSKeyedUnarchiver unarchiveObjectWithFile:self.filePAth];
if (student != nil) {
_name.text = student.name;
_age.text = [@(student.age) stringValue];
_studyID.text = student.studyID;
 NSLog(@"Archive successfully!");
}
}

SQLite

SQLite

SQLite shell command

SQLite shell command

SQLite usage

创建数据库连接对象: sqlite3

创建预编译语句对象:sqlite3_stmt

  1. 打开数据

    sqlite3_open()

  2. 将SQL语句转换为预编译语句对象

    sqlite3_prepare_v2()

  3. 执行预编译语句,每次处理一次,不需要返回值的语句(如INSERT,UPDATE,DELETE),只需要执行该函数即可

    sqlite3_step()

  4. 获取数据库中得不同类型的值

    • sqlite3_column_blob()
    • sqlite3_column_bytes()
    • sqlite3_column_bytes16()
    • sqlite3_column_count()
    • sqlite3_column_double()
    • sqlite3_column_int()
    • sqlite3_column_int64()
    • sqlite3_column_text()
    • sqlite3_column_text16()
    • sqlite3_column_type()
    • sqlite3_column_value()
  5. 销毁有sqlite3_prepare_v2()函数创建的预处理语句对象

    sqlite3_finalize()

  6. 关闭数据库(即销毁数据库连接对象)

    sqlite3_close()

    FMDB

    >

FMDB Document

Download FMDB

FMDB数据库操作类对sqlite3的操作进行了便利的封装并保证了多线程下的安全地操作数据库

FEMDB有三个主要的类
  1. FMDatabase - 表示一分单独的SQLite数据库,用来执行SQLite的命令
  2. FMResultSet - 表示FMDatabase执行查询结果集
  3. FMDatabaseQueue - 在多线程中执行多个查询或更新使用该类是线程安全的

数据库的创建

#define kDBFileName      @”database.sqlite”

NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES)[0];
NSString *DBPath = [docPath stringByAppendingPathComponent:kDBFileName];
FMDatabase *database = [FMDatabase databaseWithPath:DBPath];

打开数据库

if(![database open]){
    NSLog(@"Open database failed !");
    return;
}

执行更新

  • executeUpdate

一切不是SELECT命令的命令都是为更新。包括CREATE,UPDATE,INSERT,ALTER等。

执行结果返回一个BOOL值。YES表示成功,NO表示失败。可以调用 -lastErrorMessage和 -lastErrorCode方法获取更多的信息。

执行查询

  • executeQuery

执行结果返回FMResultSet对象,失败返回nil。同样可以调用-lastErrorMessage和 -lastErrorCode方法获取更多的信息。获得的FMResultSet对象rs后,既是只有一条记录,一样使用[rs next];

eg: FMResultSet *rs = [db executeQuery:@"SELECT Name, Age, FROM PersonList"];
while ([rs next]) {
    NSString *name = [rs stringForColumn:@"Name"];
    int age = [rs intForColumn:@"Age"];
}
FMResultSet根据类型提取数据
- objectForColumnName:
- longForColumn:
- nlongLongIntForColumn:
- boolForColumn:
- doubleForColumn:
- stringForColumn:
- dateForColumn:
- dataForColumn:
- dataNoCopyForColumn:
- UTF8StringForColumnName:


以上方法,都有个{type}ForColumnIndex:版本,根据column的位置提取数据

有些时候,只是需要query某一个row里特定的一个数值(比如只是要找John的年龄),FMDB 提供了几个比较简便的方法。这些方法定义在FMDatabaseAdditions.h,如果要使用,记得先 import进来

//找地址
NSString *address = [db stringForQuery:@"SELECT Address FROM PersonList WHERE Name = ?",@"John”];
NSString *address = [db stringForQuery:@"SELECT Address FROM PersonList WHERE Name = ?",@"John”];
//找年齡
int age = [db intForQuery:@"SELECT Age FROM PersonList WHERE Name     = ?",@"John”];

关闭数据库

  • [FMDatabase close];

数据库的批量操作

使用FMDatabase 的executeStatements:或者executeStatements:withResultBlock:(是否需 要返回结果)

NSString *sql = @"create table bulktest1 (id integer primary key autoincrement, x text);"
"create table bulktest2 (id integer primary key autoincrement, y text);"
"create table bulktest3 (id integer primary key autoincrement, z text);"
"insert into bulktest1 (x) values ('XXX');"
"insert into bulktest2 (y) values ('YYY');"
"insert into bulktest3 (z) values ('ZZZ');";
success = [database executeStatements:sql];

或者

sql = @"select count(*) as count from bulktest1;"
}];
"select count(*) as count from bulktest2;"
"select count(*) as count from bulktest3;";
success = [self.db executeStatements:sql withResultBlock:^int(NSDictionary *dictionary) {
NSInteger count = [dictionary[@"count"] integerValue];
XCTAssertEqual(count, 1, @"expected one record for dictionary %@",
dictionary);
return 0;
}];

参数绑定

**INSERT INTO myTable VALUES (?, ?, ?)**    

问号只是占位,执行操作可以使用NSArray, NSDictionary, or a va_list来匹配参数
你也可以选择使用命名参数语法:INSERT INTO myTable VALUES (:id, :name, :value)
参数名必须以冒名开头。SQLite本身支持其他字符($,@),Dictionary key的内部实现是冒号 开头。注意你的NSDictionary key不要包含冒号

NSDictionary *argsDict = @{@"name":@"Jason"};
[db executeUpdate:@"INSERT INTO myTable VALUES (:name)"withParameterDictionary:argsDict];

FMDatabaseQueue 及线程安全

不能使⽤用同⼀个FMDatabase在不同线程中操作,多线程的操作是通过FMDatabaseQueue实现

首先创建队列,然后把单任务包装到事务里,串行执行

FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
[queue inDatabase:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
FMResultSet *rs = [db executeQuery:@"select * from foo"];
while([rs next]) {
...
}

事务的回滚:(当前的队列的操作的取消)

[queue inTransaction:^(FMDatabase *db, BOOL *rollback) { 
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
if (whoopsSomethingWrongHappened) {
}
*rollback = YES;
return;
// etc...
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber
numberWithInt:4]];
}];

FMDatabaseQueue会在同一个队列里 同步执行任务, GCD也会按它接收的块的顺序来执行

关于Setting Bundles

>

  • Setting Bundle的概念更多地应该是在App的配置选择上
  • Setting Bundle可以给用户提供一个从《设置》应用里去配置应用程序的方式
  • 从开发者的角度来看,一般需要频繁修改的配置选项,如游戏的音量和控制选项等最好 放到app内部的设置页里,而类似于邮箱应用中的邮件地址和服务器的设置等不需要频 繁更改的配置项可以放到Setting Bundle里
  • 从《设置》应用中进行设置,实际上是操作iOS配置系统中的应用程序域(Application Domain),是持久的

iOS的配置系统中存在如下一些域,将来查询时严格按照如下列出域的顺序进行查找

Domain State
NSArgumentDomain volatile(易失的)
Application(Identified by the app’s identifier) persistent(持久的)
NSGlobalDomain persistent
Languages(Identified by the language names) volatile
NSRegisterationDomain volatile

registerDefaults:方法是在NSRegistrationDomain域上进行配置的,所以仅仅是存在于 内存中的,易失的