iOS常用的数据存储
在iOS开发过程中,会经常使用到数据存储.数据存储有好几种常见的方式:plist存储,偏好设置,归档,数据库等,该篇文章是我想做个记录,也想和大家分享下自己的一些见解,涉及的内容不是很深,只做实用性的参考.如果哪位大神对此有深入研究,可以留下来深入讨论下,我请吃饭.
沙盒
- iOS的存储都是存在沙盒中的,应用程序只能访问自己的沙盒
- 沙盒目录里有三个文件:Documents, Library, tmp
沙盒目录图
- Documents:用于存储用户数据或其它应该定期备份的信息
- 注意,该文件中的内容会在手机连接电脑时进行同步到云上,所以苹果不建议开发者将大量的数据写入到该文件中.如果将视频图片等大量的数据存入到该文件中,会被苹果拒的
// Documents获取方式
NSString *docStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
- Library:该目录下有两个子目录,Caches和Preferences.
- Caches:一般用于存放应用程序专用的支持文件,保存应用程序再次启动所需要的信息
- 数据存在该文件中
//Caches获取方法
NSString *cachesStr = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]
- Preferences:存放应用程序的偏好设置的文件.
- tmp:用于存放临时文件,当程序退出时该文件中的数据会被清空
//tmp获取方式
NSString *tmpStr = NSTemporaryDirectory();
关于沙盒的详细信息,可以去搜下别人的文章,我发现有很多文章对沙盒有详细的讲解的,或者参考苹果官方文档:About Files and Directories
Plist存储
定义:plist文件的全名是:Property List,它是一种用来存储串行化后的对象的文件。文件的扩展名是.plist,通常就称为plist文件.主要是用来存储一些OC对象,比如NSArray/NSDictionary,不能存放自定义对象.一般常用的Foundation对象有NSString, NSData, NSDate, NSNumber, NSArray, NSDictionary
存储原理:写入沙盒.只要有writeToFile的对象,就能进行plist存储,调用writeToFile就能自动生成plist格式的文件
注意:plist存储,不能存储自定义对象,会失败的。
plist文件在数据更新的时候需要先取出然后修改在存储
常用方法:
// 写入方法
- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;
- (BOOL)writeToURL:(NSURL *)url atomically:(BOOL)atomically;
// 取出 字典
+ (nullable NSDictionary<KeyType, ObjectType> *)dictionaryWithContentsOfFile:(NSString *)path;
+ (nullable NSDictionary<KeyType, ObjectType> *)dictionaryWithContentsOfURL:(NSURL *)url;
// 取出 数组
+ (nullable NSMutableArray<ObjectType> *)arrayWithContentsOfFile:(NSString *)path;
+ (nullable NSMutableArray<ObjectType> *)arrayWithContentsOfURL:(NSURL *)url;
获取沙盒路径
NSString *str = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
偏好设置
在开发中偏好设置用的还是比较多的,存取比较方便,不需要关心文件名,其实底层是利用字典存储一些键值对,你可以存储后打开路径文件看一下.
存储过程是用NSUserDefaults的单利调用setObject:forKey进行存储.
注意:不能存储对象,不能及时存储,需要做同步操作,把内存中的数据同步到硬盘上。
简单使用
// 存储名字jack到沙盒中,存储的key是 @"name"
[[NSUserDefaults standardUserDefaults] setObject:@"jack" forKey:@"name"];
// 同步
[[NSUserDefaults standardUserDefaults] synchronize];
// 用key @"name" 从沙盒中取出名字
NSString *str =[[NSUserDefaults standardUserDefaults] objectForKey:@"name"];
存储方法:
// 存储方法(set开头)
- (void)setObject:(nullable id)value forKey:(NSString *)defaultName;
- (void)setInteger:(NSInteger)value forKey:(NSString *)defaultName;
- (void)setFloat:(float)value forKey:(NSString *)defaultName;
- (void)setDouble:(double)value forKey:(NSString *)defaultName;
- (void)setBool:(BOOL)value forKey:(NSString *)defaultName;
- (void)setURL:(nullable NSURL *)url forKey:(NSString *)defaultName
读取数据的方法:
// 读取数据的方法
- (nullable id)objectForKey:(NSString *)defaultName;
- (nullable NSString *)stringForKey:(NSString *)defaultName;
- (nullable NSArray *)arrayForKey:(NSString *)defaultName;
- (nullable NSDictionary<NSString *, id> *)dictionaryForKey:(NSString *)defaultName;
- (nullable NSData *)dataForKey:(NSString *)defaultName;
- (nullable NSArray<NSString *> *)stringArrayForKey:(NSString *)defaultName;
- (NSInteger)integerForKey:(NSString *)defaultName;
- (float)floatForKey:(NSString *)defaultName;
- (double)doubleForKey:(NSString *)defaultName;
- (BOOL)boolForKey:(NSString *)defaultName;
- (nullable NSURL *)URLForKey:(NSString *)defaultName NS_AVAILABLE(10_6, 4_0);
删除数据:
// 删除数据
- (void)removeObjectForKey:(NSString *)defaultName;
归档
个人理解,归档的过程就是一个序列化的过程,然后存储到指定的路径路径文件.归档可以存储对象,需要遵守NSCoding协议,然后重写下面两种方法
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
具体使用方法:
- 自定义模型
Person类的.h文件
#import <UIKit/UIKit.h>
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, assign) NSInteger age;
@end
Person类的.m文件
#import "Person.h"
@interface Person ()<NSCoding>
@end
@implementation Person
// 解档
// 从一个给定unarchiver的数据中返回一个初始化对象。
- (instancetype)initWithCoder:(NSCoder *)decoder{
if (self = [super init]) {
self.name = [decoder decodeObjectForKey:@"name"];
self.age = [decoder decodeIntegerForKey:@"age"];
self.height = [decoder decodeFloatForKey:@"height"];
}
return self;
}
// 归档
/**
通过一个给定的archiver把消息接收者进行编码。
当接收到encodeObject消息的时候,类终端encodeWithCoder方法被调用。
*/
- (void)encodeWithCoder:(NSCoder *)coder{
[coder encodeObject:self.name forKey:@"name"];
[coder encodeInteger:self.age forKey:@"age"];
[coder encodeFloat:self.height forKey:@"height"];
}
- 第一种用法
// 创建对象
Person *person = [Person new];
person.name = @"jack";
person.age = 20;
person.height = 180.0f;
// 获取路径
NSString *str = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.archiver"];
// 归档
BOOL flag = [NSKeyedArchiver archiveRootObject:person toFile:filePath];
if (flag) {
NSLog(@"归档成功");
}else{
NSLog(@"归档失败");
}
// 解档
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:str];
- 第二种用法
// 创建对象
Person *person = [Person new];
person.name = @"jack";
person.age = 20;
person.height = 180.0f;
// 归档成二进制 (可以用偏好设置保存或者直接写入到沙盒)
NSData *dataArc = [NSKeyedArchiver archivedDataWithRootObject:person];
[[NSUserDefaults standardUserDefaults] setObject:dataArc forKey:@"archiver"];
[[NSUserDefaults standardUserDefaults] synchronize];
// 解档
NSData *dataUn = [[NSUserDefaults standardUserDefaults] objectForKey:@"archiver"];
Person *unPerson = [NSKeyedUnarchiver unarchiveObjectWithData:dataUn];
- 第三种用法
// 获取路径
NSString *str = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.archiver"];
// 创建一个二进制对象
NSMutableData *data = [[NSMutableData alloc] init];
// 设置归档写入对象
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
// 归档
[archiver encodeObject:person forKey:@"archiver"];// archivingDate的encodeWithCoder方法被调用
[archiver finishEncoding];
// 保存二进制到沙盒中
[data writeToFile:str atomically:YES];
// 解档
// 通过文件获取一个二进制对象
NSMutableData *data = [[NSMutableData alloc] initWithContentsOfFile:str];
// 读取二进制文件
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
// 获得类
Person *person = [unarchiver decodeObjectForKey:@"person"]; ;// initWithCoder方法被调用
[unarchiver finishDecoding];
在自定义模型的.m文件中,重写 - (instancetype)initWithCoder:(NSCoder *)decoder 时,调用的是[super init], 而不是 [super initWithCoder:decoder]; 这是因为Person类继承的是NSObject,NSObject类没有遵守NSCoding这个协议.
initWithCoder原理:只要解析文件就会调用,xib,storyboard都是文件,因此只要解析这两个文件,就会调用initWithCoder。
因此如果在storyboard使用自定义view,重写initWithCoder方法,一定要调用[super initWithCoder:],因为只有系统才知道怎么解析storyboard,如果没有调用,就解析不了这个文件。
重写NSCoding协议的方法时有一个不便的地方就是,当自定义模型特别多的时候,每个.m文件中都需要重写一遍,挺麻烦的.下面这个文件是通过运行时的方法遍历模型中的属性进行设置的,在使用的时候,需要导入#import "GGArchiverCoding.h", #import <objc/runtime.h>头文件,在.m文件中写入这句话就可以了:NSCodingImplementation;
GGArchiverCoding.h 文件
注意:该文件可以原版copy的
#define NSCodingImplementation \
- (instancetype)initWithCoder:(NSCoder *)aDecoder{\
if (self = [super init]) { \
unsigned int count; \
Ivar *ivarList = class_copyIvarList([self class], &count);\
for (int i = 0; i < count; i++) {\
Ivar ivar = ivarList[i];\
const char *ivarName = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:ivarName];\
id value = [aDecoder decodeObjectForKey:key]; \
[self setValue:value forKey:key];\
}\
}\
return self;\
}\
- (void)encodeWithCoder:(NSCoder *)aCoder{ \
unsigned int count;\
Ivar *ivarList = class_copyIvarList([self class], &count);\
for (int i = 0; i < count; i++) {\
Ivar ivar = ivarList[i];\
const char *ivarName = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:ivarName];\
id value = [self valueForKeyPath:key];\
[aCoder encodeObject:value forKey:key];\
}\
}
iOS中最常见的三种存储方式介绍完了,虽然使用率还是蛮好的,对于大型数据的存储上面三种就做不到了,或者说想做的很麻烦的.数据库就能这解决这种问题
SQLite介绍(PS:下面只是对SQLite3的简单实用的介绍,没有做详细的讲解,事后详情再补上.如果哪位大神有详细介绍的文章,可以跟帖链接,共同学习)
实际开发过程中,很少会直接用SQLite3,常常会采用一些对SQLite3封装好的三方框架,FMDB就是一个非常优秀的框架.
(度娘介绍):SQLite是一款轻型的嵌入式数据库,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了,它的处理速度比Mysql、PostgreSQL这两款著名的数据库都还快,数据库(Database)是按照数据结构来组织、存储和管理数据的仓库.
想学习数据就,建议安装下Navicat,用着很不错的一款数据库软件.
数据库可以分为两大种类:关系型数据库(主流)和 对象型数据库
常用关系型数据库PC端:Oracle、MySQL、SQL Server、Access、DB2、Sybase.嵌入式\移动客户端:SQLite
数据库的存储结构和excel很像,以表(table)为单位
数据库的每一列叫字段(column,列,属性)
每一行叫记录(row,record,每行存放多个字段对应的值)
建表:Tabel Name: 填写表明,建议以_t开头
常用的关键字有:select、insert、update、delete、from、create、where、desc、order、by、group、table、alter、view、index等等数据库中不可以使用关键字来命名表、字段
- SQL语句种类:
- DDL:数据定义语句:包括create(创表)等操作
- DML:数据操作语句:包括:insert(增加)、update(修改)、delete(删除)等操作
- DQL:数据查询语句:最常用的关键字select(查询),别的常用的关键字有where,order by,group by和having
- 字段类型:SQL将数据划分为四种数据类型
- integer : 整形值
- real:浮点值
- text:文本字符串
- blob:二进制数据
- 主键:用来唯一标识某一条记录,在创表的时候用primary key声明一个主键
创表
// 在编写数据库语句是,尽可能多的添加一些约束条件,这样可以方便交流
// 导入数据库头文件
#import <sqlite3.h>
// 保存数据库实例对象
@property (nonatomic, assign) sqlite3 *db;
// 获取沙盒路径
NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.db"];
// 将OC字符串 转成 C语言字符串
const char *cFilePath = filePath.UTF8String;
// 创表
// 将根据文件路径打开数据库,如果不存在,则会创建一个新的数据库
// 返回值表示状态
int result = sqlite3_open(cFilePath, &_db);
if (result == SQLITE_OK) {
NSLog(@"成功打开数据库");
// 创表
const char *sql = "CREATE TABLE IF NOT EXISTS t_shop (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, price integer NOT NULL);";
char *erroMsg = NULL;
// 创表语句 (exec:执行sql语句)
sqlite3_exec(_db, sql, NULL, NULL, &erroMsg);
if (erroMsg) {
NSLog(@"创表失败--%s", erroMsg);
}
}else{
NSLog(@"打开数据库失败");
}
// 移动端在操作数据库的时候一般不会考虑关闭
// 关闭数据库:sqlite3_close(db);
增加数据
static int i = 0;
i++;
NSString *name = [NSString stringWithFormat:@"name-%d", i];
NSString *sql = [NSString stringWithFormat:@"INSERT INTO t_shop (name, price) VALUES ('%@', %d);", name, i];
char *errMsg;
sqlite3_exec(self.db, sql.UTF8String, NULL, NULL, &errMsg);
NSLog(@"错误信息 = %s", errMsg);
删除数据
// 删除price大于10的数据
NSString *sql = [NSString stringWithFormat:@"DELETE FROM t_shop where price > 10"];
char *errMsg;
sqlite3_exec(self.db, sql.UTF8String, NULL, NULL, NULL);
NSLog(@"错误信息 = %s", errMsg);
修改数据
// price小于5,设置为0
NSString *sql = [NSString stringWithFormat:@"UPDATE t_shop set price = 0 where price < 5"];
char *errMsg;
sqlite3_exec(self.db, sql.UTF8String, NULL, NULL, &errMsg);
NSLog(@"%s", errMsg);
查询数据
// 查询数据如果用 sqlite3_exec(适合执行没有返回数据的操作) 需要用到第三个参数,函数的回调
// 查询所有字段
const char *sql = "SELECT * FROM t_shop;";
// 准备
// 第三个是sql长度, -1自动计算
// stmt是用来取出查询结果的
sqlite3_stmt *stmt = NULL;
int status = sqlite3_prepare_v2(self.db, sql, -1, &stmt, NULL);
if (status == SQLITE_OK) { // 准备成功 -- SQL语句正确
// sqlite3_step 步骤(一步一步的执行)
while (sqlite3_step(stmt) == SQLITE_ROW) { // 成功取出一条数据
// 后面的数字是列号
// 列好如果为0,代表的是主键的那列
const char *name = (const char *)sqlite3_column_text(stmt, 1);
const char *price = (const char *)sqlite3_column_text(stmt, 2);
NSLog(@"name = %s, price = %s", name, price);
}
}
FMDB简单用法
FMDB是以0C的方式封装了SQLite的C语言,用起来更加的面相对象,但是SQL语句还是需要写的.这比起单纯的使用SQLite3去做数据存储已经方便了很多,而且FMDB还对数据的存取操作做了线程安全的保证.
下载下来后的FMDB目录结构
在实际操作过程中,主要用到三个类
FMDatabase:一个FMDatabase对象就代表一个单独的SQLite数据库
FMResultSet:查询后的结果集.
FMDatabaseQueue:用于在多个线程中执行多个查询或更新,保证线程安全的.
- 简单用法
创建FMDatabase对象(打开数据库)
// FMDB提供了两个方法去创建FMDatabase对象
// 通过指定数据库文件路径来创建FMDatabase对象
+ (instancetype)databaseWithPath:(NSString*)inPath;
- (instancetype)initWithPath:(NSString*)inPath;
// path文件路径
FMDatabase *db = [FMDatabase databaseWithPath:path];
if (![db open]) {
NSLog(@"数据库打开失败!");
}
// 关闭数据库
// [db close];
文件路径有三种情况(在FMDB文档里有说明的)
1,具体的系统文件路径,如果不存FMDatabase对象在会自动创建
2,数据库路径为空字符串@"", 会在零食目录创建一个空的数据库,当FMDatabase关闭连接时,数据库文件也会被删除
3,nil,会创建一个内存中临时数据库,当FMDatabase关闭连接时, 数据库也会被销毁.
执行更新
在FMDB中,除查询以外的所有操作,都称为“更新”create、drop、insert、update、delete等
// 下面这些方法在FMDatabase.h文件有详细的说明,建议查看头文件.
使用executeUpdate:方法执行更新,sql和format是数据库字符串
- (BOOL)executeUpdate:(NSString*)sql, ...
- (BOOL)executeUpdateWithFormat:(NSString*)format, ...
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments
// 示例
// 创表
NSString *creatTableSql = @"CREATE TABLE IF NOT EXISTS t_userTable (id integer PRIMARY KEY autoincrement, name text, price integer)";
[db executeUpdate:creatTableSql];
// 增加数据
[db executeUpdate:@"UPDATE t_shop SET price = ? WHERE name = ?;", @20, @"Jack"]
执行查询
查询方法在FMDatabase.h文件中,返回的是FMResultSet结果集
常用的查询方法,sql和format是数据库字符串
- (FMResultSet *)executeQuery:(NSString*)sql, ...
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments
// 示例
// 查询数据
FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_shop"];
// 遍历结果集
while ([rs next]) {
NSString *name = [rs stringForColumn:@"name"];
int price = [rs intForColumn:@"price"];
}
// FMDatabaseQueue提供了三个对象方法
//1. 同步队列执行数据库操作。
- (void)inDatabase:(void (^)(FMDatabase *db))block;
//2.使用事务进行同步队列执行数据库操作
- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;
//3.使用递延交易
- (void)inDeferredTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;
在项目中用FMDB做数据的存取的时候需要考虑到线程的安全问题,所以我在使用的时候是这样用的
// 数据库队列
static FMDatabaseQueue *queue = nil;
if (!queue) {
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"fmdb.db"];
// 该方法会根据路径去获取一个队列,如果路径不存在会自动创建一个数据库
queue = [FMDatabaseQueue databaseQueueWithPath:path];
}
if (queue == nil) {
NSLog(@"创建队列失败");
return ;
}
// 创表
[queue inDatabase:^(FMDatabase *db) {
NSString *creatTableSql = @"CREATE TABLE IF NOT EXISTS t_userTable (id integer PRIMARY KEY autoincrement, uid integer, name text, timeStamp integer)";
[db executeUpdate:creatTableSql];
}];
// 添加数据
static int i = 0;
time_t now;
time_t timeStamp = time(&now);
i++;
[queue inDatabase:^(FMDatabase *db) {
[db executeUpdateWithFormat:@"INSERT INTO t_userTable (uid, name, timeStamp) VALUES (%d, %@, %ld)", i, @"jack", timeStamp];
}];
// 查询数据
[queue inDatabase:^(FMDatabase *db) {
NSString *query = [NSString stringWithFormat:@"SELECT * FROM t_userTable"];
FMResultSet *set = [db executeQuery:query];
while ([set next]) {
NSString *uid = [set stringForColumn:@"uid"];
NSString *name = [set stringForColumn:@"name"];
long int timeStamp = [set intForColumn:@"timeStamp"];
}
}];
// 修改数据
NSString *update = [NSString stringWithFormat:@"UPDATE t_userTable set uid = 100 where uid > 10"];
[queue inDatabase:^(FMDatabase *db) {
[db executeUpdate:update];
}];
// 删除数据
NSString *delete = [NSString stringWithFormat:@"DELETE FROM t_userTable where uid < %zd", 10];
[queue inDatabase:^(FMDatabase *db) {
[db executeUpdate:delete];
}];
关于数据库的详细的知识,后期不忙了再补上.大神们如有详细见解,期望您