因为豺狼也是在学习阶段, 可能会有一些纰漏, 还请各位看官无情指出, 如多少有些助益的话, 也请点个红心, 非常感谢~下面让我们开始吧!
沙盒
先理解一下沙盒机制, 简单说就是除了APP自己的目录外, 不允许你在其他地方存取数据. 整个沙盒目录下有三个子目录:Documents
Library
tmp
.
Documents
目录下的数据在连接iTunes时会进行同步, 适合存储重要数据, 如用户信息啥的.
Library
目录下又有两个子目录Caches
Preferences
, 一个是缓存, 一个是应用设置信息.
Library/Caches
目录存储体积大且不需要备份的数据, Library/Preferences
目录保存的设置信息会在iTunes连接时同步.
tmp
用来保存临时数据, 在应用关闭时候就自动删除掉了.
具体调取路径的代码如下:
NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *libPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;
NSString *cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
NSString *preferPath = NSSearchPathForDirectoriesInDomains(NSPreferencePanesDirectory, NSUserDomainMask, YES).firstObject;
NSString *tmpPath = NSTemporaryDirectory();
NSLog(@"\ndocPath : %@\nlibPath : %@\ncachesPath : %@\npreferPath : %@\ntmpPath : %@", docPath, libPath, cachesPath, preferPath, tmpPath);
另外说一句, 这些都是保存在硬盘
Data
目录中, 和应用程序本身的资源文件和可执行文件是分开的, 后者在Bundle
目录下.代码如下:
NSString *path = [[NSBundle mainBundle] bundlePath];
NSLog(@"path : %@", path);
数据持久化
所谓数据持久化就是将数据保存到硬盘, 以便于下次进入应用时快速调用. 一般的方法有如下几种:
- property list (属性列表)
- preference (偏好设置)
- NSKeyedArchiver (加密形式)
- SQLite3/FMDB (嵌入式数据库)
- CoreData (面向对象的嵌入式数据库)
property list
plist文件是将特定对象, 通过XML方式保存到目录中. 可以被序列化的类型只有OC中的对象类型(String
Array
Dictionary
Data
Number
).
// 创建地址
NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *plistPath = [docPath stringByAppendingPathComponent:@"PlistFile.plist"];
NSDictionary *dict = @{@"key1":@"value1", @"key2":@"value2"};
// 存储
BOOL ret = [dict writeToFile:plistPath atomically:YES];
if (!ret) {
NSLog(@"写入失败");
}
// 读取
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
NSLog(@"\nresult : %@", result);
- 在write方法里, 为了安全,
atomically
一般情况都是YES. 它表示是否需要先写入一个辅助文件, 再把辅助文件拷贝到目标文件地址. - 能够进行
writeToFile
的只有Array
和Dictionary
类型, 反序列化同样是调用arrayWithContentsOfFile
或dictionaryWithContentsOfFile
. - XML序列化缺点也很明显, 一点是操作的对象有限, 另一点也是最为重要的一点, 保存方式为明文保存,
!千万不要用来保存账号密码<手动滑稽>
preference
即NSUserDefaults
, 豺狼刚入坑时最爱用的方法...代码如下:简单好用却很low的方法,
// 获取偏好设置文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
// 存储
[userDefaults setObject:@"this is object" forKey:@"object"];
[userDefaults setBool:YES forKey:@"man"];
[userDefaults setObject:@[@"1", @"2", @"3"] forKey:@"array"];
// 立即保存
[userDefaults synchronize];
// 读取
NSString *object = [userDefaults objectForKey:@"object"];
BOOL man = [userDefaults boolForKey:@"man"];
NSArray *array = [userDefaults objectForKey:@"array"];
NSLog(@"\nobject : %@\nman : %@\narray : %@", object, man?@"YES":@"NO", array);
另外一个NSUserDefaults
比较少用却更直观的方法:没用过, 所以也列出来
-
创建一个偏好设置文件:
- 设置其中的
Root.plist
文件
- 在手机
设置
的最下面找到应用的偏好设置
用户在其中进行设置后, 相关设置的值可以通过预先定义好的Identifier
来取到, 设置的内容也是保存在Library/Preferences
目录下的. 代码如下:
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
BOOL toggleSwitch = [userDefaults boolForKey:@"toggle_switch_ide"];
NSString *textfield = [userDefaults objectForKey:@"textfield_ide"];
CGFloat slider = [userDefaults floatForKey:@"slider_ide"];
NSLog(@"\ntogleSwitch : %@\ntextfield : %@\nslider : %f", toggleSwitch?@"YES":@"NO", textfield, slider);
- 偏好设置一般是用来保存应用设置信息的, 最好不要在其中保存大量其他数据
- 调用
synchronize
方法会进行立即保存, 否则系统会根据I/O不定时刻保存. - 偏好设置文件保存在
Library/Preferences
目录下, 以工程的Bundle Identifier
为名的plist文件中. - 因为也属于XML序列化, 缺点同第一种方法.
NSKeyedArchiver
如果要针对更多的对象类型或者信息需要加密的话就需要使用NSKeyedArchiver
归档, 它也是一种序列化的形式, 凡是遵守NSCoding
协议的对象都可以进行NSKeyedArchiver
归档. 代码如下:
创建自定义对象类型Person.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface Person : NSObject <NSCoding>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) NSDate *birthday;
@end
Person.m
#import "Person.h"
@implementation Person
#pragma mark - NSCoding
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
// 解码
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [aDecoder decodeIntegerForKey:@"age"];
self.birthday = [aDecoder decodeObjectForKey:@"birthday"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
// 编码
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.age forKey:@"age"];
[aCoder encodeObject:self.birthday forKey:@"birthday"];
}
#pragma mark - description
- (NSString *)description {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"yyyy-MM-dd";
return [NSString stringWithFormat:@"\nname : %@\nage : %li\nbirthday : %@", self.name, self.age, [formatter stringFromDate:self.birthday]];
}
@end
自定对象建好后就是对其进行存储读取操作, 代码如下:
// 归档文件路径
NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
NSLog(@"path : %@", filePath);
// 实例化对象
Person *person = [[Person alloc] init];
person.name = @"韩梅梅";
person.age = 16;
person.birthday = [NSDate dateWithTimeIntervalSince1970:360000];
// 存储
[NSKeyedArchiver archiveRootObject:person toFile:filePath];
// 读取
Person *person2 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
NSLog(@"\nperson2 : %@", person2);
-
NSKeyedArchiver
归档可以对多个对象进行归档, 在复杂归档时就需要针对不同对象设置对应Key.不过用的并不多.
- 保存文件的扩展名随意
.如: .torrent
- 归档对象必须遵守
N�SCoding
协议以及协议的initWithCoder
和encodeWithCoder
方法. - 如果对自定义类的子类归档时, 需要先实现父类的编码解码方法, 即
[super encodeWithCoder:aCoder]
与[super initWithCoder:aDecoder]
.
SQLite3/FMDB
因为前几种方法属于覆盖式存储, 如果要改变其中某一条, 需要整体取出修改后再行归档. 相比较之前的几种方法, SQLite
方便进行增删改查, 更适合存储读取大量数据内容.
因为入坑尚浅基础也弱, 豺狼对于使用C语言的SQLite3
真的力不从心... 所以只研究了第三方数据库框架FMDB
. 因为FMDB
是用OC的方式对SQLite
进行的封装, 所以相对于C语言而言, 更利于理解, 也更加轻便, 提升开发效率<手动滑稽>. GitHub-FMDB
因为FMDB
是对SQLite
的封装, 所以使用前先导入依赖库libsqlite3.0
, 并将fmdb
文件导入工程.
- FMDatabase 执行SQL语句的主体.
- FMResultSet 使用
FMDatabase
执行查询后的结果集. - FMDatabaseQueue 类似队列的作用.
具体使用代码如下:
// 数据库路径
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
// 在安全线程操作
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
[queue inDatabase:^(FMDatabase *db) {
// 打开数据库
if (![db open]) {
NSLog(@"数据库打开失败");
}
// 创建表
NSString *createSql = @"create table if not exists PersonList(id integer primary key autoincrement, name varchar, age interger)";
if (![db executeUpdate:createSql]) {
NSLog(@"创建表失败");
}
// 插入数据
NSString *insertSql = @"insert into PersonList(name, age) values(?, ?)";
// method1
if (![db executeUpdate:insertSql, @"李雷", @14]) {
NSLog(@"插入数据失败");
}
// method2
if (![db executeUpdate:insertSql withArgumentsInArray:@[@"韩梅梅", @13]]) {
NSLog(@"插入数据失败");
}
// 删除数据
NSString *deleteSql = @"delete from PersonList where id%2 = 0";
if (![db executeUpdate:deleteSql]) {
NSLog(@"删除数据失败");
}
// 修改数据
NSString *updateSql = @"update PersonList set name=?, age=? where id < 5";
if (![db executeUpdate:updateSql, @"Jim", @25]) {
NSLog(@"修改数据失败");
}
// 查询数据
NSString *selectSql = @"select * from PersonList where id > 2";
FMResultSet *result = [db executeQuery:selectSql];
while (result.next) {
NSString *name = [result stringForColumn:@"name"];
NSInteger age = [result intForColumn:@"age"];
NSLog(@"\nname:%@\nage:%li", name, age);
}
if (![db close]) {
NSLog(@"关闭数据库失败");
}
}];
在处理大量数据的时候, FMDB可以方便的添加事务, 代码如下:
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
// 打开数据库
if (![db open]) {
NSLog(@"数据库打开失败");
}
// 创建表
NSString *createSql = @"create table if not exists PersonList(id integer primary key autoincrement, name varchar, age interger)";
if (![db executeUpdate:createSql]) {
NSLog(@"创建表失败");
}
NSString *insertSql = @"insert into PersonList(name, age) values(?, ?)";
for (NSInteger i=0; i<500; i++) {
if (![db executeUpdate:insertSql, [NSString stringWithFormat:@"name%li", i], @(1+i)]) {
NSLog(@"添加数据失败");
*rollback = YES;
}
}
NSLog(@"添加数据成功");
}];
- 所有的操作都是由
FMDatabase
来完成, 添加删除修改都是有executeUpdate
方法完成, 查询由executeQuery
方法完成, 为了数据安全最好在inDatabase
里操作, 遇到大批量数据的时候在inTransaction
里操作. - 在主线程处理大量数据的时候, 会造成堵塞, 最好使用多线程方法, 或者弹出一个
alert
显得更友好. - FMDB用到的闭包(block)需要注意强引用导致内存泄露.
- 尽量使用
FMDatabaseQueue
, 防止多线程情况下造成的数据混乱.
至于SQL语句, 开发过程中移动端需要的逻辑本来就少, 豺狼也没用认真研究过, 反正单词别写错, 大多数情况都没啥问题~
另外说一句, 还是要花点时间研究SQLite3, 因为豺狼觉得它毕竟使用的是C语言, 在Swift中的迁移性好点.
CoreData
CoreData是iOS5之后苹果推出的, 非常🐂B的一点是它的ORM(对象-关系映射)功能, 说简单点就是我们的Model和数据库可以互相转化了. CoreData本身不需要我们有任何SQL语法基础, 强大但是准备工作略繁琐.继承了苹果API一如既往的繁琐难用...
本部分参考了MJ老师的《Core Data入门》
先来大概了解一下:
- Data Model 模型文件, 描述应用中实体和实体属性, 文件类型为
.xcdatamodeld
.
- Entity 实体, 相当于表, 点击
Add Entity
创建, 点击Attributes
的'+'号创建实体属性, 点击Relationships
的'+'号创建实体关联, 点击Fetched Properties
的'+'创建抓取条件.
- NSManagedObject 数据库取出来的对象, 与
NSDictionary
类似, 使用KVC进行属性存取, 即setValue:forKey:
和valueForKey:
, 一般我们创建它的子类文件进行具体的业务操作.
使用CoreData前需要先导入CoreData.framework
依赖库, 并#import <CoreData/CoreData.h>
. 完成后进行具体操作, 首先搭建上下文环境, 代码如下:
// DataModel模型文件
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil]; // 传nil表示MainBundle
// 数据库地址
NSString *dbPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"CoreData.db"]; // 名字随意
NSURL *url = [NSURL fileURLWithPath:dbPath];
// 模型文件与数据库之间的持久化存储协调者
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// 数据库
NSError *error = nil;
NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error];
if (!store) {
[NSException raise:@"添加数据库错误" format:@"%@", [error localizedDescription]];
}
// 操作的上下文
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
context.persistentStoreCoordinator = psc;
至此, 我们的数据库已经建好, 上下文环境也设置完成了.
NSPersistentStoreCoordinator
作为协调者连接了模型文件和数据库, 是沟通的关键, 然后我们通过NSManagedObjectContext
对这个协调者进行操作.
这里说一下, 在NSManagedObjectContext
初始化的时候使用initWithConcurrencyType:
, 单纯的init
方法已经在iOS9中废弃了, 为了适配低于iOS9的系统, 参数使用NSPrivateQueueConcurrencyType
, 另外两个NSPrivateQueueConcurrencyType
表示私有线程, 不会阻塞主线程, NSMainQueueConcurrencyType
表示主线程, 会堵塞主线程.
添加数据:
// 添加数据
// 实体对象赋值
Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:_context];
[person setValue:@"lilei" forKey:@"name"];
[person setValue:@15 forKey:@"age"];
Card *card = [NSEntityDescription insertNewObjectForEntityForName:@"Card" inManagedObjectContext:_context];
[card setValue:@"12345678910x" forKey:@"num"];
// 两表之间关联
[person setValue:card forKey:@"card"];
// 同步到数据库
NSError *error = nil;
if (![_context save:&error]) {
[NSException raise:@"访问数据库错误" format:@"%@", [error localizedDescription]];
}
如果未建立NSManagedObject
的子类, 则直接使用NSManagedObject
即可.
查询与删除数据
// 查询数据
// 抓取请求
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:_context];
// 排列方式
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];
request.sortDescriptors = @[sort];
// 查询条件
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K < %@", @"age", @"20"];
request.predicate = predicate;
// 查询结果
NSError *error = nil;
NSArray *objs = [_context executeFetchRequest:request error:&error];
if (error) {
[NSException raise:@"查询数据库错误" format:@"%@", [error localizedDescription]];
}
// 删除数据
// 输出结果
__weak typeof(self) weakSelf = self;
[objs enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"name : %@, age : %@", [obj valueForKey:@"name"], [obj valueForKey:@"age"]);
[weakSelf.context deleteObject:obj];
}];
if (![_context save:&error]) {
[NSException raise:@"访问数据库错误" format:@"%@", [error localizedDescription]];
}
对于查询条件NSPredicate
, %K是key path
的替换值, %@是对应的值, 可以为数字日期等格式.
豺狼在网上找到了较为详细语法的介绍:
再介绍个从MJ老师那抄来的方法--打开CoreData的SQL语句输出开关:
1.打开Product,点击EditScheme...
2.点击Arguments,在ArgumentsPassed On Launch中添加2项
1> -com.apple.CoreData.SQLDebug
2> 1
会输出操作记录, 比较方便查看具体的操作.
至此, 豺狼花了三天时间学习整理的iOS数据持久化已经基本完成了, 参考了多位大神的教程, 查验了很多资料, 所以并非全部原创, 如果有侵权之处, 请及时联系我!
期间能想到的基本都写上了, 也许有些内容比较狭隘, 甚至有些地方理解的有误, 还请各位同仁多多交流指教! 如果对你有些许帮助请点个❤️, 关注一下豺狼, 感谢! <感激涕零>