iOS

iOS 本地数据存储

Posted by LOLITA0164 on November 1, 2016

iOS数据存储特性

sandbox沙盒

每个 iOS 应用程序都有自己的应用沙盒,可以将沙盒简单的理解为应用的文件夹,每个应用的沙盒都是相互独立的,其他应用不能访问该沙盒,他也不能访问其他应用的沙盒。

沙盒的结构与用途

  • Documents:保存应用运行时生成的需要持久化的数据,iTunes同步设备时会备份该目录。重要数据
  • Library/Caches:保存应用运行时生成的需要持久化的数据,iTunes同步设备不会备份该目录。非重要数据
  • Library/Preference:保存应用的偏好设置,iTunes同步设备时会备份该目录。
  • tmp:保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统也可能会清除该目录下的文件。iTunes同步设备时不会备份该目录。

沙盒目录的获取方式

Documents文件夹的获取方式

// 利用沙盒根目录拼接字符串
NSString *homePath = NSHomeDirectory();
NSString *docPath = [homePath stringByAppendingString:@"/Documents"];
// 利用沙盒根目录拼接”Documents”字符串
NSString *homePath = NSHomeDirectory();
NSString *docPath = [homePath stringByAppendingPathComponent:@"Documents"];
// NSDocumentDirectory 要查找的文件
// NSUserDomainMask 代表从用户文件夹下找
// 在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [path stringByAppendingPathComponent:@"xxx.plist"];

这里的参数说明一下: 第一个参数:枚举类型,常用的有

  • NSDocumentDirectory(Documents文件夹)
  • NSCachesDirectory(Library/Caches)

第二个参数:NSUserDomainMask,表示从用户文件夹下找。 第三个参数:打印的路径会是这种形式~/Documents,我们一般都会用YES,这样可以获取完整路径字符串。 这个方法的返回值是一个数组,但在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素,所以我们取第一个元素

Library/Caches文件夹的获取方式

NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [path stringByAppendingPathComponent:@"student.data"];

tmp文件夹的获取方式

NSString *tmp= NSTemporaryDirectory();

Library/Preference文件夹的获取方式 通过 NSUserDefaults 类存取该目录下的设置信息

查看沙盒

Window -> Devices/Simulators -> 选择你的Devices或者Simulators -> 选中App -> ⚙️ -> Download Container -> 右击显示包内容

最终你可以看到以下内容:

沙盒

注:你也可以将其数据文件修改后,使用 Replace Container 进行替换。


几种存储方式

一、plist

plist 全称叫 property list,属性列表,是一种基于 xml 形式的文件,可存储的数据类型有:NSArray 和 NSDictionary,这两种集合类型可存储任意一种可序列化的数据类型,如NSArray、NSDictionary、NSString、BOOL、NSNumber等数据类型,但是不可以存储自定义对象,即使该自定义对象实现了 Coding 协议,如果想存储自定义对象,需要将其转化为 NSData 类型再进行存储,但本质上已经不是存储自定义对象了。

代码操作 plist 文件

示例:

plist文件的归档

NSDictionary* dic = @{
                      @"name":@"LOLITA0164",
                      @"age":@(10)
                      };
NSString* path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString* filePath = [path stringByAppendingPathComponent:@"info.plist"];
[dic writeToFile:filePath atomically:YES];

plist文件的解档

NSDictionary* dic2 = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSLog(@"%@",dic2);

我们查看沙盒中的文件以及文件信息:

沙盒中新增的文件

沙盒中的文件

双击打开,查看其中的文件信息

文件信息

我们使用其他文本编辑器来查看

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>age</key>
	<integer>10</integer>
	<key>name</key>
	<string>LOLITA0164</string>
</dict>
</plist>

这些就是完整的文本信息,我们可以看到,plist 实质上是一种 xml 形式的文件。

补充:除了通过上述直接查看文件信息,你也可以读取成 NSString 类型输出,也同样可以看到文件完整内容。

NSString* string = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
NSLog(@"%@",string);

输出结果:

输出结果

注:plist 中只存储一种文件类型,要么是 NSArray 类型,要么是 NSDictionary 类型,当你想要操作存储的数据时,需要先读取后,在进行操作。

手动操作 plist 文件

除了我们通过系统代码的形式操作 plist 文件,我们也可以直接在项目中创建 plist 文件:

