iOS数据持久化以及本地数据存取的相关操作

一、应用结构

  1. 每个应用都有自己的独立沙盒, 即文件系统目录, 除自身外不可对其进行访问
  2. 应用沙盒结构分析:
  • 应用程序包: 包含了所有的资源文件和可执行文件
    • Documents: 保存应用运行时生成的需要持久化的数据, iTunes同步设备时会备份该目录。例如, 游戏应用可将游戏存档保存在该目录
    • tmp: 保存应用运行时所需的临时数据, 使用完毕后再将相应的文件从该目录删除。应用没有运行时, 系统也可能会清除该目录下的文件。iTunes同步设备时不会备份该目录
    • Library/Caches: 保存应用运行时生成的需要持久化的数据, iTunes同步设备时不会备份该目录。一般存储体积大、不需要备份的非重要数据
    • Library/Preference: 保存应用的所有偏好设置, iOS的Settings(设置)应用会在该目录中查找应用的设置信息。iTunes同步设备时会备份该目录

二、生成保存路径

根据保存数据类型不同, 采取不同的保存路径,以下示例均统一放在Documents文件夹下

1. 沙盒根目录拼接Documents文件夹路径生成保存路径(不推荐)

NSString *homeDirectory = NSHomeDirectory();
NSString *documentPath = [homeDirectory stringByAppendingPathComponent:@"Documents"];

2. 通过用户文件夹下查找, 即NSUserDomainMask参数的含义(推荐)

//  由于程序有且只有独立拥有一个Documents文件夹, 所以数组中仅仅只有一个元素, 无论是第一个对象还是最后一个对象均可
NSArray *applicationFilesArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
//  对要存储的的内容添加文件名, 第一种方法会自动添加'/', 故可以直接添加文件名, 而第二种需要手动添加
NSString *documentPath = [applicationFilesArray firstObject];
    
NSString *filePath1 = [documentPath stringByAppendingPathComponent:@"newInfo.plist"];
NSString *filePath2 = [documentPath stringByAppendingString:@"/newInfo.plist"];

//  可以发现这两个路径相同
NSLog(@"filePath1 Address:%@", filePath1);
NSLog(@"filePath2 Address:%@", filePath2);

3. 其他路径

  • tmp:路径通过拼接字符串获得
[NSTemporaryDirectory() stringByAppendingPathComponent:@"newInfo.plist"]
  • Library/Caches:同示例, 修改首个参数为NSCachesDirectory即可
  • Library/Preference:通过NSUserDefaults存取该目录下的设置消息

三、应用数据存储的常用方式主要以五种形式为主:

XML属性列表(plist)、偏好设置(Preference)、NSKeyedArchiver(NSCoding)、SQLite3Core Data

1、 属性列表plist:

对象支持的主要类型包含NSStringNSDictionaryNSArrayNSDataNSNumber等, 其他本数据类型可以通过NSData进行转换存储

//  创建文件方法如下:
//  创建可变字典, 初始化设置值, 如果后期需要修改值, 只需要重新设置, 再保存一次即可
//  如果采用不可变字典, 首次写入依然可以生成, 但是后期不可更改信息
NSArray *applicationFilesArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentPath = [applicationFilesArray firstObject];
NSString *filePath = [documentPath stringByAppendingString:@"/newInfo.plist"];

NSMutableDictionary *saveInfoDic = [NSMutableDictionary dictionary];
[saveInfoDic setObject:@"Leo" forKey:@"name"];
[saveInfoDic setObject:@"Male" forKey:@"sex"];
[saveInfoDic setObject:@"25" forKey:@"age"];
[saveInfoDic writeToFile:filePath atomically:YES];
    
[saveInfoDic setObject:@"Female" forKey:@"sex"];
[saveInfoDic writeToFile:filePath atomically:YES];

//  读取文件方法如下:
//  从目标路径下获取字典, 获取单个属性键入键值即可
NSDictionary *readInfoDic = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSLog(@"age:%@", readInfoDic[@"age"]);
NSLog(@"name:%@", readInfoDic[@"name"]);

2、 偏好设置Preference:

对象支持类型包含NSStringNSDictionaryNSArrayNSDataNSNumber等, 其他基本数据类型可以通过NSData进行转换存储

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableDictionary *testDic = [NSMutableDictionary dictionary];
    
