注册

iOS本地数据持久化

在iOS开发中,有很多数据持久化的方案,本文章将介绍以下6种方案:



plist文件(序列化)

preference(偏好设置)

NSKeyedArchiver(归档)

SQLite3

FMDB

CoreData



沙盒


每个APP的沙盒下面都有相似目录结构,如图




16ccf78bd0f0e20229a3c4383f6d9266.png


下面的代码得到的是应用程序目录的路径,在该目录下有三个文件夹:Documents、Library、temp以及一个.app包!该目录下就是应用程序的沙盒,应用程序只能访问该目录下的文件夹!!!



NSString *path = NSHomeDirectory();




1、Documents 目录:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。该路径可通过配置实现iTunes共享文件。可被iTunes备份。


2、AppName.app 目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。


3、Library 目录:这个目录下有两个子目录:

Preferences 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好.

Caches 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。

可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。


4、tmp 目录:这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。该路径下的文件不会被iTunes备份。

// 获取沙盒主目录路径
NSString *homeDir = NSHomeDirectory();
// 获取Documents目录路径
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
// 获取Library的目录路径
NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
// 获取Caches目录路径
NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
// 获取tmp目录路径
NSString *tmpDir = NSTemporaryDirectory();

//获取应用程序程序包中资源文件路径的方法
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"apple" ofType:@"png"];
UIImage *appleImage = [[UIImage alloc] initWithContentsOfFile:imagePath];


plist文件(序列化)


可以被序列化的类型只有如下几种:

NSArray; //数组
NSMutableArray; //可变数组
NSDictionary; //字典
NSMutableDictionary; //可变字典
NSData; //二进制数据
NSMutableData; //可变二进制数据
NSString; //字符串
NSMutableString; //可变字符串
NSNumber; //基本数据
NSDate; //日期


数据存储与读取的实例:

/**
写入数据到plist
*/
- (void)writeToPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"写入数据地址%@",path);

NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
NSArray *array = @[@"123", @"王佳佳", @"iOS"];
//序列化,把数组存入plist文件
[array writeToFile:fileName atomically:YES];
NSLog(@"写入成功");
}

/**
从plist读取数据

@return 读出数据
*/
- (NSArray *)readFromPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"读取数据地址%@",path);

NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
//反序列化,把plist文件数据读取出来,转为数组
NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@"%@", result);
return result;
}



存储时使用writeToFile:atomically:方法。 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。这是更安全的写入文件方法,一般都写YES。



Preference(偏好设置)


Preference通常用来保存应用程序的配置信息的,一般不要在偏好设置中保存其他数据。


数据存储与读取的实例:

- (void)writeToPreference{

//1.获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向偏好设置中写入内容
[userDefaults setObject:@"wangjiajia" forKey:@"name"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];

NSString *path = NSHomeDirectory();
}

- (void)readFromPreference{
//获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

//读取偏好设置
NSString *name = [userDefaults objectForKey:@"name"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
}


使用偏好设置对数据进行保存,它保存的时间是不确定的,会在将来某一时间自动将数据保存到 Preferences 文件夹下,如果需要即刻将数据存储,使用 [defaults synchronize]。



Preference(偏好设置)plist文件(序列化)都是保存在 plist 文件中,但是plist文件(序列化)操作读取时需要把整个plist文件都进行读取,而Preference(偏好设置) 可以直接通过 key-value单个读取。



归档解归档


要使用归档,其归档对象必须实现NSCoding协议



NSCoding协议声明的两个方法都必须实现。

encodeWithCoder:用来说明如何将对象编码到归档中。

initWithCoder:用来说明如何进行解档来获取一个新对象。



数据存储与读取的实例:

/**
归档
*/
- (void)keyedArchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [[Person alloc] init];
person.name = @"wangjiajia";
[NSKeyedArchiver archiveRootObject:person toFile:file];
}

/**
解档
*/
- (void)keyedUnarchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
if (person) {
NSLog(@"name:%@",person.name);
}
}


SQLite3


以下代码块将会介绍sqlite3.h中主要的API。

/* 打开数据库 */
int sqlite3_open(
const char *filename, /* 数据库路径(UTF-8) */
sqlite3 **pDb /* 返回的数据库句柄 */
);

/* 执行没有返回的SQL语句 */
int sqlite3_exec(
sqlite3 *db, /* 数据库句柄 */
const char *sql, /* SQL语句(UTF-8) */
int (*callback)(void*,int,char**,char**), /* 回调的C函数指针 */
void *arg, /* 回调函数的第一个参数 */
char **errmsg /* 返回的错误信息 */
);

/* 执行有返回结果的SQL语句 */
int sqlite3_prepare_v2(
sqlite3 *db, /* 数据库句柄 */
const char *zSql, /* SQL语句(UTF-8) */
int nByte, /* SQL语句最大长度,-1表示SQL支持的最大长度 */
sqlite3_stmt **ppStmt, /* 返回的查询结果 */
const char **pzTail /* 返回的失败信息*/
);