手动创建

使用方式就和操作项目中自带的 Info.plist 一样,也不用多说。这种本地存放文件的应用情景很多,比如一些地区的文件,包括省、市、县等信息

二、文件存储

通过 plist 存储的示例,我们可以看出,起到关键性作用的方法是 writeToFile:atomically: ,我们知道,NSString 和 NSData 同样有这两种方法,但是这两种存储和 NSFileManager 存储是一样的,存储的是二进制数据,即使你命名为 xxx.plist。

示例:

// NSData* data = [@"xiaoming" dataUsingEncoding:NSUTF8StringEncoding];
// [data writeToFile:filePath atomically:YES];

NSString* content = @"LOLITA0164";
[content writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
    
NSString* string = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
NSLog(@"\n%@",string);

输出:

输出结果

关于文件存储以及管理可以参考这篇 iOS 关于文件操作 NSFileManager


三、NSUserDefaults

NSUserDefaults 是一种存储用户偏好的存储方式,例如保存用户信息,对应用的设置等信息。

NSUserDefaults 可以存储的数据类型:NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary。如果想要存储其他类型,可转换为前面的类型再使用 NSUserDefaults 存储。

简单使用一:存储字符串

如果想要将字符串永久保存到NSUserDefaults中去,只需要简单的操作(一个Value 一个Key ),对相同的Key赋值约等于一次覆盖,要保证每一个Key的唯一性。

//将NSString 对象存储到 NSUserDefaults 中
NSString *passWord = @"1234567";
NSUserDefaults *user = [NSUserDefaults standardUserDefaults];
[user setObject:passWord forKey:@"userPassWord"];
//取出数据
NSUserDefaults *user = [NSUserDefaults standardUserDefaults];
NSString *passWord = [ user objectForKey:@"userPassWord"];

简单使用二:可变数组

NSUserDefaults 存储的对象全是不可变的,例如,如果我想要存储一个 NSMutableArray 对象,必须先转换成不可变数组(NSArray)再存储,同样,取出来的也是不可变数组,要使用可变数组需要再次转换。

//存储数组
NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:@"123",@"234", nil];
NSArray *array = [NSArray arrayWithArray:mutableArray];
    
NSUserDefaults *user = [NSUserDefaults standardUserDefaults];
[user setObject:array forKey:@"记住存放的一定是不可变的"];
NSUserDefaults *user = [NSUserDefaults standardUserDefaults];
//转换成可变数组
NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:[user objectForKey:@"记住存放的一定是不可变的"]];

简单使用三:非上述类型则需要转化成上述类型,例如图片

//存数据
UIImage *image=[[UIImage alloc]initWithContentsOfFile:@"photo.jpg"];
NSData *imageData = UIImageJPEGRepresentation(image, 1);//UIImage对象转换成NSData
[[NSUserDefaults standardUserDefaults] setObject:imageData forKey:@"image"]
//用synchronize方法把数据强行持久化到standardUserDefaults数据库中,因为NSUserDefaults存储数据不是及时的
[[NSUserDefaults standardUserDefaults] synchronize];
//取数据
NSData *imageData = [[NSUserDefaults standardUserDefaults] dataForKey:@"image"];
UIImage *Image = [UIImage imageWithData:imageData];//NSData转换为UIImage

简单使用四:自定义对象

存储的数据如果是对象的话,可以将自定义对象类型通过归档转化成NSData类型再进行存储,此时,该对象模型需要遵守NSCoding协议,并且要实现实现encodeWithCoder和initWithCoder方法,前者是将对象进行编码,后者用来解码。

以Student类为例。 首先 .h 文件中要继承NSCoding协议,.m文件如下:

-(void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:self.name forKey:@"name"];
}

-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    self = [super init];
    if (self) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
    }
    return self;
}

存储一个学生信息

//存数据
Student *student = [[Student alloc] ini];
//下面进行的是对student对象的 name , studentNumber ,sex 的赋值
student.name = @"LOLITA0164";
student.age = @"24";
//将student类型变为NSData类型
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:student];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"oneStudent"];

存储多个数据时候,可以使用for循环,将所有的学生信息存储到一个可变数组中,再可变数组转化成不可变数组,最后使用归档转化成NSData类型进行存储

