写在前面:iOS本地持久化存储的路径
Documents: 最常用的目录,存放重要的数据,iTunes同步时会备份该目录
Library/Caches: 一般存放体积大,不重要的数据,iTunes同步时不会备份该目录
Library/Preferences: 存放用户的偏好设置,iTunes同步时会备份该目录
tmp: 用于存放临时文件,在程序未运行时可能会删除该文件夹中的数据,iTunes同步时不会备份该目录
存储方式:NSUserDefaults
、Plist
、NSKeyedArchiver
、SQLite3
、Core Data
、Keychain
、FMDB
一、NSUserDefaults 方式存储
1.1 写入
NSUserDefaults *login = [NSUserDefaults standardUserDefaults];
[login setObject:self.passwordField.text forKey:@"token"];
[login synchronize];
1.2 读取
NSUserDefaults *login = [NSUserDefaults standardUserDefaults];
NSString *str = [login objectForKey:@"token"];
1.只能存储OC常用数据类型(NSString、NSDictionary、NSArray、NSData、NSNumber等类型)而不能直接存储自定义数据。
2.键值对存储,直接指定存储类型。
二、plist 方式存储
2.1 写入
#pragma mark - 保存到本地
- (void)saveToLocale {
[self.view endEditing:YES];
if (!self.nameField.text.length) {
[TipUtils showToast:self.view message:@"应用名称不能为空"];
return;
}
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0]stringByAppendingPathComponent:@"password.plist"];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL Exists = [fileManager fileExistsAtPath:path];
if (!Exists) {
_serectArray = [[NSMutableArray alloc] init];
} else {
_serectArray = [[NSMutableArray alloc] initWithContentsOfFile:path];
}
// 这里是修改更新数据
for (int i=0;i <self.serectArray.count;i++) {
if ([self.myPassword.timeID isEqualToString:self.serectArray[i][@"timeID"]]) {
self.serectArray[i][@"name"] = self.nameField.text;
[self.serectArray writeToFile:path atomically:YES];
if ([self.serectArray writeToFile:path atomically:YES]) {
[TipUtils showToast:self.view message:@"保存成功"];
[self performSelector:@selector(delayMethod) withObject:nil afterDelay:1.0f];
} else {
NSLog(@"保存失败");
}
return;
}
}
NSDictionary *dict = [NSDictionary dictionary];
dict = @{@"timeID":[self getItemID], @"name":self.nameField.text};
[self.serectArray addObject:dict];
[self.serectArray writeToFile:path atomically:YES];
if ([self.serectArray writeToFile:path atomically:YES]) {
[TipUtils showToast:self.view message:@"保存成功"];
[self performSelector:@selector(delayMethod) withObject:nil afterDelay:1.0f];
NSLog(@"存储的数据有:%@",self.serectArray);
} else {
NSLog(@"保存失败");
}
}
#pragma mark - 延时函数
- (void)delayMethod {
[self.navigationController popViewControllerAnimated:YES];
}
2.2 读取
#pragma mark - 读取数据
- (void)readDataFromPlist {
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0]stringByAppendingPathComponent:@"password.plist"];
NSLog(@"存储路径-%@",path);
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL Exists = [fileManager fileExistsAtPath:path];
if (!Exists) {
_serectArray = [[NSMutableArray alloc] init];
} else {
_serectArray = [[NSMutableArray alloc] initWithContentsOfFile:path];
}
}
1.只能存储OC常用数据类型(NSString、NSDictionary、NSArray、NSData、NSNumber等类型)而不能直接存储自定义模型对象。
· 如果想用对象,只能再用明杰的框架进行字典转模型了。
· 或者想存储自定义模型对象 -> 只能将自定义模型对象转换为字典存储;
2.系统的plist文件,只能读取,不行写入,自定义的可读可写
3.plist文件存储的位置,但一般存在Documents中
4.如果存储图片路径的话,一定要存储相对位置,因为每次启动APP,plist文件的路径就会变化,自然图片的位置也就变化了。
三、NSKeyedArchiver归档(NSCoding)
- 归档解档最大的好处在于可以存储自定义对象数据
- 归档解档要注意ios版本
- 新建一个数据模型类
Person
#import <Foundation/Foundation.h>
@interface Person : NSObject<NSCoding>
@property(nonatomic, copy) NSString *name;
@end
#import "Person.h"
@implementation Person
-(id)init {
if (self == nil) {
self = [super init];
}
return self;
}
- (void)encodeWithCoder:(nonnull NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
}
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)aDecoder {
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
}
return self;
}
@end
- 新建一个归档解档通用工具类
ArchiveTools
#import <Foundation/Foundation.h>
@interface ArchiveTools : NSObject
+ (BOOL)archiveObject:(id)object prefix:(NSString *)prefix;
+ (id)unarchiveClass:(Class)class prefix:(NSString *)prefix;
+ (NSString *)getPathWithPrefix: (NSString *)prefix;
@end
#import "ArchiveTools.h"
@implementation ArchiveTools
#pragma mark - 归档解档
+ (BOOL)archiveObject:(id)object prefix:(NSString *)prefix {
if (!object) {
return NO;
}
NSError *error;
if (@available(iOS 11.0, *)) {
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object
requiringSecureCoding:YES
error:&error];
if (error)
return NO;
[data writeToFile:[self getPathWithPrefix:prefix] atomically:YES];
} else {
NSData*data = [NSKeyedArchiver archivedDataWithRootObject:object];
[data writeToFile:[self getPathWithPrefix:prefix] atomically:YES];
}
return YES;
}
+ (id)unarchiveClass:(Class)class prefix:(NSString *)prefix {
NSError *error;
NSData *data = [[NSData alloc] initWithContentsOfFile:[self getPathWithPrefix:prefix]];
//会调用对象的initWithCoder方法
if (@available(iOS 11.0, *)) {
id content = [NSKeyedUnarchiver unarchivedObjectOfClass:class fromData:data error:&error];
if (error) {
return nil;
}
return content;
} else {
id content = [NSKeyedUnarchiver unarchiveObjectWithData:data];
return content;
}
}
#pragma mark - 存放文件的路径
+ (NSString *)getPathWithPrefix: (NSString *)prefix {
// document路径
NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0];
// 自定义一个文件夹
NSString *filePathFolder = [documentPath stringsByAppendingPaths:@[@"archiveTemp"]].firstObject;
if (![[NSFileManager defaultManager] fileExistsAtPath:filePathFolder]) {
[[NSFileManager defaultManager] createDirectoryAtPath:filePathFolder withIntermediateDirectories:YES attributes:nil error:nil];
}
NSString *path = [NSString stringWithFormat:@"%@/%@.archive",filePathFolder,prefix];
NSLog(@"%@",path);
return path;
}
3.调用使用
#import "ViewController.h"
#import "Person.h"
#import "ArchiveTools.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
person.name = @"I am Lili";
// 存储
BOOL isSuccess = [ArchiveTools archiveObject:person prefix:NSStringFromClass(person.class)];
NSLog(@"啦啦-%@",NSStringFromClass(person.class));
if (isSuccess) {
NSLog(@"存储成功");
} else {
NSLog(@"存储失败");
}
// 读取
Person *content = [ArchiveTools unarchiveClass:Person.class prefix:NSStringFromClass(Person.class)];
NSLog(@"内容是-%@",content);
NSLog(@"具体-%@", content.name);
//删除归档文件
NSFileManager *defaultManager = [NSFileManager defaultManager];
if ([defaultManager isDeletableFileAtPath:[ArchiveTools getPathWithPrefix:NSStringFromClass(Person.class)]]) {
[defaultManager removeItemAtPath:[ArchiveTools getPathWithPrefix:NSStringFromClass(Person.class)] error:nil];
}
}
四、SQLite3
1. 在项目中导入libsqlite3.0.tbd框架
2.使用数据库
#import "ViewController.h"
#import <sqlite3.h>
@interface ViewController ()
@property(nonatomic, assign) int res;
@end
@implementation ViewController
{
sqlite3 *db; // 声明对象
}
- (void)viewDidLoad {
[super viewDidLoad];
[self createDB];
[self createTable];
[self insertData];
[self selectData];
// 关闭数据库
sqlite3_close(db);
}
#pragma mark - 数据库相关操作
- (void)createDB {
// 创建数据库路径
NSString *dbPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0]stringByAppendingPathComponent:@"test.sqlite"];
NSLog(@"数据库路径-%@",dbPath);
// 创建或者打开数据库
const char *p = [dbPath UTF8String];
int res = sqlite3_open(p, &db);
self.res = res;
}
// 一、创建表格
- (void)createTable {
if(self.res == SQLITE_OK) {
NSLog(@"数据库成功打开");
NSString *sql = @"create table if not exists temps (t_id integer primary key autoincrement, t_name varchar(20))";
if ([self execNoQueryWithSQL:sql]) {
NSLog(@"创建表格成功");
} else {
NSLog(@"创建表格失败");
}
} else {
NSLog(@"数据库未正常打开-%d",self.res);
}
}
// 二、插入数据
- (void)insertData {
if (self.res == SQLITE_OK) {
NSString *insert_sql = @"insert into temps (t_name) values('banala')";
if ([self execNoQueryWithSQL:insert_sql]) {
NSLog(@"成功插入数据");
} else {
NSLog(@"插入数据失败");
}
} else {
NSLog(@"数据库未正常打开-%d",self.res);
}
}
// 三、删除数据
- (void)deleteData {
if (self.res == SQLITE_OK) {
NSString *delete_sql = @"delete from temps where t_id=2";
if ([self execNoQueryWithSQL:delete_sql]) {
NSLog(@"成功删除数据");
} else {
NSLog(@"删除数据失败");
}
} else {
NSLog(@"数据库未正常打开-%d",self.res);
}
}
// 四、修改数据
- (void)updataData {
if (self.res == SQLITE_OK) {
NSString *update_sql = @"update temps set t_name='ios' where t_id=1";
if ([self execNoQueryWithSQL:update_sql]) {
NSLog(@"成功更新数据");
} else {
NSLog(@"更新数据失败");
}
} else {
NSLog(@"数据库未正常打开-%d",self.res);
}
}
// 五、查询简单数据,无参数
- (void)selectData {
if (self.res == SQLITE_OK) {
// NSString *select_sql1 = @"select * from temps where t_id=1";
NSString *select_sql1 = @"select * from temps";
sqlite3_stmt *stmt1 = [self execQueryWithSQL:select_sql1];
while (sqlite3_step(stmt1) == SQLITE_ROW) {
int t_id = sqlite3_column_int(stmt1, 0);
const unsigned char *t_name = sqlite3_column_text(stmt1, 1);
NSString *name = [NSString stringWithUTF8String:(char *)t_name];
NSLog(@"%i %@",t_id,name);
}
// 释放stmt statement
sqlite3_finalize(stmt1);
} else {
NSLog(@"数据库未正常打开-%d",self.res);
}
}
// 六、查询数据2 参数化的sql语句 查找 id>2 并且名字以p开头的 用 ?占位
- (void)selectDataWithParams {
if (self.res == SQLITE_OK) {
int seachId2 = 1;
NSString *seach_name = @"p%";
NSString *seach_sql = @"select * from temps where t_id>? and t_name like ?";
sqlite3_stmt *stmt6 = [self execQueryWithSQL:seach_sql andWithParams:@[[NSNumber numberWithInt:seachId2],seach_name]];
//准备执行(相当于点击run query),执行的时候是一行一行的执行
while (sqlite3_step(stmt6) == SQLITE_ROW) {
//按照当前列的类型选数据,列数从0开始
int t_id = sqlite3_column_int(stmt6, 0);
const unsigned char *t_name = sqlite3_column_text(stmt6, 1);
NSString *name = [NSString stringWithUTF8String:(char*)t_name];
NSLog(@"..>>>>>>...%i %@",t_id,name);
}
sqlite3_finalize(stmt6);
} else {
NSLog(@"数据库未正常打开-%d",self.res);
}
}
#pragma mark - 执行除查询以外的操作
- (BOOL)execNoQueryWithSQL:(NSString *)sql {
/*
执行
参数1:sqlite3 对象
参数2:c形式的 sql语句
参数3:回调函数
参数4:回调函数的参数
参数5:错误信息(可以char类型指针接受错误信息,用来查错使用)
*/
char *error;
int result = sqlite3_exec(db, [sql UTF8String], NULL, NULL, &error);
if (result == SQLITE_OK) {
return YES;
} else {
NSLog(@"%s",error);
}
return NO;
}
#pragma mark - 返回查询结果集,无参数
-(sqlite3_stmt *)execQueryWithSQL:(NSString *)sql {
sqlite3_stmt *stmt;
/*
准备执行查询的sql语句 (相当于把查询语句写好)
参数3:sql语句长度,通常用-1表示(系统会自动计算),也可以用strlength函数计算
参数4:sql_stmt对象 (执行的对象)
参数5:未执行的sql语句
*/
int pre_res = sqlite3_prepare_v2(db, [sql UTF8String], -1, &stmt, NULL);
if (pre_res == SQLITE_OK) {
return stmt;
}
return NULL;
}
#pragma mark - 返回查询结果集m,有参数
-(sqlite3_stmt *)execQueryWithSQL:(NSString *)sql andWithParams:(NSArray *)params{
sqlite3_stmt *stmt;
int pre_res = sqlite3_prepare_v2(db, [sql UTF8String], -1, &stmt, NULL);
if (pre_res == SQLITE_OK) {
if (params!=nil) {
for (int i = 0 ; i<params.count; i++) {
id obj = params[i];
//绑定的数据类型可能为NSString或者NSNumber,或者数据为空,分别判断
if (obj == nil) {
// 数据为空
sqlite3_bind_null(stmt, i+1);
} else if ([obj respondsToSelector:@selector(objCType)]) {
//当前的绑定的数据类型位NSNumber
//NSNumber判断包装的是int?longInt?shortInt?float?double?
/*
strstr(参数1,参数2) (strstr() c中函数搜索一个字符串在另一个字符串中的第一次出现,则该函数返回第一次匹配的字符串的地址,找不到返回NULL)
判断参数1中的字符在参数2的字符串char*中出现的索引
[obj objCType] 如果obj是int返回字符串i
*/
if (strstr("ilsILS", [obj objCType])) {
/*
绑定参数 如果有where
参数1:sqlite_stmt对象 (statement结果集)
参数2:占位符索引 从1开始
参数3:替代占位符的真实参数
*/
sqlite3_bind_int(stmt, i+1, [obj intValue]);
} else if (strstr("fdFD", [obj objCType])){
sqlite3_bind_double(stmt, i+1, [obj doubleValue]);
} else {
stmt = nil;
}
} else if ([obj respondsToSelector:@selector(UTF8String)]) {
//当前的绑定的数据类型为NSString 判断是否有UTF8String方法
//用bind替换占位符 索引从1开始
sqlite3_bind_text(stmt, i+1, [obj UTF8String], -1, NULL);
} else {
stmt = nil;
}
}
}
return stmt;
}
return NULL;
}
@end
五、Core Data
Core Data是iOS5之后才出现的一个框架,提供了直接使用SQLite数据库的大部分灵活性,它提供了对象-关系映射(ORM)的功能,即能够将OC对象转化成数据,保存在SQLite数据库文件中,也能够将保存在数据库中的数据还原成OC对象,通过CoreData管理应用程序的数据模型,可以极大程度减少需要编写的代码数量!
- 废话不多说,直接上代码!
//创建数据库
- (void)createSqlite{
//1、创建模型对象
//获取模型路径
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CoreData__" withExtension:@"momd"];
//根据模型文件创建模型对象
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
//2、创建持久化存储助理:数据库
//利用模型对象创建助理对象
NSPersistentStoreCoordinator *store = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
//数据库的名称和路径
NSString *docStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *sqlPath = [docStr stringByAppendingPathComponent:@"coreData.sqlite"];
NSLog(@"数据库 path = %@", sqlPath);
NSURL *sqlUrl = [NSURL fileURLWithPath:sqlPath];
NSError *error = nil;
//设置数据库相关信息 添加一个持久化存储库并设置存储类型和路径,NSSQLiteStoreType:SQLite作为存储库
[store addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:sqlUrl options:nil error:&error];
if (error) {
NSLog(@"添加数据库失败:%@",error);
} else {
NSLog(@"添加数据库成功");
}
//3、创建上下文 保存信息 操作数据库
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
//关联持久化助理
context.persistentStoreCoordinator = store;
_context = context;
}
六、Keychain(SQLite API进行封装的库)
- Keychain存储也并不是绝对安全,越狱设备可以拿到
- 程序卸载还存在
- 对于每个应用来说,keychain都有两个访问区,私有区和公共区
私有区是一个闭合的存储区域,每个应用只能操作自己的私有区,本应用存储的任何数据对其他程序不可见,其他程序也没有权限访问这个私有区。(可以理解为存在钥匙串的沙盒)。
公共区,apple提供给同一个开发者账号开发的多个app之间的一个数据共享模块。现在只局限于同一个开发者账号下的不同app之间数据共享。这个区域是独立于私有区的另外一个数据存储空间。实现多个应用间共同访问一些数据。
缺陷:Keychain变化的几种情况
1.越狱机
2.部分操作系统bug
3.应用卸载后升级iOS系统
七、第三方FMDB,BGFMDB
- 下载FMDB框架,程序导入sqlite3.0框架
- 废话不多说,上代码
#import "ViewController.h"
#import "FMDB.h"
#import "Person.h"
@interface ViewController ()
{
FMDatabase *db;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self createDB];
[self deleteData];
[self queryData];
}
-(void)createDB {
// 1.
NSString *docuPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *dbPath = [docuPath stringByAppendingPathComponent:@"test.db"];
NSLog(@"!!!dbPath = %@",dbPath);
//2.创建对应路径下数据库
db = [FMDatabase databaseWithPath:dbPath];
//3.在数据库中进行增删改查操作时,需要判断数据库是否open
[db open];
if (![db open]) {
NSLog(@"db open fail");
return;
}
//4.数据库中创建表(可创建多张)
NSString *sql = @"create table if not exists t_student ('ID' INTEGER PRIMARY KEY AUTOINCREMENT,'name' TEXT NOT NULL, 'phone' TEXT NOT NULL,'score' INTEGER NOT NULL)";
//5.执行更新操作 此处database直接操作,不考虑多线程问题,多线程问题,用FMDatabaseQueue 每次数据库操作之后都会返回bool数值,YES,表示success,NO,表示fail,可以通过 @see lastError @see lastErrorCode @see lastErrorMessage
BOOL result = [db executeUpdate:sql];
if (result) {
NSLog(@"create table success");
}
[db close];
}
- (void)insertData {
[db open];
// 插入
BOOL result2 = [db executeUpdate:@"insert into 't_student'(ID,name,phone,score) values(?,?,?,?)" withArgumentsInArray:@[@113,@"x3",@"13",@53]];
if (result2) {
NSLog(@"insert into 't_studet' success");
[self showAlertWithTitle:@"insert success" message:nil person:nil];
} else {
[self showAlertWithTitle:[db lastError].description message:nil person:nil];
}
[db close];
}
-(void)deleteData{
[db open];
BOOL result = [db executeUpdate:@"delete from 't_student' where ID = ?" withArgumentsInArray:@[@153]];
if (result) {
NSLog(@"delete from 't_student' success");
[self showAlertWithTitle:@"delete success" message:nil person:nil];
} else {
[self showAlertWithTitle:[db lastError].description message:nil person:nil];
}
[db close];
}
-(void)updateData{
[db open];
BOOL result = [db executeUpdate:@"update 't_student' set ID = ? where name = ?" withArgumentsInArray:@[@153,@"x3"]];
if (result) {
NSLog(@"update 't_student' success");
[self showAlertWithTitle:@"update success" message:nil person:nil];
} else {
[self showAlertWithTitle:[db lastError].description message:nil person:nil];
}
[db close];
}
-(void)queryData {
[db open];
FMResultSet *result = [db executeQuery:@"select * from 't_student' where ID = ?" withArgumentsInArray:@[@153]];
NSMutableArray *arr = [NSMutableArray array];
while ([result next]) {
Person *person = [Person new];
person.ID = [result intForColumn:@"ID"];
person.name = [result stringForColumn:@"name"];
person.phone = [result stringForColumn:@"phone"];
person.score = [result intForColumn:@"score"];
[arr addObject:person];
NSLog(@"从数据库查询到的人员 %d-%@-%@-%d",person.ID, person.name,person.phone,person.score);
[self showAlertWithTitle:@"query success" message:nil person:person];
}
}
-(void)showAlertWithTitle:(NSString *)title
message:(NSString *)message
person:(Person *)person
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"sure" style:UIAlertActionStyleDefault handler:nil];
[alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
textField.text = person.name ? person.name : @"other";
}];
[alert addAction:cancelAction];
[self presentViewController:alert animated:YES completion:^{
}];
}
@end