数据保存(永久保存)方式
一、数据保存(永久保存)方式有五种:
1.NSUserDefaults:保存设置数据
2.归档:保存自定义数据
3.文件(plist,txt)
4.数据库和CoreData
5.KeyChain(钥匙串—系统中 钥匙串访问 这个程序)
只有数据库和CoreData才适合用于保存大量的数据(效率高,因为有数据库的算法),其它方式只用于保存少量数据(保存大量数据效率低)。
前四个在沙盒范围内的,第五个即使删除app也存在于手机中。
KeyChain钥匙串是单向加密的,最为安全。
数据保存可以提升app的体验度:一开始进入app,显示的是上次关闭时的数据;当刷新时才请求新的数据
数据保存多数用于实现`收藏`功能
二、NSUserDefaults
(1)只能保存基本数据类型(int,float,double,BOOL,NSString,NSArray,NSDictionary,NSData,NSURL等),不能保存自定义的对象
(2)用于保存系统的设置型数据,如手机操作系统里面的`通用`设置数据
(3)NSUserDefaults 系统采取定期保存的机制,具体多久保存一次未知,也可手动采取立即保存(强制保存)(需要调用synchronize)
(4)只用于保存少量数据
(5)以键值对的形式保存
基本操作:
- //set方法保存(定期保存,如果立即保存,需要调用synchronize)
- [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"key1"];
- [[NSUserDefaults standardUserDefaults] setObject:@[] forKey:@"key2"];//数组
- //强制保存
- [[NSUserDefaults standardUserDefaults] synchronize];
- //取值
- [[NSUserDefaults standardUserDefaults] boolForKey:@"key"];
- //删除
- [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"key"];
应用例子:自动登录
- #import "ViewController.h"
- //自动登录
- //#define kAutoLoginKey @"kAutoLoginKey"
- @interface ViewController ()
- @property (weak, nonatomic) IBOutlet UITextField *username;
- @property (weak, nonatomic) IBOutlet UITextField *password;
- - (IBAction)login:(id)sender;
- - (IBAction)autoLoginBtnClick:(UIButton *)sender;
- @end
- @implementation ViewController
- - (void)viewDidLoad {
- [super viewDidLoad];
- //自动登录
- if ([[NSUserDefaults standardUserDefaults] boolForKey:@"kAutoLoginKey"])
- {
- //发送登录请求,然后请求主页界面的数据
- NSLog(@"发送登录请求");
- }
- else
- {
- //弹出登录界面,登录成功后dismiss掉登录界面
- NSLog(@"弹出登录");
- }
- }
- - (IBAction)login:(id)sender {
- //若勾选了自动登录按钮,保存用户信息到 NSUserDefaults
- }
- //自动登录按钮
- - (IBAction)autoLoginBtnClick:(UIButton *)sender
- {
- //修改按钮状态
- sender.selected = !sender.selected;
- //保存当前状态
- [[NSUserDefaults standardUserDefaults] setBool:sender.selected forKey:@"kAutoLoginKey"];
- [[NSUserDefaults standardUserDefaults] synchronize];
- }
- @end
拓展应用:重启app,记录上次的选中状态。
三、归档(序列化)与解归档(反序列化)
(1)能保存自定义的对象,解决NSUserDefaults不能解决的问题
(2)转化成NSData,以键值对的形式保存:要保存的对象(类),被转化为NSData,保存到NSUserDefaults下
(3)归档和反归档需要`被归档的类`遵守NSCoding协议,自定义的类需要重写`encodeWithCoder:`和`initWithCoder:`方法
- #import <Foundation/Foundation.h>
- //归档需要遵守NSCoding协议
- @interface Person : NSObject<NSCoding>
- @property (nonatomic, copy) NSString *name;
- @property (nonatomic) NSInteger age;
- + (instancetype)personWithName:(NSString *)name age:(int)age;
- @end
- #import "Person.h"
- @implementation Person
- + (instancetype)personWithName:(NSString *)name age:(int)age
- {
- Person *person = [[self alloc] init];
- person.name = name;
- person.age = age;
- return person;
- }
- //归档的时候会调用,也就是调用encodeObject:forKey:
- - (void)encodeWithCoder:(NSCoder *)aCoder
- {
- //保存
- [aCoder encodeObject:self.name forKey:@"name"];
- [aCoder encodeObject:@(self.age) forKey:@"age"];
- //encodeObject:(id类型),因此int类型无法保存
- }
- //解归档的时候会调用,也就是调用decodeObjectForKey:
- - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
- {
- if (self = [super init])
- {
- //取值
- self.name = [aDecoder decodeObjectForKey:@"name"];
- self.age = [[aDecoder decodeObjectForKey:@"age"] integerValue];
- }
- return self;
- }
- @end
- #import "ViewController.h"
- #import "Person.h"
- @interface ViewController ()
- @end
- @implementation ViewController
- (void)viewDidLoad {- [super viewDidLoad];
- [self encode];
- [self decode];
- }
- /**
- * 归档
- */
- - (void)encode
- {
- Person *p1= [Person personWithName:@"小明" age:20];
- Person *p2 = [Person personWithName:@"小张" age:30];
- NSArray *array = @[p1,p2];
- //保存对象转化为二进制数据(一定是可变对象)
- NSMutableData *data = [NSMutableData data];
- //1.初始化
- NSKeyedArchiver *archivier = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
- //2.归档
- [archivier encodeObject:array forKey:@"key"];
- //3.完成归档 --- 必须调用,否则报警告,且无法完成归档
- [archivier finishEncoding];
- //4.保存
- [[NSUserDefaults standardUserDefaults] setObject:data forKey:@"data"];
- //保存到指定目录:
- //[data writeToFile:保存路径 atomically:YES];
- /*
- atomically:
- 为YES,表示先写入到临时目录,确认整个文件保存过程中没出错后,再自动写入到指定的目录
- 为NO,表示直接写入到指定的目录,就算出错也继续写入
- 因此推荐 atomically:YES
- */
- NSLog(@"%@",data);
- }
- /**
- * 解归档
- */
- - (void)decode
- {
- //获取保存的数据
- NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:@"data"];
- //1.初始化
- NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
- //2.解归档
- NSArray *persons = [unarchiver decodeObjectForKey:@"key"];
- //3.完成解归档
- [unarchiver finishDecoding];
- for (Person *person in persons)
- {
- NSLog(@"%@ - %ld",person.name,(long)person.age);
- }
- }
- @end
`
四、文件保存 writeToFile:
1、plist 文件
(1)创建方式:直接创建、代码创建:把数据保存到plist文件中
(2)plist 文件 属于 xml 文件
2、txt 文件
- NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
- #if 0
- BOOL isOK = [@"111" writeToFile:[cachePath stringByAppendingPathComponent:@"1.txt"] atomically:YES encoding:NSUTF8StringEncoding error:nil];
- #endif
- NSArray *array = @[@"111",@"222",@{@"key1":@"value1",@"key2":@[@"333",@"444"]}];
- BOOL isOK = [array writeToFile:[cachePath stringByAppendingPathComponent:@"1.plist"] atomically:YES];
- if (!isOK)
- {
- NSLog(@"写入失败");
- }
- else
- {
- NSLog(@"写入成功");
- }
五、数据库和CoreData
2、fmdb 第三方库
(1)线程不安全的
(直接调用 FMDatabase 对象,执行读写等操作)
- //获取Document路径
- NSURL *url = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0];
- //把url对象转换成string
- NSLog(@"%@",[url absoluteString]);//本地路径
- NSLog(@"%@",[url relativePath]);//相对路径
- //设置创建数据库的路径(包行数据库文件的命名)
- NSString *path = [[url relativePath] stringByAppendingPathComponent:@"user.db"];
- /**************** 线程不安全 ****************/
- //初始化database对象
- FMDatabase *database = [FMDatabase databaseWithPath:path];
- //创建并打开/打开数据库:如果数据库不存在,就创建并且打开;否则直接打开。
- [database open];
- //除了查询(select)是query操作之外,其他的都是update操作
- // [database executeUpdate:<#(NSString *), ...#>];
- // [database executeQuery:<#(NSString *), ...#>];
- //关闭数据库
- [database close];
(2)线程安全的
(调用 FMDatabaseQueue 对象来执行数据库操作)
我们知道直接使用libsqlite3进行数据库操作其实是线程不安全的,如果遇到多个线程同时操作一个表的时候可能会发生意想不到的结果。为了解决这个问题建议在多线程中使用FMDatabaseQueue对象,相比FMDatabase而言,它是线程安全的。
保证每一次读写等操作都不是异步的,而是同步的;同时有两个线程进入该操作时,按顺序执行。
- /**************** 线程安全 ****************/
- FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
- //执行sql语句 (线程安全的数据库不需要打开,inDatabase内含打开数据库的代码)
- [queue inDatabase:^(FMDatabase *db) {
- //执行增删改查
- NSString *sql = @"select * from xx";
- [db executeQuery:sql];
- [db executeUpdate:@""];
- }];
注意:dataWithPath中的路径参数一般会选择保存到沙箱中的Documents目录中;如果这个参数设置为nil则数据库会在内存中创建;如果设置为@””则会在沙箱中的临时目录创建,应用程序关闭则文件删除。
(3)两个基本方法
FMDatabase类提供了两个方法`executeUpdate:`和`executeQuery:`分别用于执行无返回结果的查询和有返回结果的查询。需要指出的是,如果调用有格式化参数的sql语句时,格式化符号使用“?”而不是“%@”、等。下面是两种情况的代码片段:
a.无返回结果
- -(void)executeNonQuery:(NSString *)sql{
- //执行更新sql语句,用于插入、修改、删除
- if (![self.database executeUpdate:sql]) {
- NSLog(@"执行SQL语句过程中发生错误!");
- }
- }
b.有返回结果
- -(NSArray *)executeQuery:(NSString *)sql{
- NSMutableArray *array=[NSMutableArray array];
- //执行查询sql语句
- FMResultSet *result= [self.database executeQuery:sql];
- while (result.next) {
- NSMutableDictionary *dic=[NSMutableDictionary dictionary];
- for (int i=0; i<result.columnCount; ++i) {
- dic[[result columnNameForIndex:i]]=[result stringForColumnIndex:i];
- }
- [array addObject:dic];
- }
- return array;
- }
对于有返回结果的查询而言,查询完返回一个游标FMResultSet,通过遍历游标进行查询。而且FMDB中提供了大量intForColumn、stringForColumn等方法进行取值。
(3)事务
用于多线程多个同时操作时,保证操作全都成功,否则全不成功。
不是只有FMDB才支持事务,sql本身就支持事务,FMDB将其封装成了几个方法来调用,不用自己写对应的sql。其实在在使用libsqlite3操作数据库时也是原生支持事务的(因为这里的事务是基于数据库的,FMDB还是使用的SQLite数据库),只要在执行sql语句前加上“begin transaction;”执行完之后执行“commit transaction;”或者“rollback transaction;”进行提交或回滚即可。另外在Core Data中大家也可以发现,所有的增、删、改操作之后必须调用上下文的保存方法,其实本身就提供了事务的支持,只要不调用保存方法,之前所有的操作是不会提交的。
在FMDB中FMDatabase有beginTransaction、commit、rollback三个方法进行开启事务、提交事务和回滚事务。
- //事务:要么操作都成功,要么都不成功。
- FMDatabase *database = [FMDatabase databaseWithPath:path];
- //打开数据库:如果数据库不存在,就创建并且打开。否则直接打开。
- [database open];
- //创建表
- NSString *sql = @"create table if not exists User (id integer primary key autoincrement, name text)";
- //执行sql
- [database executeUpdate:sql];
- sql = @"insert into User (name) values (?)";
- //添加事务
- [database beginTransaction];
- //插入10条数据
- for (int i = 0; i < 10; i++)
- {
- BOOL isOK = [database executeUpdate:sql,[NSString stringWithFormat:@"test%d",i+1]];
- //在这里模拟失败
- if (i == 5)
- {
- //操作失败,回滚数据库:取消本次操作,把数据库回滚到`添加事务前`的状态
- [database rollback];
- return;
- }
- if (isOK)
- {
- NSLog(@"插入成功");
- }
- else
- {
- NSLog(@"插入失败");
- //正常的程序设计,都会在插入失败这里设置回滚
- [database rollback];
- return;
- }
- }
- NSLog(@"%@",path);
- //提交事务
- [database commit];
- //运行结果:插入数据成功,但数据库中没有数据 --- 事务。
事务模拟转账:
- //添加事务
- [database beginTransaction];
//转账- BOOL isOK = [database executeUpdate:sql,[NSString stringWithFormat:@"test%d",i+1]];
- if (!isOK)
- {
- [database rollback];
- return;
- }
//收账- isOK = [database executeUpdate:sql,[NSString stringWithFormat:@"test%d",i+1]];
- if (!isOK)
- {
- [database rollback];
- return;
- }
- [database commit];
(4)fmdb的二次封装
3、CoreData
(1)基本操作
(2)实体和实体之间的关系
(3)版本升级和数据迁移
六、KeyChain
()以键值对的形式保存数据