NSArray * array = [NSArray arrayWithArray:dataArray];
[[NSUserDefaults standardUserDefaults] setObject:array forKey:@"allStudent"];
// 取数据
NSdData *data = [[NSUserDefaults standardUserDefaults] objectForKey:@"allStudent"];
NSArray *allStudentArray = [NSKeyedUnarchiver unarchiveObjectWithData:data];

总之,上面的示例无非就是将数据源转换为可存储的类型,然后使用NSUserDefaults 的实例通过 setObject:forKey: 的形式将数据存到偏好设置中。

当你第一次使用 NSUserDefaults 时,系统会创建一个 Bundle Identifier 为名称的 plist 文件,我们可以在 Library/Preferences 文件下可以看到该文件:

偏好设置文件

可以看出,NSUserDefaults 实质上是存储了一个字典类型的数据,这一点从 setObject:forKey: 就可以看出。因此,当我们操作 NSUserDefaults,实际上就是系统创建的 plist 文件的操作。

四、NSKeyedArchiver/NSKeyedUnarchiver

NSKeyedArchiver/NSKeyedUnarchiver 是一种将数据序列化和反序列化的工具。当使用列化和反序列化时,需要确保数据类型遵循 NSCoding 协议,实现 encodeWithCoder:initWithCoder: 方法,前者告诉 NSKeyedArchiver 如何进行编码,后者告诉 NSKeyedUnarchiver 如何解码。

通过 NSKeyedArchiver ,我们不仅可以将数据序列化成 NSData,也可以直接存储到指定文件。

支持的数据类型:一切遵循了 NSCoding 协议的数据类型,如NSDictionary。当然,最重要的时,我们可以实现自定义数据模型的本地持久化。

示例:

Student* s = [Student new];
s.name = @"LOLITA0164";
NSString* path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString* filePath = [path stringByAppendingPathComponent:@"info.txt"];

// 写入文件
[NSKeyedArchiver archiveRootObject:s toFile:filePath];

// 读取数据
Student* s2 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
NSLog(@"%@",s2.name);

注:

1、通过 NSKeyedArchiver 方式存储的数据,你需要通过对应的 NSKeyedUnarchiver 方式读取,因为数据是通过特殊的编码方式进行编码的。

2、归档的形式来保存数据,只能一次性归档保存以及一次性解压。所以只能针对小量数据,而且对数据操作比较笨拙,即如果想改动数据的某一小部分,还是需要解压整个数据或者归档整个数据。

归档形式只适合小数量,通常都是全部存或者全部取的业务中,如果是设计增删改查等操作,还是使用数据库要更好。

五、SQLite

SQLite 是一个轻量级的关系型数据库。iOS 中使用SQLite时,需要导入 libsqlite3.tbd 库。下面演示 SQLite 简单的数据库操作。

#import <sqlite3.h>
// 定义数据库
{
    sqlite3 *db;
}

首先我们需要设置数据库地址

// 获取数据库路径
NSString* path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString* filePath = [path stringByAppendingPathComponent:@"student.sqlite"];

创建数据库

// 创建数据库
int result = sqlite3_open(filePath.UTF8String, &db);
if (result == SQLITE_OK) {
    NSLog(@"打开数据库成功.");
    // 创表学生表
    const char *sql = "create table if not exists t_student (id integer primary key autoincrement, name text, age integer);";
    char *errorMesg = nil;
    //sqlite3_exec()可以执行任何SQL语句,比如创表、更新、插入和删除操作
    int result = sqlite3_exec(db, sql, nil, nil, &errorMesg);
    if (result == SQLITE_OK) {
        NSLog(@"成功创建t_student表");
    } else {
        NSLog(@"创建 t_student失败:%s",errorMesg);
    }
}
else{
    NSLog(@"打开数据库失败.");
}

插入数据

for (int i=0; i<10; i++) {
    NSString* name = [NSString stringWithFormat:@"LOLITA-%i",i];
    NSInteger age = i+1;
    NSString* sql = [NSString stringWithFormat:@"insert into t_student (name,age) values ('%@',%ld);",name,age];
    char *errorMesg = nil;
    int result = sqlite3_exec(db, sql.UTF8String, nil, nil, &errorMesg);
    if (result == SQLITE_OK) {
        NSLog(@"添加数据成功");
    }else {
        NSLog(@"添加数据失败:%s",errorMesg);
    }
}

查询数据

