iOS数据存储
1. 概论
在iOS开发中数据存储的方式可以归纳为两类: 存储文件 和 存储到数据库.
2.文件存储
2.1 沙盒
- 文件下载思路:
客户端发送请求->服务器响应,返回NSData->客户端接受数据; - 沙盒机制(sandbox):每个iOS应用都有自己的应用沙盒,即文件系统目录.属于封闭式的,所有APP都在单独的沙盒中运行;为了:1⃣️完美的用户体验需要对跨应用程序进行整合统一;2⃣️封闭跨应用可以保证系统的安全性;(iOS8以后开放了几个固定的系统区域,例如第三方图片编辑,第三方输入法等)
应用沙盒一般包括:
- Documents:保存应用运行时生成需要的持久化数据iTunes会备份.
- tmp:保存应用运行时所需的临时数据.应用完毕会删除.不会备份
- library的Cache:保存应用运行时生成的需要持久化的缓存数据,不自动删除.一般存储体积大、不需要备份的非重要数据
- Library的Preferences:保存应用偏好设置.会备份.
获取沙盒路径:
- 获取根路径:NSHomeDirectory()的返回值;
- 获取tmp路径:NStempoaryDirectory();
- 获取Library/Preference:通过NSUserDefaults类存取该目录下的设置信息
- 获取library/Cache:同Documents.
- 获取Documents路径:
方式1---使用字符串拼接=>根目录路径加上Documents; stringByAppendingPathComponent:@"Documents" // 不建议采用,因为新版本的操作系统可能会修改目录名
方式2:搜索模式:(主要)
代码如下:
在某个范围搜索摸个文件路径 函数:NSSearchPathForDirectoriesInDomains:(参数directory,参数domainMask,参数rxpandTitle)
// 1. directory 表示获取哪个文件夹目录
// 2. domainMask 表示查找范围
// 3. rxpandTitle 表示是否展开波浪号
例:获取Documents路径.
NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; // 在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素,所以下标为0;
// 最后 writeToFile 即可
注意:
- iOS 8.0 之后,出于安全考虑, 将沙盒路径和mainBundle路径进行了分离.
- 写进文件时要对操作文件名和路径进行 stringByAppendingPathComponent 拼接
2.2 文件数据存储与读取方式:
-
plist存储(XML 属性列表) - 基本对象类型--数据
- plist 文件的 根节点 只有 NSArray 和 NSDictionary 两种数据类型,其它基本对象类型类型 可以通过 NSArray 和 NSDictionary 间接存储到 plist 文件
- 使用
writeToFile: atomically:
方法直接将对象写到属性列表文件中
-
preference存储 (偏好设置:保存用户名、字体大小、是否自动登录) - 键值对
- 通过 [NSUserDefaults standardUserDefaults] 获取 NSUserDefaults 对象,这个对象专门用来做偏好设置存储.
- 存储:
[[NSUserDefaults standardUserDefaults] setObject:@"hm" forKey:@"account"];
- 读取:
NSString *account = [[NSUserDefaults standardUserDefaults] objectForKey:@"account"];
- 偏好设置注意点: iOS8之前, 通常还需要做一个同步操作, 就是把缓存数据同步到硬盘当中[defaults synchornize];
-
NSKeyedArchiver 归档(转换成二进制数据存储到闪存中) - 对象:遵守NSCoding自定义对象类型和基本对象类型--用户详细信息
- 保存:
[NSKeyedArchiver archiveRootObject:person toFile:path];
path = NSSearchPathForDirectoriesInDomains xxx
- 遵守NSCoding协议,协议有2个方法:encodeWithCoder:会在归档时调用;initWithCoder:会在解档时调用.
- 在归档方法中需要指定如何归档对象中的对应实例变量,
- (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_name forKey:NameKey]; }
- 在解档方法中指定如何解码文件中的数据为对象的实例变量,
- (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super init]) _name = [aDecoder decodeObjectForKey:NameKey]; return self;
-
注意解档时,如果父类也遵守了NSCoding协议.则需要
[super initWithCoder]
.特别的,UIView实现了NScoding协议方法.加载解析storyboard/xib时会使用[super initWithCoder]方法.
- 保存:
3. 数据库存储
可以通过SQL直接访问数据库,也可以通过ORM进行对象关系映射访问数据库。这两种方式恰恰对应iOS中SQLite和Core Data的内容,在此将重点进行分析:
3.1 SQLite 关系数据库
SQLite是目前主流的嵌入式关系型数据库;特点是:轻量级,跨平台.
- 基于C语言开发的轻型数据库.
- 需要设置一个自增的id,主键PK: 唯一区别数据库上内容 ,网络数据库会查询当前可用的id, 查到之后建立排它锁, 读取数据完成会打开排它锁 ; 自动增长的id 生成是数据库来负责,程序员无需关心!
- SQLite是采用的动态数据类型,即使创建时定义一种类型,在实际操作时也可以存储其他类型.不过不推荐
- 建立连接后移动端通常使用持久化连接开发数据库;(跟网络连接不同);
在iOS中操作SQLite数据库可以分为以下几步(注意先在项目中导入libsqlite3框架):
- 打开数据库,利用sqlite3_open()打开数据库会指定一个数据文件保存路径,如果 文件存在则直接打开,否则创建并打开; 打开数据库会得到一个sqlite3类型的对象,后面需要借助这个对象进行其他操作,称为句柄。
- 执行SQL语句,(包括有返回值和无返回值语句).
- SQL语句推荐使用从文件加载,这样不需要
- 对于无返回值语句(增删改),直接通过sqlite3_exec()函数执行;
- 对于有返回值语句(查),则首先通过sqlite3_prepare_vc2()进行sql语句评估(语法检测),然后通过sqlite3_step()依次取出查询结果的每一行数据,对于每行数据都可以通过对应的sqlite3_colimn_类型()方法获得对应类的数据,如此反复循环直到遍历完成.最后释放句柄.
数据库大量操作问题
如果一次向数据库中插入大量数据,十分耗时,如何解决:
耗时是因为SQLite数据库操作中,如果不显示开启事务,那么每一条数据库操作指令,都会隐式打开/提交一次事务; 而事务的开启和关闭时内存操作, 比较耗时;
解决办法就是在操作之前主动开启事务,在操作结束,提交事务即可 ; 验证效果可以使用CACurrentMediaTime() //取绝对时间观察耗时
事务是一个执行单元,表示这个单元内,要么都执行成功,要么都失败;
注意: 手动开启事务之后,执行单元功能也需要我们手动执行: 在事务开启期间,只要有一个操作出现错误,就回滚事务到初始状态;
[CATransaction begin]; //开启事务
for i in 10000
{
xxx;
if xxx { [CATransition rollBack] } //判断如果操作失败回滚
}
[CATransaction commit]; //关闭事务
在整个操作过程中无需管理数据库连接,对于嵌入式SQLite操作是持久连接(尽管可以通过sqlite3_close()关闭),不需要开发人员自己释放连接。纵观整个操作过程,其实与其他平台的开发没有明显的区别,较为麻烦的就是数据读取,在iOS平台中使用C进行数据读取采用了游标的形式,每次只能读取一行数据,较为麻烦。因此实际开发中不妨对这些操作进行封装:
1.定义一个sqlite3类型属性句柄使用它对数据库操作:
@property (nonatomic) sqlite3 *database;
2.打开数据库
//打开或创建并打开数据库(一般保存到沙盒Documents目录中)
NSString *directory=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *filePath=[directory stringByAppendingPathComponent:dbname];
//注意filePath需要转换为C的字符串才可做此函数参数,XCode7beat5之后不需要加UTF8String转了
//返回true表示打开成功
return SQLITE_OK == sqlite3_open(filePath, &_database)
- 3.操作数据库: 单步执行sql语句插入,修改,删除
//插入,修改,删除只是sql执行语句不同,步骤相同
// 从bundle的 .sql文件中加载 sql语句:
NSString *path = [[NSBundle mainBundle] pathForResource : "db.sql" ofType:nil];
NSString * sql = [NSString contensOfFile:path];
/ *
参数
1. 数据库全局句柄
2. 要执行的 SQL
3. callback,执行完成 SQL 之后,调用的 C 语言函数指针,通常传入 nil
4. 第三个参数 callback,函数参数的地址,通常传入 nil
5. 错误信息,有其他方式获取执行情况,通常传入 nil
返回值 如果 == SQLITE_OK 表示成功
*/
return sqlite3_exec(_database, sql, NULL, NULL, &error) == SQLITE_OK
- 4.操作数据库:查询 - 返回数据
// 1. 预编译 SQL 检查语法正确性
/**
参数
1. 全局数据库句柄
2. 要执行 SQL 的 C 语言的字符串
3. 要执行 SQL 的以字节为单位的长度,但是,如果传入 -1,SQLite 框架会自动计算
4. STMT - 预编译的指令句柄
- 后续针对`本次查询`所有操作,全部基于此句柄
- 必须注意的,句柄一定要释放
- 编译完成后,可以理解为一个临时的数据集合,通过 step 函数,能够顺序获取其中的结果
5. 关于 STMT 尾部参数的指针,通常传入 NULL
返回值
如果编译成功,表示 SQL 能够正常执行,返回 SQLITE_OK
*/
COpaquePointer *stmt;
if (SQLITE_OK != sqlite3_prepare_v2(_database, sql, -1, &stmt, NULL)) { NSLog(@"SQL错误"); sqlite3_finalize(stmt);
return nil
}
//
//创建字典数组
NSMutableArray *rows=[NSMutableArray array];//数据行
//单步执行sql语句,获得ROW 对应一条完整记录
while (SQLITE_ROW == sqlite3_step(stmt)) {
int columnCount = sqlite3_column_count(stmt); //记录查询列数
// 创建单条记录的字典
NSMutableDictionary *dic=[NSMutableDictionary dictionary];
for col in 0..<cols {
// 1> 列名 Int8 / CChar / Byte
const char *name cName = sqlite3_column_name(stmt, col)
// 2> 数据类型
let type = sqlite3_column_type(stmt, col)
const unsigned char *value;
switch type {
case SQLITE_FLOAT: // 小数
value = sqlite3_column_double(stmt, col);
case SQLITE_INTEGER: // 整数
value = Int(sqlite3_column_int64(stmt, col));
case SQLITE3_TEXT: // 字符串
// 记录 C 语言的字符串
value = sqlite3_column_text(stmt, i);
case SQLITE_NULL: // 空值,一般数据库中允许字段为 nil,但是 OC 的字典不能插入 nil
value = NSNull() // NSNull 就是专门向字典和数组中插入控制使用的
default:
print("不支持的数据类型")
}
dic[[NSString stringWithUTF8String:name]]=[NSString stringWithUTF8String:(const char *)value]; //xcode7之后无需转换UTF8
}
[rows addObject:dic]
}
//释放句柄
sqlite3_finalize(stmt);
//返回数据
return rows;
一般数据是从网络读取的,但是考虑缓存问题,通常会选择将微博数据保存到本地; 实际开发中并不会在控制器中直接调用数据操作方法, 在这里引入一个 Service ,操作数据库的访问服务层; 进行数据的增删该查,由于访问层不需要过多的设置,所以定义成单例,保证程序只有一个即可;在其中将对数据库的操作转换为对模型的操作
Core Data
概述
当前,各类应用开发中只要牵扯到数据库操作通常都会用到一个概念“对象关系映射(ORM)”;iOS中ORM框架首选Core Data; ORM框架的作用就是将 关系数据库中的 表 转换为程序中的 对象,所以其本质还是对数据库的操作,例如: Core Data中如果存储类型配置为SQLite则本质还是操作的SQLite数据库;
上面代码中我们已经可以将数据库操作转换为了对象操作,服务层中的方法中已经将这些操作封装起来;但是操作过程比较复杂:首先手动创建数据库,其次再手动创建模型和访问层;
上述的数据映射到实体的过程完全是手动的;Core Data 就是为了解决这个问题而产生的;
使用Core Data 进行数据存取并不需要手动创建数据库,这个过程完全由Core Data 框架帮我们完成, 开发人员面对的是模型, 只需把模型创建起来, 具体数据库如何创建则不用管;
步骤:
- 在项目中添加Data Model 模型文件,并在其中创建实体和关系:
* **注意: 实体对象不需要创建ID主键** , Attributes中应该是有意义属性 ( 创建过程中应该考虑对象需要的属性 , 而不是数据库中表有几个字段 ;)
* 所有属性应该指定具体类型,因为实体对象会对应 生成ObjC模型类.
* 实体对象中 其他实体对象类型的属性(类似模型嵌套) 应该通过Relationships建立, 并且注意实体之间的对应关系,(例如: 一个用户用多条微博, 而一条微博则只属于一个用户)
- 根据创建好的模型文件(.xcdatamodeld文件) 生成具体的实体类, 在Xcode中添加"NSManagedObject Subclass"文件,按照步骤要选择创建的模型及实体,Xcode就会根据所创建的模型生成具体的实体类;
- 通过模型生成的类过程相当简单,不需要手动维护,如果模型有变化,重新生成即可;
- 注意: 从此方法创建的类都几次与NSMagagedObject,每个NSManagedObject对象对应着数据库中一条记录
- 集合属性()会生成访问此属性的分类方法;
- 使用@dynamic 代表 数据属性实现, 具体实现细节不用关心
- 最后Core Data的使用 ;
Core Data 的使用;
要使用Core Data完成数据的存取,先了解一下Core Data几个核心类:
- Persistent Object Store : 可以理解为 存储持久对象的数据库(例如:SQLite;Core也支持其他类型的数据存储:如xml,二进制数据等)
- Managed Object Model: 对象模型 , 对应Xcode中创建的模型文件.
- Persistent Store Coordinator: 对象模型和实体类之间的转换协调器, 用于管理不同存储对象的上下文.
- Managed Object Context: 对象管理上下文, 负责实体对象和数据库之间的交互 ;
Core Data使用起来相对直接使用SQLite3的C语言API而言更加面向对象,
操作步骤如下:
1.创建管理的上下文 :
-(NSManagedObjectContext *)createDbContext{
NSManagedObjectContext *context;
//1. 加载模型文件, 参数为nil表示打开包中所有模型文件并合成一个
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil]
//创建解析器
NSPersistentStoreCoordinator *sc = [ [NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model];
//2. 指定保存路径
NSString *dir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *path=[dir stringByAppendingPathComponent:@"myDatabase.db"];
NSURL *url=[NSURL fileURLWithPath:path];
//3. 添加SQLite持久存储到解析器
[sc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error];
if (error) {
NSLog(@"数据库打开失败!错误:%@",error.localizedDescription);
} else {
//4. 创建管理对象上下文,指定器存储
context = [NSManagedObjectContext new];
context.persistentStoreCoordinator = sc;
}
return context ;
}
经过这几个步骤后可以得到管理对象上下文 NSManagedObjectContext , 如果第一次创建上下文,Core Data还会自动创建存储文件 (这里使用SQLite3数据库),并根据模型对象创建对应的表结构 ; 为了方便使用
2.查询数据:
- 对于有条件的查询,Core Data通过谓词来实现: 创建请求 -> 设置请求条件 -> 调用上下文执行请求;
-(void)addUserWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{
//添加一个对象
User *us= [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:self.context];
us.name=name;
us.screenName=screenName;
us.profileImageUrl=profileImageUrl;
us.mbtype=mbtype;
us.city=city;
NSError *error;
//保存上下文
if (![self.context save:&error]) {
NSLog(@"添加过程中发生错误,错误信息:%@!",error.localizedDescription);
}
}
-
如果有多个条件, 只要使用谓词组合即可, 那么对于关联对象怎么查询呢?需要分两种情况:
a. 查找一个对象只有唯一一个关联对象的情况, (如一条微博只能属于一个用户) ;通过keypath查询:-(NSArray *)getStatusesByUserName:(NSString *)name{ NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: @ "Status"]; request.predicate = [NSPredicate predicateWithFormat:@"user.name=%@",name]; NSArray *array=[self.context executeFetchRequest:request error:nil]; return array;
b. 查找一个对象有多个关联对象的情况,例如查找发送微博内容中包含"Watch"并且User为"小娜"的用户(一个用户有多条微博),此时使用 谓词 过滤;
-(NSArray *)getUsersByStatusText:(NSString *)text screenName: (NSString *) screenName { NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Status"] ; request.predicate = [NSPredicate predicateWithFormat:@"text LIKE '*Watch*'",text]; NSArray *statues = [self.context executeFetchRequest:request error:nil]; MSPredicate *userPredicate = [NSPredicate predicateWithFormat:@"user.screenName=%@",screenName]; NSArray *users = [statues filteredArrayUsingPredicate:userPredicate]; return users; } //如果单纯查找微博中包含"Watch"的用户,直接查出对应的微博,然后通过每个微博的user属性即可获得用户组即可;
增删改数据
插入数据 需要调用实体描述对象 NSEnityDescription返回一个实体对象, 然后设置对象属性,最后保存当前的上下文即可; 这里需要注意,增删改操作完最后必须调用管理上下文的保存方法,否则操作不会执行(同ios绘图)
//添加一个对象
User *us= [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:self.context];
us.name=name;
us.screenName=screenName;
us.city=city;
//删除一个对象
//[self.context deleteObject:user];
//修改一个数据
//User *us=[self getUserByName:name]; //根据name获取对象
//修改属性
//us.name=name1;
//us.screenName=screenName1;
//us.city=city1;
NSError *error;
//最后都需要 - 保存上下文
if (![self.context save:&error]) {
NSLog(@"添加过程中发生错误,错误信息:%@!",error.localizedDescription);
}
调试
虽然Core Data操作(如果使用SQLite数据库)最终转换为SQL操作,但是调试起来却不如SQL那么方便,因为看不到最终生成的SQL语句, 可以在Xcode设置: Product-Scheme-Edit Scheme-Run-Arguments中的passed On launch中依次添加两个参数(注意参数顺序不能错):-com.apple.CoreData.SQLDebug和1 ;
之后运行程序过程中如果操作了SQL语句就打印在输出面板; 还有一点注意: 如果模型发生变化,此时可以重新生成实体类文件,但是所生成的数据库并不会自动更新,需要重新生成;
FMDB框架
相对比与SLQite3来说Core Data存在着诸多优势,它面向对象,开发人员不必过多的关系更多的数据库操作知识,但是其本身也有一些限制,例如: 不能跨平台,ORM框架都存在性能问题(最终要转换SQL操作); 所以最好是对SQL进行封装--FMDB;
1. 使用
1.FMDB既然是对于libsqlite3框架的封装,自然使用起来也是类似的,使用前也要打开一个数据库,这个数据库文件存在则直接打开否则会创建并打开。这里FMDB引入了一个FMDatabase对象来表示数据库,打开数据库和后面的数据库操作全部依赖此对象。下面是打开数据库获得FMDatabase对象的代码:
-(void)openDb:(NSString *)dbname{
//取得数据库保存路径,通常保存沙盒Documents目录(如果此参数设为nil会默认在内存中创建数据库,如果设为@""则会在沙盒中的临时目录创建)
NSString *directory=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSLog(@"%@",directory);
NSString *filePath=[directory stringByAppendingPathComponent:dbname];
//创建FMDatabase对象
self.database=[FMDatabase databaseWithPath:filePath];
//打开数据
if ([self.database open]) {
NSLog(@"数据库打开成功!");
// 执行数据库操作
//1.更新
//[FMDatabase executeUpdate:error:withArgumentsInArray:orVAList:]
//2. 查询
FMResultSet *rs = [database executeQuery:@"要执行的sql语句"];
while([rs next]){
// 提取查询数据
NSString *rsData = [rs stringForColumn:@"first name"];
}
//关闭数据库
[database close]
}else{
NSLog(@"数据库打开失败!");
}
}
2.在FMDB中FMDatabase类提供了两个方法executeUpdate:和executeQuery:分别用于执行无返回结果的更新和有返回结果的查询。唯一需要指出的是,如果调用有格式化参数的sql语句时,格式化符号使用“?”而不是“%@”、等。(是为了安全)
多线程中FMDB应用.
直接使用libsqlite3进行数据库操作其实是线程不安全的, 因此在多线程中需使用FMDatabaseQueue对象, 相比FMDatabase而言,他是线程安全的;
//创建 , 最好放在一个单例中:
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:apath];
//使用
[queue inDatabase:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
FMResultSet *rs = [db excuteQuery:@"select * from foo"];
while ([rs next]){
//......
}
}];
//如果要支持事务:
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
if (whoopsSomethingWrongHappened) {
*rollback = YES;
return;
}
}];