//  可以对键值对进行初始化
[defaults setObject:@"Leo" forKey:@"userName"];
[defaults setFloat:18.0f forKey:@"age"];
[defaults setBool:YES forKey:@"auto_login"];
    
//  该操作是对数据进行一个登记注册
[defaults registerDefaults:testDic];
    
//  取得之前设置的偏好设置
NSUserDefaults *getDefaults = [NSUserDefaults standardUserDefaults];
    
//  对应上式初始化, 再取得偏好设置文件后, 已经在册的键将被更新, 不存在的键将被创建
[getDefaults setFloat:25.0f forKey:@"age"];
[getDefaults setObject:@"Male" forKey:@"sex"];
    
//  将数据保存到本地磁盘当中进行保存, 此时为立即执行保存操作, 如果不执行该方法, 也许会造成数据还未保存完毕的结果
[getDefaults synchronize];

//  读取偏好设置的键值
NSLog(@"age:%@", [defaults objectForKey:@"age"]);
NSLog(@"sex:%@", [defaults objectForKey:@"sex"]);

3、 NSKeyedArchiver(NSCoding):

支持类型同plist, 如果自定义类对象要采取该方法, 则须遵守NSCoding协议

  • 协议中包含两个方法:
    • 方法一:每次归档均会调用该方法, 一般在方法中指定如何归档对象中的每个实例变量
      • 采取encodeObject:forKey:进行归档实例变量
      • - (void)encodeWithCoder:(NSCoder *)aCoder;
    • 方法二:每次从文件中恢复(解码)对象时, 调用该方法。一般在方法中指定如何解码对象中的每个实例变量
      • 采取decodeObjectForKey:进行归档实例变量
      • - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;

1. 归档一个已存在的对象到Documents下

NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
//  后缀名为archive (默认archive文件是加密的, 而设置为plist后缀的话可以进行明文阅读)
NSString *filePath = [documentPath stringByAppendingPathComponent:@"archiverExp.archive"];

//  1、归档一个已有对象如数组对象到Documents下
NSArray *keyArchiveArray = @[@"testA", @"testB"];
[NSKeyedArchiver archiveRootObject:keyArchiveArray toFile:filePath];
    
//  解码恢复对象
NSArray *unarchiveArray = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
//  打印解码信息
NSLog(@"unarchiveArray:%@", unarchiveArray);

2. 归档自定义对象

首先创建自定义对象, 以下几种归档均采用该对象为示例

#import <Foundation/Foundation.h>

@interface Example : NSObject<NSCoding>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger number;

@end
#import "Example.h"

@implementation Example

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    //  如果该类为子类, 父类遵守协议, 则需self = [super initWithCoder:aDecoder];
    self.name = [aDecoder decodeObjectForKey:@"name"];
    self.number = [aDecoder decodeIntegerForKey:@"number"];
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    //  如果该类为子类, 父类遵守协议, 则需[super encodeWithCoder:aCoder];
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeInteger:self.number forKey:@"number"];
}

@end

进行归档操作

NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
//  后缀名为archive (默认archive文件是加密的, 而设置为plist后缀的话可以进行明文阅读)
NSString *filePath = [documentPath stringByAppendingPathComponent:@"archiverExp.archive"];

//  2、归档自定义对象
Example *archiveExample = [[Example alloc] init];
archiveExample.name = @"eg";
archiveExample.number = 1;
[NSKeyedArchiver archiveRootObject:archiveExample toFile:filePath];
    
//  对象类型匹配, 解码恢复对象
Example *unarchiveExample = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
NSLog(@"unarchiveExample name:%@", unarchiveExample.name);
NSLog(@"unarchiveExample number:%ld", unarchiveExample.number);

3. 归档多个自定义对象

NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
//  后缀名为archive (默认archive文件是加密的, 而设置为plist后缀的话可以进行明文阅读)
NSString *filePath = [documentPath stringByAppendingPathComponent:@"archiverExp.archive"];

Example *archiveExample = [[Example alloc] init];
archiveExample.name = @"eg";
archiveExample.number = 1;
Example *archiveExample2 = [[Example alloc] init];
archiveExample2.name = @"eg2";
archiveExample2.number = 2;