const char *sql = "select id, name, age from t_student";
// 结果集
sqlite3_stmt *stmt = nil;
int result = sqlite3_prepare_v2(db, sql, -1, &stmt, nil);
if (result == SQLITE_OK) {
    NSLog(@"查询语句合法");
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        NSInteger index = sqlite3_column_int(stmt, 0);
        const unsigned char *sname = sqlite3_column_text(stmt, 1);
        NSString* name = [NSString stringWithUTF8String:(const char *)sname];
        NSInteger age = sqlite3_column_int(stmt, 2);
        NSLog(@"%ld name:%@ age:%ld",index, name, age);
    }
} else {
    NSLog(@"查询语句非法");
}

查询结果:

结果

修改数据

// 修改数据
NSString *sql = @"update t_student set name = 'xiaoming' where age = 6";
char *errorMesg = nil;
int result = sqlite3_exec(db, sql.UTF8String, nil, nil, &errorMesg);
if (result == SQLITE_OK) {
    NSLog(@"更改成功");
}else {
    NSLog(@"更改失败");
}

修改结果:

修改结果

删除数据

// 删除数据
NSString *sql = @"delete from t_student where age <= 3";
char *errorMesg = nil;
int result = sqlite3_exec(db, sql.UTF8String, nil, nil, &errorMesg);
if (result == SQLITE_OK) {
    NSLog(@"删除成功");
}else {
    NSLog(@"删除失败");
}

删除结果:

删除结果

同样的方法,我们可以从沙盒中取出数据库进行查看:

数据库

使用数据库工具打开,查看数据

查看数据

六、FMDB等三方库

原生的 iOS 的 API 在操作的时候,需要使用 C 语言的函数,对于熟悉 OC 这种面向对象的语言的小伙伴,非常的不友好,于是 FMDB 就出现了。

FMDB 是一款简洁、易用的针对 libsqlite3 的封装库。

FMDB 中重要的类

  • FMDatabase/FMDatabaseQueue

    FMDatabase 是一个数据库操作对象类,可以用来执行 SQL 语句,FMDatabaseQueue 则是针对数据安全的,该对象只有一个串行队列,以保证线程是安全的。

  • FMResultSet

    查询的数据结果集

说明:

1、一切不是 SELECT SQL命令视为更新操作,使用 executeUpdate: 执行操作,SELECT 使用 excuteQuery: 执行查询操作。

2、调用 lastErrorMessage lastErrorCode 方法来得到错误信息。

创建数据库

#import "FMDB.h"	// 导入FMDB
@property (strong ,nonatomic) FMDatabase* db; // 创建数据库操作对象
// 获取文件路径
NSString* filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
// 拼接上文件名
filePath = [filePath stringByAppendingPathComponent:@"student.db"];