/* 关闭数据库 */
int sqlite3_close(sqlite3 *db);


处理SQL返回结果的一些API

#pragma mark - 定位记录的方法
/* 在查询结果中定位到一条记录 */
int sqlite3_step(sqlite3_stmt *stmt);
/* 获取当前定位记录的字段名称数目 */
int sqlite3_column_count(sqlite3_stmt *stmt);
/* 获取当前定位记录的第几个字段名称 */
const char * sqlite3_column_name(sqlite3_stmt *stmt, int iCol);
# pragma mark - 获取字段值的方法
/* 获取二进制数据 */
const void * sqlite3_column_blob(sqlite3_stmt *stmt, int iCol);
/* 获取浮点型数据 */
double sqlite3_column_double(sqlite3_stmt *stmt, int iCol);
/* 获取整数数据 */
int sqlite3_column_int(sqlite3_stmt *stmt, int iCol);
/* 获取文本数据 */
const unsigned char * sqlite3_column_text(sqlite3_stmt *stmt, int iCol);


由于其他API相对来说比较简单,这里就只给出执行有返回结果的SQL语句的实例

/* 执行有返回值的SQL语句 */
- (NSArray *)executeQuery:(NSString *)sql{
NSMutableArray *array = [NSMutableArray array];
sqlite3_stmt *stmt; //保存查询结果
//执行SQL语句,返回结果保存在stmt中
int result = sqlite3_prepare_v2(_database, sql.UTF8String, -1, &stmt, NULL);
if (result == SQLITE_OK) {
//每次从stmt中获取一条记录,成功返回SQLITE_ROW,直到全部获取完成,就会返回SQLITE_DONE
while( SQLITE_ROW == sqlite3_step(stmt)) {
//获取一条记录有多少列
int columnCount = sqlite3_column_count(stmt);
//保存一条记录为一个字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < columnCount; i++) {
//获取第i列的字段名称
const char *name = sqlite3_column_name(stmt, i);
//获取第i列的字段值
const unsigned char *value = sqlite3_column_text(stmt, i);
//保存进字典
NSString *nameStr = [NSString stringWithUTF8String:name];
NSString *valueStr = [NSString stringWithUTF8String:(const char *)value];
dict[nameStr] = valueStr;
}
[array addObject:dict];//添加当前记录的字典存储
}
sqlite3_finalize(stmt);//stmt需要手动释放内存
stmt = NULL;
NSLog(@"Query Stmt Success");
return array;
}
NSLog(@"Query Stmt Fail");
return nil;
}


在使用数据库存储时主要存在以下步骤。



1、创建数据库

2、创建数据表

3、数据的“增删改查”操作

4、关闭数据库



在使用sqlite3时,数据表的创建及数据的增删改查都是通过sql语句实现的。下面是一些常用的SQL语句。



创建表:

create table 表名称(字段1,字段2,……,字段n,[表级约束])[TYPE=表类型];

插入记录:

insert into 表名(字段1,……,字段n) values (值1,……,值n);

删除记录:

delete from 表名 where 条件表达式;

修改记录:

update 表名 set 字段名1=值1,……,字段名n=值n where 条件表达式;

查看记录:

select 字段1,……,字段n from 表名 where 条件表达式;



FMDB


FMDB是一种第三方的开源库,FMDB就是对SQLite的API进行了封装,加上了面向对象的思想,让我们不必使用繁琐的C语言API函数,比起直接操作SQLite更加方便。


FMDB主要是使用以下三个类


FMDatabase : 一个单一的SQLite数据库,用于执行SQL语句。

FMResultSet :执行查询一个FMDatabase结果集。

FMDatabaseQueue :在多个线程来执行查询和更新时会使用这个类。



一般的FMDB数据库操作4个:


创建数据库

打开数据库、关闭数据库

执行更新的SQL语句

执行查询的SQL语句



创建数据库

/**
创建数据库
*/
- (void)createDatabase{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"FMDB.db"];
NSLog(@"数据库路径:%@",filePath);
/**
1. 如果该路径下已经存在该数据库,直接获取该数据库;
2. 如果不存在就创建一个新的数据库;
3. 如果传@"",会在临时目录创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
4. 如果传nil,会在内存中临时创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
*/
self.database = [FMDatabase databaseWithPath:filePath];
}


打开关闭数据库

/* 打开数据库,成功返回YES,失败返回NO */
- (BOOL)open;
/* 关闭数据库,成功返回YES,失败返回NO */
- (BOOL)close;


执行更新的SQL语句

在FMDB里除了查询操作,其他数据库操作都称为更新。而更新操作FMDB给出以下四种方法。

/* 1. 直接使用完整的SQL更新语句 */
[self.database executeUpdate:@"insert into mytable(num,name,sex) values(0,'wangjiajia1','m');"];

NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
/* 2. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要后面的参数进行替代 */
[self.database executeUpdate:sql,@1,@"wangjiajia2",@"m"];

