常见的储存方式
- 文件读写存储(plist,NSUserDefaults)
- 解归档存储(NSKeyedArchiver)
- 数据库存储(SQLite、FMDB、CoreData、Keychain)
了解缓存,先要了解iOS中沙盒机制这个概念
沙盒其实质就是在iOS系统下,每个应用在内存中对应的存储空间。
每个iOS应用都有自己的应用沙盒(文件系统目录),与其他文件系统隔离,各个沙盒之间相互独立,而且不能相互访问(手机没有越狱情况下),各个应用程序的沙盒相互独立的,在系统内存消耗过高时,系统会收到内存警告并自动退出软件。这就保证了系统的数据的安全性及系统的稳定性。
IOS应用程序职能在系统为该应用所分配的文件区域下读写文件,这个文件区域就是应用程序沙盒。所有的非代码文件如:图片、声音、映象等等都存放在此。
在mac中command+shift+G命令,然后输入users/用户名/library命令进入库,然后依次进入application support/iphone simulator/版本/applications文件夹,这里面的各个文件夹对应着各个应用程序。
Documents:除了基于NSUserDefaults的首选项设置外,应用程序的数据、文件都保存在该目录下
Library:基于NSUserDefaults的首选项参数保存在Library/Preferences下
tmp:应用程序存储临时文件,ios同步时itunes不会同步这里面的数据,当应用程序不在需要这些文件时,应当删除以避免占用空间。
1. 文件读写存储(NSFileManager)
-
文件操作可通过单例 NSFileManager 处理。文件存储的路径可以代码设置。
-
可以存储大量数据,对数据格式没有限制。
-
但由于数据的存取必须是一次性全部操作,所以在频繁操作数据方面性能欠缺。
int main(int argc, const char * argv[]) {
@autoreleasepool {
/// 路径 如无则自动创建一个
NSString *stringPath = @"/Users/songzhuo/Desktop/string写入文件.txt";
NSString *dataPath = @"/Users/songzhuo/Desktop/data写入文件.txt";
NSString *arrayPath = @"/Users/songzhuo/Desktop/array写入文件.plist";
// 写入
NSString *string = @"string写文件";
BOOL isWriteString =[string writeToFile:stringPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
if (isWriteString) { NSLog(@"string 文件写入成功"); };
NSData *data =[[NSData alloc]initWithContentsOfFile:stringPath];
BOOL isWriteData =[data writeToFile:dataPath atomically:YES];
if (isWriteData) { NSLog(@"data 文件写入成功"); }
// 数组在写文件时 包含元素只能是 NSString NSArray NSData NSNumber NSDictionary
NSString *sanNameStr = @"张三";
NSString *siNameStr = @"李四";
NSString *wuNameStr = @"王五";
NSArray *nameArray = [NSArray arrayWithObjects:sanNameStr, siNameStr, wuNameStr,nil];
BOOL isWriteArray =[nameArray writeToFile:arrayPath atomically:YES];
if (isWriteArray) { NSLog(@"array 写入文件成功"); }
/// 读取文件
NSString *readStr =[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@" 读取的string为 : %@", readStr);
NSString *readDataStr =[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@" 读取的data为 :%@", readDataStr);
NSArray *readArr =[[NSArray alloc]initWithContentsOfFile:arrayPath];
NSLog(@"读取的array为 :%@",readArr);
//字典存入plist文件
NSDictionary *dic =[[NSDictionary alloc]initWithObjectsAndKeys:
sanNameStr,@"第一",
siNameStr,@"第二",
wuNameStr,@"第三",
nameArray,@"姓名数组", nil];
[dic writeToFile:arrayPath atomically:YES];
NSDictionary *myDic =[[NSDictionary alloc]initWithContentsOfFile:arrayPath];
NSArray * nameArr =myDic[@"姓名数组"];
NSLog(@"nameArr : %@",nameArr);
}
return 0;
}
当我们正常写完数据之后,后面需要添加新的数据并不能直接像上面那样再次操作。一旦再次操作,会覆盖掉原先的数据。为了避免覆盖,我们需要对添加的数据进行偏移操作。
int main(int argc, const char * argv[]) {
@autoreleasepool
{
NSString *path = @"/Users/songzhuo/Desktop/文件的偏移量.txt";
NSString*oldStr = @"old string ,";
[oldStr writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
NSFileHandle *wirteFileHandle =[NSFileHandle fileHandleForWritingAtPath:path];
//添加数据
NSString *appendString = @"append string";
NSData *data =[appendString dataUsingEncoding:NSUTF8StringEncoding];
//设置偏移量 如果不设置偏移量 系统默认从文件一开始添加数据
[wirteFileHandle seekToEndOfFile]; //设置偏移量跳到文件的末尾
[wirteFileHandle writeData:data]; //写入数据
[wirteFileHandle closeFile]; //关闭文件
//文件定位读取
// 从中间读取文件一直读取到结尾
NSFileHandle *readFileHandle =[NSFileHandle fileHandleForReadingAtPath:path];
if (readFileHandle == nil) {
NSLog(@"读取失败");
}
//获取文件的总长度
NSFileManager *fileManager =[NSFileManager defaultManager];
NSDictionary *dic =[fileManager attributesOfItemAtPath:path error:nil];
NSLog(@"dic : %@",dic);
NSNumber *number =[dic objectForKey:NSFileSize];
int length = [number intValue];
NSLog(@"length = %d", length);
//设置文件读取的偏移量
[readFileHandle seekToFileOffset:length/2];
NSData *readData =[readFileHandle readDataToEndOfFile];//读到结尾
//从开始读取文件一直读取到某一位置
// - (NSData *)readDataOfLength:(NSUInteger)length;
NSString*readStr =[[NSString alloc]initWithData:readData encoding:NSUTF8StringEncoding];
NSLog(@"readStr : %@",readStr);
[readFileHandle closeFile];
}
return 0;
}
当存储完数据后,因业务需求,需要改变数据存储的位置,我们就需要对文件进行移动或删除等操作。
int main(int argc, const char * argv[]) {
@autoreleasepool
{
//创建NSFileManager对象 用类方法来创建文件
NSFileManager * file = [NSFileManager defaultManager];
NSString *str = @"对一个文件操作";
//字符串转换为二进制数据
NSData * data = [str dataUsingEncoding:NSUTF8StringEncoding];
//创建一个文件并且写入数据
NSString *home = NSHomeDirectory();
NSString *path = [home stringByAppendingPathComponent:@"file.txt"];
[file createFileAtPath:path contents:data attributes:nil];
NSLog(@"主目录 = %@", home);
/* ______________创建文件夹_____________ */
NSString *folderPath = @"/Users/hhg/Desktop/myFolder";
BOOL isSuccess =[file createDirectoryAtPath:folderPath withIntermediateDirectories:YES attributes:nil error:nil];
if (isSuccess) {
NSLog(@"文件夹创建成功");
}
/*___________读取文件__________*/
//读取二进制数据
NSData *readData =[file contentsAtPath:path];
//将二进制数据转码
NSString *readStr =[[NSString alloc]initWithData:readData encoding:NSUTF8StringEncoding];
NSLog(@"读取文件的内容: %@",readStr);
/* __________文件的移动复制剪切______________ */
//移动
NSString *movePath = @"/Users/hhg/Desktop/myFolder/file.txt";
BOOL isMove = [file moveItemAtPath:path toPath:movePath error:nil];
if (isMove) {
NSLog(@"文件移动成功");
}
//文件复制
[file copyItemAtPath:movePath toPath:folderPath error:nil];
/* _________文件的删除____________*/
BOOL isDelete = [file fileExistsAtPath:movePath];
if ( isDelete ) {
// [file removeItemAtPath:movePath error:nil];
}
NSDictionary *dic = [file attributesOfItemAtPath:movePath error:nil];
NSNumber *num = [dic objectForKey:NSFileSize];
NSLog(@"字典:%@", dic);
NSLog(@" 尺寸大小:%@", num);
//-->创建文件夹->创建文件(写入字符串)-> 移动文件 ->读取文件内容->删除文件
}
return 0;
}
1.1 plist 格式文件存储
- plist文件(XML属性列表),在使用plist进行数据存储和读取,只适用于系统自带的一些常用类型才能用,且必须先获取路径相对麻烦
- 可以存储的类型有NSString,NSDictionary,NSArray,NSNumber,Boolean,NSDate,NSData等基本类型
- 常用于存储用户的设置,或存储项目中经常用到又不经常改变的数据
- 创建.plist可以用xcode工具,也可以用代码
- 不适合存储大量数据,而且只能存储基本类型
- 可以实现:增,删,改,查等操作,但数据的存取是一次性的全部操作,所以性能方向表现并不好
- NSDate,BOOL,Int,Float,NSNumber,NSData的数据存储都是转换成NSDictionary的Key-Value形式之后,通过NSDictionary存储方式存储的。
//写入文件
NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *path = [doc stringByAppendingPathComponent:@"myself.plist"];
NSDictionary *dict = @{@"name": @"yixiang"};
[dict writeToFile:path atomically:YES];
//读取文件
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
1.2 NSUserDefaults 沙盒存储(个人偏好设置)
- NSUserDefaults 沙盒存储(个人偏好存储) 是个单例类,用于存储少量数据,例如登录后的用户名,密码等。
- 应用程序启动后,会在沙盒路径Library -> Preferences 下默认生成以工程bundle为名的.plist文件,用NSUserDefaults存储的数据都是存储在该.plist文件中。
- 这种方式本质是操作plist文件,所以性能方面的考虑同plist文件数据储存
//写入文件
NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
[defaults setObject:@"yixiang" forKey:@"name"];
[defaults setInteger:27 forKey:@"age"];
[defaults synchronize];
//读取文件
NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
NSString *name=[defaults objectForKey:@"name"];
NSInteger age=[defaults integerForKey:@"age"];
2. 解归档存储
- plist 与 NSUserDefaults(个人偏好设置两种类型的储存只适用于系统自带的一些常用类型,而且前者必须拿到文件路径,后者也只能储存应用的主要信息。
- 对于开发中自定义的数据模型的储存,我们可以考虑使用归档储存方案。 归档保存数据,文件格式自己可以任意,没有要求
- 即便设置为常用的数据格式(如:.c .txt .plist 等)要么不能打开,要么打开之后乱码显示。
- 值得注意的是使用归档保存的自定义模型需要实现NSCoding协议下的两个方法。 不适合存储大量数据,可以存储自定义的数据模型
- 不适合存储大量数据,可以存储自定义的数据模型。
- 虽然归档可以存储自定义的数据结构,但在大批量处理数据时,性能上仍有所欠缺。
YXPerson.h文件如下:
@interface YXPerson : NSObject<NSCoding>
@property(nonatomic,copy) NSString *name;
@property(nonatomic,assign) int age;
@end
YXPerson.m文件如下:
#import "YYPerson.h"
@implementation YYPerson
-(void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.age forKey:@"age"];
}
-(id)initWithCoder:(NSCoder *)aDecoder{
if (self=[super init]) {
self.name=[aDecoder decodeObjectForKey:@"name"];
self.age=[aDecoder decodeIntegerForKey:@"age"];
}
return self;
}
@end
在ViewController中对它进行写入和读取
//写入对象
YXPerson *p=[[YXPerson alloc]init];
p.name=@"yixiang";
p.age=27;
NSString *docPath=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject];
NSString *path=[docPath stringByAppendingPathComponent:@"person.yixiang"];
[NSKeyedArchiver archiveRootObject:p toFile:path];
//读取对象
YXPerson *p=[NSKeyedUnarchiver unarchiveObjectWithFile:path];
3. 数据库存储
3.1. SQLITE数据库
- 上述三种方法都无法存储大批量的数据,有性能的问题。
下面简单介绍一下,如何打开数据库,新增一张表格,然后对其进行增删改查的操作。
sqlite3 *_db;
- (void)openDB{
//获取数据库文件路径
NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *fileName = [doc stringByAppendingPathComponent:@"students.sqlite"];
//将OC字符串转换为c语言的字符串
const char *cfileName = fileName.UTF8String;
//打开数据库文件(如果数据库文件不存在,那么该函数会自动创建数据库文件)
int result = sqlite3_open(cfileName, &_db);
if (result == SQLITE_OK) {//打开成功
NSLog(@"成功打开数据库");
}else{
NSLog(@"打开数据库失败");
}
}
- (void)createTable{
//创建表
const char *sql = "CREATE TABLE IF NOT EXISTS t_student(id integer PRIMARY KEY AUTOINCREMENT,name text NOT NULL,age integer NOT NULL);";
char *errmsg= NULL;
int result = sqlite3_exec(_db, sql, NULL, NULL, &errmsg);
if (result==SQLITE_OK) {
NSLog(@"创建表成功");
}else{
NSLog(@"创建表失败---%s",errmsg);
}
}
- (void)insertData{
//插入数据
for (int i=0; i<10; i++) {
//拼接sql语句
NSString *name = [NSString stringWithFormat:@"yixiangboy--%d",arc4random_uniform(100)];
int age = arc4random_uniform(20)+10;
NSString *sql = [NSString stringWithFormat:@"INSERT INTO t_student (name,age) VALUES ('%@',%d);",name,age];
//执行SQL语句
char *errmsg = NULL;
sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &errmsg);
if (errmsg) {//如果有错误信息
NSLog(@"插入数据失败--%s",errmsg);
}else{
NSLog(@"插入数据成功");
}
}
}
- (void)deleteData{
//删除age小于15的数据
NSString *sql = [NSString stringWithFormat:@"DELETE FROM t_student WHERE age<15"];
char *errmsg = NULL;
sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@"删除数据失败");
}else{
NSLog(@"删除数据成功");
}
}
- (void)updateData{
//大于20岁的都置为20岁
NSString *sql = [NSString stringWithFormat:@"UPDATE t_student set age=20 WHERE age>20"];
char *errmsg = NULL;
sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@"更新数据失败");
}else{
NSLog(@"更新数据成功");
}
}
- (void)queryData{
const char *sql = "SELECT id,name,age FROM t_student WHERE age<20";
sqlite3_stmt *stmt = NULL;
//进行查询前的准备工作
if(sqlite3_prepare_v2(_db, sql, -1, &stmt, NULL)==SQLITE_OK){//SQL语句没有问题
NSLog(@"查询语句没有问题");
//每调用一次sqlite3_step函数,stmt就会指向下一条记录
while (sqlite3_step(stmt)==SQLITE_ROW) {//找到一条记录
//取出数据
//(1)取出第0个字段的值(int)
int ID=sqlite3_column_int(stmt, 0);
//(2)取出第一列字段的值(text)
const unsigned char *name = sqlite3_column_text(stmt, 1);
//(3)取出第二列字段的值(int)
int age = sqlite3_column_int(stmt, 2);
printf("%d %s %d\n",ID,name,age);
}
}else{
NSLog(@"查询语句有问题");
}
}
3.2. FMDB
- 它是一款轻型的嵌入式数据库,安卓和ios开发使用的都是SQLite数据库;占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了;而且它的处理速度比Mysql、PostgreSQL这两款著名的数据库都还快。
- FMDB 正是基于 SQLite 开发的一套开源库。使用时,需要自己写一些简单的SQLite语句
FMDatabase *db;
- (void)openDB {
//1、获取数据库文件路径
NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *fileName = [doc stringByAppendingPathComponent:@"students.sqlite"];
//2、获取数据库连接
_db = [FMDatabase databaseWithPath:fileName];
//3、打开数据库连接
if ([_db open]) {
NSLog(@"打开数据库成功");
}else{
NSLog(@"打开数据库失败");
}
}
- (void)createTable{
BOOL result = [_db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_student (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, age integer NOT NULL);"];
if (result) {
NSLog(@"创建表格成功");
}else{
NSLog(@"创建表格失败");
}
}
- (void)insertData{
for (int i=0; i<10; i++) {
NSString *name = [NSString stringWithFormat:@"yixiang-%d",arc4random_uniform(100)];
int age = arc4random_uniform(20)+10;
BOOL result = [_db executeUpdate:@"INSERT INTO t_student (name, age) VALUES (?, ?);",name, @(age)];
if (result) {
NSLog(@"插入成功");
}else{
NSLog(@"插入失败");
}
}
}
- (void)deleteData{
BOOL result = [_db executeUpdate:@"DELETE FROM t_student WHERE age<15"];
if (result) {
NSLog(@"删除成功");
}else{
NSLog(@"删除失败");
}
}
- (void)updateData{
BOOL result = [_db executeUpdate:@"UPDATE t_student set age=20 WHERE age>20"];
if (result) {
NSLog(@"更新成功");
}else{
NSLog(@"更新失败");
}
}
- (void)queryData{
FMResultSet *resultSet = [_db executeQuery:@"SELECT * FROM t_student WHERE age > ?",@(20)];
while ([resultSet next]) {
int ID = [resultSet intForColumn:@"id"];
NSString *name = [resultSet stringForColumn:@"name"];
int age = [resultSet intForColumn:@"age"];
NSLog(@"%d %@ %d",ID,name,age);
}
}
3.3. CoreData
- CoreData 是苹果给出的一套基于 SQLite 的数据存储方案;而且不需要自己写任何SQLite语句。该功能依赖于 CoreData.framework 框架,该框架已经很好地将数据库表和字段封装成了对象和属性,表之间的一对多、多对多关系则封装成了对象之间的包含关系
- Core Data的强大之处就在于这种关系可以在一个对象更新时,其关联的对象也会随着更新,相当于你更新一张表的时候,其关联的其他表也会随着更新。Core Data的另外一个特点就是提供了更简单的性能管理机制,仅提供几个类就可以管理整个数据库。由于直接使用苹果提供的CoreData容易出错,这里提供一个很好的三方库 MagicalRecord
缓存系统
对大多数 APP 而言,都是 Hybrid 开发,Web 页与原生同时存在,其中 Web 页可能是 UIWeb 也可能是 WKWeb 。所以与之相应的缓存系统,应该包括 Web 缓存与 原生接口数据缓存两部分。
原生接口部分的数据缓存
存储方式:主要采用文件读写、归档、个人偏好设置(NSUserDefaults) 。
具体说明:大部分接口数据解析之后写入文件保存(读写操作最好 GCD 子线程操作);整个应用需要用到的重要数据模型可以考虑采用归档方式(标记状态的数据模型);与用户相关的信息、单个标记标识等采用个人偏好设置。
补充: 原生接口数据存储方式以上三种方式就已够用;当然对于一些涉及查询、删除、更新等操作的数据模型,就需要使用数据库操作。这里推荐使用 CoreData 的封装库 MagicalRecord 。
3.4 Keychain存储
iOS keychain 是一个相对独立的空间,保存到keychain钥匙串中的信息不会因为卸载/重装app而丢失, 。相对于NSUserDefaults、plist文件保存等一般方式,keychain保存更为安全。所以我们会用keyChain保存一些私密信息,比如密码、证书、设备唯一码(把获取到用户设备的唯一ID 存到keychain 里面这样卸载或重装之后还可以获取到id,保证了一个设备一个ID)等等
keychain是用SQLite进行存储的。用苹果的话来说是一个专业的数据库,加密我们保存的数据,可以通过metadata(attributes)进行高效的搜索。keychain适合保存一些比较小的数据量的数据,如果要保存大的数据,可以考虑文件的形式存储在磁盘上,在keychain里面保存解密这个文件的密钥。