// 创建数据库
self.db = [FMDatabase databaseWithPath:filePath];
// 打开数据库执行 SQL 命令
if ([self.db open]) {
    NSLog(@"数据库打开成功。");
    BOOL success = [self.db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_student (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(10) NOT NULL, age INTEGER DEFAULT 1)"];
    if (success) {
        NSLog(@"创建表成功。");
    }
    else{
        NSLog(@"创建表失败。");
    }
}
else{
    NSLog(@"数据库打开失败。");
}

插入数据

for (int i=10; i<20; i++) {
    NSString* name = [NSString stringWithFormat:@"LOLITA_%d",i];
    BOOL success = [self.db executeUpdate:@"INSERT INTO t_student(name, age) VALUES (?, ?);",name,@(i)];
    if (success) {
        NSLog(@"数据插入成功。");
    }else{
        NSLog(@"数据插入失败。");
    }
}

查询数据

executeQuery:方法会返回一个 FMResultSet 类型的数据结果集,对该结果集使用 next 方法可以访问每一条数据记录(即使你只取一条记录,也需要调用 next)。

FMResultSet 获取不同类型数据格式的方法有:

  • intForColumn:
  • intForColumnIndex: // 下同

  • longForColumn:
  • longLongIntForColumn:
  • boolForColumn:
  • doubleForColumn:
  • stringForColumn:
  • dateForColumn:
  • dataForColumn:
  • dataNoCopyForColumn:
  • UTF8StringForColumn:
  • objectForColumn:
FMResultSet* result = [self.db executeQuery:@"SELECT * FROM t_student"];
while ([result next]) {
    NSInteger ID = [result intForColumn:@"id"];
    NSString* name = [result stringForColumn:@"name"];
    NSInteger age = [result intForColumnIndex:2];
    NSLog(@"id:%ld, name:%@, age:%ld",ID, name, age);
}

修改数据

BOOL success = [self.db executeUpdate:@"UPDATE t_student SET name = 'liwei' WHERE age%3;"];
if (success) {
    NSLog(@"修改成功。");
}else{
    NSLog(@"修改失败。");
}

删除数据

BOOL success = [self.db executeUpdate:@"DELETE FROM t_student WHERE age%3 AND age%2"];
if (success) {
    NSLog(@"删除成功。");
}else{
    NSLog(@"删除失败。");
}

事务

事务的概念不必多说。上述示例中,都是非事务的,即无伦执行的 SQL 语句是否发生错误都会 commit 本次操作。

FMDB 中事务操作:

触发事务:begin transaction

提交事务:commit transaction

回滚事务:rollback transaction

FMDatabase 的事务通常是将执行 SQL 语句放在 begin transactionrollback transaction 之间,一旦发生错误,立即完成 commit 事件,即回滚事务。

[self.db beginTransaction];
BOOL isRollBack = NO;
@try {
    // 执行你的 SQL 语句
} @catch (NSException *exception) {
    isRollBack = YES;
    // 回滚事务
    [self.db rollback];
} @finally {
    if (!isRollBack) {	// 如果未发生错误,则提交该事务
        [self.db commit];
    }
}

在数据库操作结束后,记得要关闭数据库 :

[self.db close];

上述示例就是 FMDatabase 实例的简单的 CRUD,但是,FMDatabase 是不安全的,官网也明确指明,不要在多线程中使用 FMDatabase 操作数据库,因为会发生意想不到的错误。因此,FMDB 提供了一个 FMDatabaseQueue 来创建队列执行事务,该队列是一个串型队列,它能够保证在多线程中任务的顺序执行,一次实现线程安全。

FMDatabaseQueue

// 创建一个 FMDatabaseQueue 队列
FMDatabaseQueue *dabaseQueue = [FMDatabaseQueue databaseQueueWithPath:filePath]];

非事务

FMDatabaseQueue 通过将 FMDatabase 的示例对象进行回调的方式进行线程安全性控制,需要执行的 SQL 语句就在该回调中完成,执行的方法、方式和正常使用 FMDatabase 无任何区别。

[dabaseQueue inDatabase:^(FMDatabase *db) {
	// 执行 SQL
}];

事务

当然,我们还是可以使用 beginTransaction,rollback ,commit 实现事务

[dabaseQueue inDatabase:^(FMDatabase *db) {
	[self.db beginTransaction];
	BOOL isRollBack = NO;
	@try {
    	// 执行你的 SQL 语句
	} @catch (NSException *exception) {
    	isRollBack = YES;
    	// 回滚事务
    	[self.db rollback];
	} @finally {
    	if (!isRollBack) {	// 如果未发生错误,则提交该事务
	        [self.db commit];
    }
}
}];

不过,除了自己完成事务的开始和提交,FMDB 已经帮我们封装好了事务的处理:

[dataBaseQueue inTransaction:^(FMDatabase *db, BOOL *rollback) {
    BOOL whoopsSomethingWrongHappened = true;
    whoopsSomethingWrongHappened &= 执行 SQL 语句1
    whoopsSomethingWrongHappened &= 执行 SQL 语句2
    whoopsSomethingWrongHappened &= 执行 SQL 语句3
    // 如果有错误则回滚
    if (!whoopsSomethingWrongHappened){
        *rollback = YES;
        return;
    }
    // do something
}];

更多 FMDB 使用请参考其github

更多 SQL 语句请参考菜鸟教程 或者 MySQL:常规操作示例

七、Core Data

CoreData 提供数据 – OC 对象映射关系来实现数据与对象管理,这样无需任何 SQL 语句就能操作他们。

CoreData 数据持久化框架是 Cocoa API 的一部分,⾸次在 iOS5 版本的系统中出现,它允许按照实体-属性-值模型组织数据,并以 XML 、⼆进制文件或者 SQLite 数据⽂件的格式持久化数据。

更多关于 Core Data 请参考:iOS CoreData数据库之创建详解


2018-10-24 新编