/* 3. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要数组参数里面的参数进行替代 */
[self.database executeUpdate:sql
withArgumentsInArray:@[@2,@"wangjiajia3",@"m"]];

/* 4. SQL语句字符串可以使用字符串格式化,这种我们应该比较熟悉 */
[self.database executeUpdateWithFormat:@"insert into mytable(num,name,sex) values(%d,%@,%@);",4,@"wangjiajia4",@"m"];


执行查询的SQL语句

查询方法与更新方法类似,只不过查询方法存在返回值,可以通过返回值给出的相应方法获取需要的数据。

/* 执行查询SQL语句,返回FMResultSet查询结果 */
- (FMResultSet *)executeQuery:(NSString*)sql, ... ;
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... ;
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;


执行查询语句实例如下

- (NSArray *)getResultFromDatabase{
//执行查询SQL语句,返回查询结果
FMResultSet *result = [self.database executeQuery:@"select * from mytable"];
NSMutableArray *array = [NSMutableArray array];
//获取查询结果的下一个记录
while ([result next]) {
//根据字段名,获取记录的值,存储到字典中
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
int num = [result intForColumn:@"num"];
NSString *name = [result stringForColumn:@"name"];
NSString *sex = [result stringForColumn:@"sex"];
dict[@"num"] = @(num);
dict[@"name"] = name;
dict[@"sex"] = sex;
//把字典添加进数组中
[array addObject:dict];
}
return array;
}


多线程安全FMDatabaseQueue

由于在多线程同时操作FMDatabase对象时,会造成数据混乱的问题,FMDB提供了一个可以确保线程安全的类(FMDatabaseQueue)。

FMDatabaseQueue的使用比较简单

//创建多线程安全队列对象
self.queue = [FMDatabaseQueue databaseQueueWithPath:filePath];
//在block块内自行相关数据库操作即可
[self.queue inDatabase:^(FMDatabase * _Nonnull db) {
}];


事务

事务,是指作为单个逻辑工作单元执行的一系列操作,要么完整地执行,要么完全地不执行。


比如要更新数据库的大量数据,我们需要确保所有的数据更新成功,才采取这种更新方案,如果在更新期间出现错误,就不能采取这种更新方案了,这就是事务的用处。只有事务提交了,开启事务期间的操作才会生效。

以下是事务的例子

//事务
-(void)transaction {
// 开启事务
[self.database beginTransaction];
BOOL isRollBack = NO;
@try {
for (int i = 0; i<500; i--) {
NSNumber *num = @(i+6);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [self.database executeUpdate:sql,num,name,sex];
if ( !result ) {
NSLog(@"插入失败!");
isRollBack = YES;
return;
}
}
}
@catch (NSException *exception) {
isRollBack = YES;
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
@finally {
if (!isRollBack) {
NSLog(@"插入成功,事务提交");
//事务提交
[self.database commit];
}else{
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
}
}

//多线程安全事务实例
- (void)transactionByQueue {
//开启事务
[self.queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
BOOL isRollBack = NO;
for (int i = 0; i<500; i++) {
NSNumber *num = @(i+1);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [db executeUpdate:sql,num,name,sex];
if ( !result ) {
isRollBack = YES;
return;
}
}

//当最后*rollback的值为YES的时候,事务回退,如果最后*rollback为NO,事务提交
*rollback = isRollBack;
}];
}



CoreData




b9ac439bd9e6354f521eca9d39e1d5ac.png


CoreData核心结构图.png


以下是CoreData常用类的作用描述



PersistentObjectStore:存储持久对象的数据库(例如SQLite,注意CoreData也支持其他类型的数据存储,例如xml、二进制数据等)。

ManagedObjectModel:对象模型,对应Xcode中创建的模型文件。

PersistentStoreCoordinator:对象模型和实体类之间的转换协调器,用于管理不同存储对象的上下文。

ManagedObjectContext:对象管理上下文,负责实体对象和数据库之间的交互。



CoreData主要工作原理如下



读取数据库的数据时,数据库数据先进入数据解析器,根据对应的模板,生成对应的关联对象。

向数据库插入数据时,对象管理器先根据实体描述创建一个空对象,对该对象进行初始化,然后经过数据解析器,根据对应的模板,转化为数据库的数据,插入数据库中。

更新数据库数据时,对象管理器需要先读取数据库的数据,拿到相互关联的对象,对该对象进行修改,修改的数据通过数据解析器,转化为数据库的更新数据,对数据库更新。



CoreData的使用步骤如下



1.添加框架。

2.数据模板和对象模型。

3.创建对象管理上下文。

4.数据的增删改查操作。



在其中第二步的时候要注意,Xcode 8.0 之后和之前的Xcode 版本是有一些区别的。在 8.0之后创建.xcdatamodeld文件之后,添加实体之后会自动生成对应的对应类文件。

0 个评论

要回复文章请先登录注册