//  3、归档多个自定义对象
//  建立可变数据区, 并连接到一个NSKeyedArchiver对象
NSMutableData *archiveData = [NSMutableData data];
NSKeyedArchiver *archive = [[NSKeyedArchiver alloc] initForWritingWithMutableData:archiveData];

//  开始对对象存档, 存档的数据都会储存在NSMutableData中
[archive encodeObject:archiveExample forKey:@"eg1"];
[archive encodeObject:archiveExample2 forKey:@"eg2"];
    
//  指定完对象存档, 调用该方法告诉系统存档完毕
[archive finishEncoding];
    
//  将存档好的数据写入文件
[archiveData writeToFile:filePath atomically:YES];
    
//  解码恢复对象
NSData  *unarchiveData = [NSData dataWithContentsOfFile:filePath];
    
//  根据数据, 解析出对应的NSKeyedUnarchive对象
NSKeyedUnarchiver *unarchive = [[NSKeyedUnarchiver alloc] initForReadingWithData:unarchiveData];
Example *unarchiveExample = [unarchive decodeObjectForKey:@"eg1"];
Example *unarchiveExample2 = [unarchive decodeObjectForKey:@"eg2"];
    
//  解码恢复完毕, 调用该方法告诉系统解码完毕, 此时unarchive不能再解码对象
[unarchive finishDecoding];

//  打印解码信息
NSLog(@"unarchiveExample name:%@", unarchiveExample.name);
NSLog(@"unarchiveExample2 name:%@", unarchiveExample2.name);

//  同理也可以将多个对象放到数组中进行归/解档, 在归/解档操作时会默认对数组中的对象自动进行encodeWithCoder:和initWithCoder的方法

4. 利用归档来实现深复制

Example *archiveExample = [[Example alloc] init];
archiveExample.name = @"eg";
archiveExample.number = 1;

//  4、利用归档来实现深复制
//  临时存储数据
NSData *tempData = [NSKeyedArchiver archivedDataWithRootObject:archiveExample];
    
//  解析data, 生成对象
Example *waitForCopyExample = [NSKeyedUnarchiver unarchiveObjectWithData:tempData];
    
//  通过内存地址的打印, 可以发现已经完成了深拷贝操作
NSLog(@"example1 address:0x%x", archiveExample);
NSLog(@"example2 address:0x%x", waitForCopyExample);

四、SQLite3

最常用的开源数据库, 内存开销小, 操作都是基于C底层, 效果好, 除开SQL语句需要多使用熟练以外

  1. 常用的5种数据类型: textintegerfloatbooleanblob
  2. 在iOS开发中要进行其使用, 需要添加库文件libsqlite3.0.tbdlibsqlite3.tbd均可, 后者是一个原始库, 前者是一个指针指向最新的数据库, 如果更新则无需手动修改
  3. 添加完库文件后, 在使用处还需导入<sqlite3.h>, 如果多处地方需使用, 不妨写入预编译头文件进行导入
  4. 不排除现在部分开发者使用fmdb等第三方数据库框架可以省去写SQL语句, 但是把底层数据库知识储备完善, 有百利而无一害
  5. 更多更清楚的了解SQL

1. 创建数据库文件, 并打开

NSString *documentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [documentDirectory stringByAppendingPathComponent:@"dataBase.sqlite"];
    
sqlite3 *sqliteDB = nil;
//  sqlite3_open()语句: 其将在指定路径下打开数据库, 如果不存在, 则新建数据库
//  通过openResult的返回结果可知道操作是否成功
int openResult = sqlite3_open([filePath UTF8String], &sqliteDB);
if (openResult != SQLITE_OK) {
    NSLog(@"打开数据库文件失败");
    return;
}

2. 创建表格

NSString *documentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [documentDirectory stringByAppendingPathComponent:@"dataBase.sqlite"];
    
sqlite3 *sqliteDB = nil;
//  sqlite3_open()语句: 其将在指定路径下打开数据库, 如果不存在, 则新建数据库
//  通过openResult的返回结果可知道操作是否成功
int openResult = sqlite3_open([filePath UTF8String], &sqliteDB);
if (openResult != SQLITE_OK) {
    NSLog(@"打开数据库文件失败");
    return;
}

//  创建一个错误信息
char *errorMsg = nil;
    
