iOS数据本地持久化方法总结

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

plist文件(序列化)
preference(偏好设置)
NSKeyedArchiver(归档)
SQLite3
FMDB
CoreData

沙盒

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

iOS数据本地持久化方法总结_第1张图片
image.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

iOS数据本地持久化方法总结_第2张图片
CoreData核心结构图.png

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

PersistentObjectStore:存储持久对象的数据库(例如SQLite,注意CoreData也支持其他类型的数据存储,例如xml、二进制数据等)。
ManagedObjectModel:对象模型,对应Xcode中创建的模型文件。
PersistentStoreCoordinator:对象模型和实体类之间的转换协调器,用于管理不同存储对象的上下文。
ManagedObjectContext:对象管理上下文,负责实体对象和数据库之间的交互。

CoreData主要工作原理如下

读取数据库的数据时,数据库数据先进入数据解析器,根据对应的模板,生成对应的关联对象。
向数据库插入数据时,对象管理器先根据实体描述创建一个空对象,对该对象进行初始化,然后经过数据解析器,根据对应的模板,转化为数据库的数据,插入数据库中。
更新数据库数据时,对象管理器需要先读取数据库的数据,拿到相互关联的对象,对该对象进行修改,修改的数据通过数据解析器,转化为数据库的更新数据,对数据库更新。

CoreData的使用步骤如下

1.添加框架。
2.数据模板和对象模型。
3.创建对象管理上下文。
4.数据的增删改查操作。

在其中第二步的时候要注意,Xcode 8.0 之后和之前的Xcode 版本是有一些区别的。在 8.0之后创建.xcdatamodeld文件之后,添加实体之后会自动生成对应的对应类文件。
以下文章可以作为参考
Xcode8 CoreData的使用
Xcode 8 Core Data 生成代码 编译错误

你可能感兴趣的:(iOS数据本地持久化方法总结)