//  如果在创表语句中去掉if not exists, 将会出现“工作表已存在”的报错语句
//  sqlite3_exec()可以执行任何SQL语句, 对于表创建以及表中数据的增删改都可以进行操作
char *createTableSQL = "create table if not exists t_person(id integer primary key autoincrement, name text, age integer);";
int executeResult = sqlite3_exec(sqliteDB, createTableSQL, NULL, NULL, &errorMsg);
if (executeResult != SQLITE_OK) {
    NSLog(@"数据库创建表格失败, 原因为:%s", errorMsg);
    return;
}

//  执行完操作, 关闭数据库
sqlite3_close(sqliteDB);

3. 数据库的值绑定

  • SQLite中的插入语句, 带占位符进行插入

    • sqlite3_prepare_v2()用于判断语句是否有误, 其参数依次代表:
    sqlite3 *db,            /* Database handle */                   数据库句柄
    const void *zSql,       /* SQL statement, UTF-16 encoded */     SQL语句
    int nByte,              /* Maximum length of zSql in bytes. */  SQL语句的最大字符串长度、一般设-1即可, 从零终止符开始自动计算字符长度
    sqlite3_stmt **ppStmt,  /* OUT: Statement handle */             SQLy语句解析后的句柄
    const void **pzTail     /* OUT: Pointer to unused portion of zSql */    一个去观察SQL语句是否编译完毕的指针、指向下一个SQL语句开始的地址
    
    • sqlite3_bind_text()用于函数值的绑定, 其参数根据不同的数据格式略有不同, 基本以stmt指针、将绑定的值在表格中的位置, 默认为1开始、绑定值、数据长度, 设为-1自动计算、可选函数回调执行内存清理工作
NSString *documentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [documentDirectory stringByAppendingPathComponent:@"dataBase.sqlite"];
    
sqlite3 *sqliteDB = nil;
//  sqlite3_open()语句: 其将在指定路径下打开数据库, 如果不存在, 则新建数据库
//  通过openResult的返回结果可知道操作是否成功
int openResult = sqlite3_open([filePath UTF8String], &sqliteDB);
if (openResult != SQLITE_OK) {
    NSLog(@"打开数据库文件失败");
    return;
}

//  根据需求编写SQL插入语句
char *insertSQL = "insert into t_person(name, age) values (?, ?);";
sqlite3_stmt *insertStmt = nil;
if (sqlite3_prepare_v2(sqliteDB, insertSQL, -1, &insertStmt, NULL) == SQLITE_OK) {
    sqlite3_bind_text(insertStmt, 1, "Leo", -1, NULL);
    sqlite3_bind_int(insertStmt, 2, 27);
}
if (sqlite3_step(insertStmt) != SQLITE_DONE) {
    NSLog(@"数据插入错误");
}
    
//  关闭数据库句柄
sqlite3_finalize(insertStmt);
//  关闭数据库
sqlite3_close(sqliteDB);

4. 数据库的数据查询

  • sqlite3_step()返回SQLITE_ROW查询到一条新记录
  • sqlite3_column_*()用于获取每个字段对应的值, 第二参数为索引
NSString *documentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [documentDirectory stringByAppendingPathComponent:@"dataBase.sqlite"];
    
sqlite3 *sqliteDB = nil;
//  sqlite3_open()语句: 其将在指定路径下打开数据库, 如果不存在, 则新建数据库
//  通过openResult的返回结果可知道操作是否成功
int openResult = sqlite3_open([filePath UTF8String], &sqliteDB);
if (openResult != SQLITE_OK) {
    NSLog(@"打开数据库文件失败");
    return;
}

char *querySQL = "select id, name, age from t_person;";
sqlite3_stmt *queryStmt = nil;
if (sqlite3_prepare_v2(sqliteDB, querySQL, -1, &queryStmt, NULL) == SQLITE_OK) {
    while (sqlite3_step(queryStmt) == SQLITE_ROW) {
        int _id = sqlite3_column_int(queryStmt, 0);
        char *_name = (char *)sqlite3_column_text(queryStmt, 1);
        int _age = sqlite3_column_int(queryStmt, 2);
        NSString * name = [NSString stringWithUTF8String:_name];
        NSLog(@"id:%i, name:%@, age:%i", _id, name, _age);
    }
}
sqlite3_finalize(queryStmt);
sqlite3_close(sqliteDB);

只想日后回头看看来时的路,能发现自己并不是一无所有。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容