所谓的持久化,就是将数据保存到硬盘中,使得在应用程序或机器重启后可以继续访问之前保存的数据。
- 数据的持久化存储:
- 偏好设置 (Preferences)
- 属性列表 (PropertyLists)
- 对象的归档&解档 (NSKeyedArchiver& NSKeyedUnarchiver)
- SQLite3 (FMDB、SQLCipher、WCDB)
- CoreData
- 数据本地化&共享
- UIPasteboard
- Keychain
- iCloud
- 存储到服务器
...
在介绍各种存储方法之前,有必要说明以下沙盒机制。
iOS应用程序只能在为该改程序创建的文件系统中读取文件,不可以去其它地方访问,此区域被成为沙盒,所以所有的非代码文件都要保存在此,例如图像,图标,声音,映像,属性列表,文本文件等。
- 每个应用程序都有自己的存储空间
- 应用程序不能翻过自己的围墙去访问别的存储空间的内容
- 应用程序请求的数据都要通过权限检测,假如不符合条件的话,不会被放行。
通过这张图只能从表层上理解sandbox是一种安全体系,应用程序的所有操作都要通过这个体系来执行,其中核心内容是:sandbox对应用程序执行各种操作的权限限制。
-
目录结构
- 目录特性
顾名思义,沙盒中不同的文件夹有不同的用途,所以在选择存放目录时,一定要认真选择适合的目录,否则会有审核被拒的可能(主要原因是ios的icloud的同步问题)。
NSSearchPathForDirectoriesInDomains方法用于查找目录,返回指定范围内的指定名称的目录的路径集合。有三个参数:
** directory ** NSSearchPathDirectory类型的enum值,表明我们要搜索的目录名称,比如这里用NSDocumentDirectory表明我们要搜索的是Documents目录。如果我们将其换成NSCachesDirectory就表示我们搜索的是Library/Caches目录。
** * domainMask* ** NSSearchPathDomainMask类型的enum值,指定搜索范围,这里的NSUserDomainMask表示搜索的范围限制于当前应用的沙盒目录。还可以写成NSLocalDomainMask(表示/Library)、NSNetworkDomainMask(表示/Network)等。
** * expandTilde* ** BOOL值,表示是否展开波浪线。我们知道在iOS中的全写形式是/User/userName,该值为YES即表示写成全写形式,为NO就表示直接写成“~”。
"应用程序包": 这里面存放的是应用程序的源文件,包括资源文件和可执行文件。
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
Documents: 苹果建议将程序中建立的或在程序中浏览到的文件数据保存在该目录下,iTunes备份和恢复的时候会包括此目录。最常用的目录,iTunes同步该应用时会同步此文件夹中的内容,适合存储重要数据。这个路径是存放用户重要的文档,并且这个路径下的文件会被苹果备份到iCloud(Apple默认是存放5年)。
这里要特别注意,如果你的APP中在这个路径下保存了“下载”文件,就会导致无法上架。所以一般会放应用程序本身生成的文件,例如用户登录之后的个人信息,或者游戏进度等内容。
NSString *docDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
Library:存储程序的默认设置或其它状态信息。可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。
NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
Library/Caches: 存放缓存文件,iTunes不会备份此目录,此目录下文件不会在应用退出删除。iTunes不会同步此文件夹,适合存储体积大,不需要备份的非重要数据。
NSString *cachesDir = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
Library/Preferences: iTunes同步该应用时会同步此文件夹中的内容,通常保存应用的设置信息。用于存储程序的默认设置或其它状态信息。
tmp:这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。iTunes不会备份此文件夹,系统的磁盘空间不够或者重新启动就会自动清空,所以此目录适合保存应用中的一些临时文件,用完即删,不需要手动删除,但是如果文件过大,建议用完手动删除。
NSString *tmpDir = NSTemporaryDirectory();
iTunes在与iPhone同步时,备份所有的Documents和除Caches以外的Library文件。
iPhone在重启时,会丢弃所有的tmp文件。
模拟器(Simulator)
bundlePath:/Users/MountainX/Library/Developer/CoreSimulator/Devices/F0A41D22-52BE-4F30-A200-F9DD0FA44D3F/data/Containers/Bundle/Application/BCF184B0-B606-4923-A57F-B9627DE0F078/Test.app
docDir:/Users/MountainX/Library/Developer/CoreSimulator/Devices/F0A41D22-52BE-4F30-A200-F9DD0FA44D3F/data/Containers/Data/Application/B5ABA2D0-C061-4278-8DDD-469FE8B6D799/Documents
libDir:/Users/MountainX/Library/Developer/CoreSimulator/Devices/F0A41D22-52BE-4F30-A200-F9DD0FA44D3F/data/Containers/Data/Application/B5ABA2D0-C061-4278-8DDD-469FE8B6D799/Library
cachesDir:/Users/MountainX/Library/Developer/CoreSimulator/Devices/F0A41D22-52BE-4F30-A200-F9DD0FA44D3F/data/Containers/Data/Application/B5ABA2D0-C061-4278-8DDD-469FE8B6D799/Library/Caches
tmpDir:/Users/MountainX/Library/Developer/CoreSimulator/Devices/F0A41D22-52BE-4F30-A200-F9DD0FA44D3F/data/Containers/Data/Application/B5ABA2D0-C061-4278-8DDD-469FE8B6D799/tmp/
真机(Device)
bundlePath:/var/containers/Bundle/Application/359CB843-BB2F-46F6-B1C0-288FE1191C9D/Test.app
docDir:/var/mobile/Containers/Data/Application/8B594896-A51B-4DDA-A69D-81D9793CF33A/Documents
libDir:/var/mobile/Containers/Data/Application/8B594896-A51B-4DDA-A69D-81D9793CF33A/Library
cachesDir:/var/mobile/Containers/Data/Application/8B594896-A51B-4DDA-A69D-81D9793CF33A/Library/Caches
tmpDir:/private/var/mobile/Containers/Data/Application/8B594896-A51B-4DDA-A69D-81D9793CF33A/tmp/
//Mac设置查看隐藏文件
//打开终端,输入命令
//显示Mac隐藏文件的命令:
defaults write com.apple.finder AppleShowAllFiles -bool true
//隐藏Mac隐藏文件的命令:
defaults write com.apple.finder AppleShowAllFiles -bool false
//输完单击Enter键,重新启动Finder就可以了
//重启Finder:鼠标单击窗口左上角的苹果标志-->强制退出-->Finder
一、偏好设置 (Preferences)
- NSUserDefaults是一个单例
[NSUserDefaults standardUserDefaults]
- 轻量级的持久化存储方式
- 专门用来保存应用程序的配置信息的,一般情况下不要在偏好设置中保存其他的数据
- 存储的默认对象必须是一个属性列表,也就是NSData,NSString,NSNumber,NSDate,NSArray或NSDictionary的一个实例(或者集合,实例的一个组合)
/// -setInteger:forKey: is equivalent to -setObject:forKey: except that the value is converted from an NSInteger to an NSNumber.
- (void)setInteger:(NSInteger)value forKey:(NSString *)defaultName;
/*!
-integerForKey: is equivalent to -objectForKey:, except that it converts the returned value to an NSInteger. If the value is an NSNumber, the result of -integerValue will be returned. If the value is an NSString, it will be converted to NSInteger if possible. If the value is a boolean, it will be converted to either 1 for YES or 0 for NO. If the value is absent or can't be converted to an integer, 0 will be returned.
*/
- (NSInteger)integerForKey:(NSString *)defaultName;
如果要存储任何其他类型的对象,则通常应将其存档为NSData (不建议)
-
偏好设置会将所有的数据保存到同一个文件夹中,即存储在Library->Preferences目录下的一个以此应用包名来命名的plist文件
NSUserDefaults会缓存信息以避免每次需要默认值时打开用户的默认数据库。当您设置一个默认值时,它会在当前进程中同步更改,并且异步存储到默认数据库,这样在其他进程才可以在数据库中获取更新后的默认值。
UserDefaults设置数据时,不是立即写入,而是根据I/O情况不定时地把缓存中的数据写入本地磁盘,所以保存到系统的时间是不确定的。
/*!
-setObject:forKey: immediately stores a value (or removes the value if nil is passed as the value) for the provided key in the search list entry for the receiver's suite name in the current user and any host, then asynchronously stores the value persistently, where it is made available to other processes.
*/
- (void)setObject:(nullable id)value forKey:(NSString *)defaultName;
/*!
-objectForKey: will search the receiver's search list for a default with the key 'defaultName' and return it. If another process has changed defaults in the search list, NSUserDefaults will automatically update to the latest values. If the key in question has been marked as ubiquitous via a Defaults Configuration File, the latest value may not be immediately available, and the registered value will be returned instead.
*/
- (nullable id)objectForKey:(NSString *)defaultName;
- 如果需要即刻存储数据,调用synchronize方法同步存储到默认数据库
[[NSUserDefaults standardUserDefaults] synchronize]
- synchronize方法未来将被弃用,建议使用键值观察者(KVO)来通知对特定默认值的任何更新。
也可以在defaultCenter通知中心注册为NSUserDefaultsDidChangeNotification的观察者,以通知本地默认数据库的所有更新。 - UserDefaults类是线程安全的。
- 不要尝试直接访问首选项子系统。修改偏好属性列表文件可能会导致更改丢失,反映更改延迟以及应用程序崩溃。要配置首选项,请改用macOS中的默认命令行工具。
- 除了教育机构中的托管设备外,用户的默认设置本地存储在单个设备上,并保留用于备份和恢复。要在用户连接的设备上同步首选项和其他数据,请改为使用基于iCloud的NSUbiquitousKeyValueStore。
二、属性列表 (PropertyLists)
- plist文件即XML文档,只不过穿了名为plist的马甲而已。
- 如果对象是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,就可以使用
writeToFile:atomically:
方法直接将对象写到属性列表文件中
其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。这是更安全的写入文件方法,一般都写YES。 - 读取时使用类似
arrayWithContentsOfFile:
方法
Xcode中的Info.plist的Source Code如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
三、对象的归档&解档 (NSKeyedArchiver& NSKeyedUnarchiver)
- 归档存储一般存储自定义对象,并且自定义对象需要遵守NSCoding协议.
#import <Foundation/Foundation.h>
@interface Person : NSObject<NSCoding>
@property(nonatomic,copy)NSString *name;
@property(nonatomic,copy)NSString *phone;
@end
#import "Person.h"
@implementation Person
//编码
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeObject:self.phone forKey:@"phone"];
}
//解码
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
self.phone = [aDecoder decodeObjectForKey:@"phone"];
}
return self;
}
@end
- 如果父类也遵守了NSCoding协议,那么需要
在encodeWithCoder:
方法中加上[super encodeWithCoder:aCoder]
;
在initWithCoder:
方法中将self = [super init]
写成self = [super initWithCoder:aDecoder]
。
确保继承自父类的实例变量也能被解码和解码。
特别提醒
如果父类使用runtime获取到property list
然后遍历property进行decode和encode操作
那么使用super的方法相当于执行了子类的方法,因为在父类的方法中执行[self class]和在子类中执行[self class]发送的消息是一样的。
#pragma mark - NSCoding
//特别提醒:仅限用于继承于NSObject的数据模型的归档&解档
-(void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int count=0;
Ivar *ivar=class_copyIvarList([self class], &count);
for (int i=0; i<count; i++) {
Ivar iva=ivar[i];
const char *name=ivar_getName(iva);
NSString*strName=[NSString stringWithUTF8String:name];
id value=[self valueForKey:strName];
[aCoder encodeObject:value forKey:strName];
}
free(ivar);
}
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self=[super init]) {
unsigned int count=0;
Ivar *ivar=class_copyIvarList([self class], &count);
for (int i=0; i<count; i++) {
Ivar iva=ivar[i];
const char*name=ivar_getName(iva);
NSString *strName=[NSString stringWithUTF8String:name];
id value=[aDecoder decodeObjectForKey:strName];
if (value) {
[self setValue:value forKey:strName];
}
}
free(ivar);
}
return self;
}
所以使用runtime进行编码&解码时,正确的使用姿势如下:
#pragma mark - NSCoding
-(void)encodeWithCoder:(NSCoder *)aCoder
{
Class currentClass = self.class;
if (currentClass == NSObject.class) {
return;
}
while (currentClass && currentClass != [NSObject class])
{
unsigned int count = 0;
Ivar *ivar = class_copyIvarList(currentClass, &count);
if (count>0) {
for (int i=0;i<count;i++) {
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar[i])];
[aCoder encodeObject: [self valueForKey:key] forKey:key];
}
}
currentClass = class_getSuperclass(currentClass);
free(ivar);
}
}
-(id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init]) {
Class currentClass = self.class;
while (currentClass && currentClass != [NSObject class])
{
unsigned int count = 0;
Ivar *ivar = class_copyIvarList(currentClass, &count);
if (count > 0) {
for (int i = 0;i < count;i++) {
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar[i])];
id value = [aDecoder decodeObjectForKey:key];
if (value) {
[self setValue:value forKey:key];
}
}
}
currentClass = class_getSuperclass(currentClass);
free(ivar);
}
}
return self;
}
用class_copyPropertyList
也是可以的,不过得encodeWithCoder
和initWithCoder
必须配套,因为ivar_getName
得到的属性名称带下划线,而property_getName
不带下划线!
#pragma mark - NSCoding
-(void)encodeWithCoder:(NSCoder *)aCoder
{
Class currentClass = self.class;
if (currentClass == NSObject.class) {
return;
}
while (currentClass && currentClass != [NSObject class])
{
unsigned int count = 0;
objc_property_t *pList = class_copyPropertyList(currentClass, &count);
if (count>0) {
for (int i=0;i<count;i++) {
NSString *key = [NSString stringWithUTF8String:property_getName(pList[i])];
[aCoder encodeObject: [self valueForKey:key] forKey:key];
}
}
currentClass = class_getSuperclass(currentClass);
free(pList);
}
}
-(id)initWithCoder:(NSCoder *)aDecoder
{
Class currentClass = self.class;
if (currentClass == NSObject.class) {
return nil;
}
while (currentClass && currentClass != [NSObject class])
{
unsigned int count = 0;
objc_property_t *pList = class_copyPropertyList(currentClass, &count);
if (count > 0) {
for (int i = 0;i < count;i++) {
NSString *key = [NSString stringWithUTF8String:property_getName(pList[i])];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
currentClass = class_getSuperclass(currentClass);
free(pList);
}
return self;
}
- 对象归档是调用NSKeyedArchiver的工厂方法
archiveRootObject: toFile:
方法
NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
Person *person = [[Person alloc] init];
person.avatar = self.avatarView.image;
person.name = self.nameField.text;
person.age = [self.ageField.text integerValue];
[NSKeyedArchiver archiveRootObject:person toFile:file];
- 解档对象就调用NSKeyedUnarchiver的一个工厂方法
unarchiveObjectWithFile:
即可
NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
if (person) {
self.avatarView.image = person.avatar;
self.nameField.text = person.name;
self.ageField.text = [NSString stringWithFormat:@"%ld", person.age];
}
四、SQLite3 (FMDB)
之前的所有存储方法,都是覆盖存储。如果想要增加一条数据就必须把整个文件读出来,然后修改数据后再把整个内容覆盖写入文件。所以它们都不适合存储大量的内容。
1.字段类型
表面上SQLite将数据分为以下几种类型:
integer : 整数
real : 实数(浮点数)
text : 文本字符串
blob : 二进制数据,比如文件,图片之类的
实际上SQLite是无类型的。即不管你在创表时指定的字段类型是什么,存储是依然可以存储任意类型的数据。而且在创表时也可以不指定字段类型。SQLite之所以什么类型就是为了良好的编程规范和方便开发人员交流,所以平时在使用时最好设置正确的字段类型!主键必须设置成integer
- 准备工作
准备工作就是导入依赖库啦,在iOS中要使用SQLite3,需要添加库文件:libsqlite3.dylib并导入主头文件,这是一个C语言的库,所以直接使用SQLite3还是比较麻烦的。
3.使用
- 创建数据库并打开
操作数据库之前必须先指定数据库文件和要操作的表,所以使用SQLite3,首先要打开数据库文件,然后指定或创建一张表。
/**
* 打开数据库并创建一个表
*/
- (void)openDatabase {
//1.设置文件名
NSString *filename = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
//2.打开数据库文件,如果没有会自动创建一个文件
NSInteger result = sqlite3_open(filename.UTF8String, &_sqlite3);
if (result == SQLITE_OK) {
NSLog(@"打开数据库成功!");
//3.创建一个数据库表
char *errmsg = NULL;
sqlite3_exec(_sqlite3, "CREATE TABLE IF NOT EXISTS t_person(id integer primary key autoincrement, name text, age integer)", NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@"错误:%s", errmsg);
} else {
NSLog(@"创表成功!");
}
} else {
NSLog(@"打开数据库失败!");
}
}
- 执行指令
使用 sqlite3_exec() 方法可以执行任何SQL语句,比如创表、更新、插入和删除操作。但是一般不用它执行查询语句,因为它不会返回查询到的数据。
/**
* 往表中插入1000条数据
*/
- (void)insertData {
NSString *nameStr;
NSInteger age;
for (NSInteger i = 0; i < 1000; i++) {
nameStr = [NSString stringWithFormat:@"Bourne-%d", arc4random_uniform(10000)];
age = arc4random_uniform(80) + 20;
NSString *sql = [NSString stringWithFormat:@"INSERT INTO t_person (name, age) VALUES('%@', '%ld')", nameStr, age];
char *errmsg = NULL;
sqlite3_exec(_sqlite3, sql.UTF8String, NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@"错误:%s", errmsg);
}
}
NSLog(@"插入完毕!");
}
- 查询指令
前面说过一般不使用 sqlite3_exec() 方法查询数据。因为查询数据必须要获得查询结果,所以查询相对比较麻烦。示例代码如下:
sqlite3_prepare_v2() : 检查sql的合法性
sqlite3_step() : 逐行获取查询结果,不断重复,直到最后一条记录
sqlite3_coloum_xxx() : 获取对应类型的内容,iCol对应的就是SQL语句中字段的顺序,从0开始。根据实际查询字段的属性,使用sqlite3_column_xxx取得对应的内容即可。
sqlite3_finalize() : 释放stmt
/**
* 从表中读取数据到数组中
*/
- (void)readData {
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1000];
char *sql = "select name, age from t_person;";
sqlite3_stmt *stmt;
NSInteger result = sqlite3_prepare_v2(_sqlite3, sql, -1, &stmt, NULL);
if (result == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
char *name = (char *)sqlite3_column_text(stmt, 0);
NSInteger age = sqlite3_column_int(stmt, 1);
//创建对象
Person *person = [Person personWithName:[NSString stringWithUTF8String:name] Age:age];
[mArray addObject:person];
}
self.dataList = mArray;
}
sqlite3_finalize(stmt);
}
4.总结
总得来说,SQLite3的使用还是比较麻烦的,因为都是些c语言的函数,理解起来有些困难。不过在一般开发过程中,使用的都是第三方开源库 FMDB,封装了这些基本的c语言方法,使得我们在使用时更加容易理解,提高开发效率。
1.简介
FMDB是iOS平台的SQLite数据库框架,它是以OC的方式封装了SQLite的C语言API,它相对于cocoa自带的C语言框架有如下的优点:
使用起来更加面向对象,省去了很多麻烦、冗余的C语言代码
对比苹果自带的Core Data框架,更加轻量级和灵活
提供了多线程安全的数据库操作方法,有效地防止数据混乱
2.核心类
FMDB有三个主要的类:
- FMDatabase
一个FMDatabase对象就代表一个单独的SQLite数据库,用来执行SQL语句
- FMResultSet
使用FMDatabase执行查询后的结果集
- FMDatabaseQueue
用于在多线程中执行多个查询或更新,它是线程安全的
3.打开数据库
和c语言框架一样,FMDB通过指定SQLite数据库文件路径来创建FMDatabase对象,但FMDB更加容易理解,使用起来更容易,使用之前一样需要导入sqlite3.dylib。打开数据库方法如下:
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
FMDatabase *database = [FMDatabase databaseWithPath:path];
if (![database open]) {
NSLog(@"数据库打开失败!");
}
值得注意的是,Path的值可以传入以下三种情况:
具体文件路径,如果不存在会自动创建
空字符串@"",会在临时目录创建一个空的数据库,当FMDatabase连接关闭时,数据库文件也被删除
nil,会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁
4.更新
在FMDB中,除查询以外的所有操作,都称为“更新”, 如:create、drop、insert、update、delete等操作,使用executeUpdate:方法执行更新:
//常用方法有以下3种:
- (BOOL)executeUpdate:(NSString*)sql, ...
- (BOOL)executeUpdateWithFormat:(NSString*)format, ...
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments
//示例
[database executeUpdate:@"CREATE TABLE IF NOT EXISTS t_person(id integer primary key autoincrement, name text, age integer)"];
//或者
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES(?, ?)", @"Bourne", [NSNumber numberWithInt:42]];
5.查询
查询方法也有3种,使用起来相当简单:
- (FMResultSet *)executeQuery:(NSString*)sql, ...
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments
查询示例:
//1.执行查询
FMResultSet *result = [database executeQuery:@"SELECT * FROM t_person"];
//2.遍历结果集
while ([result next]) {
NSString *name = [result stringForColumn:@"name"];
int age = [result intForColumn:@"age"];
}
6.线程安全
在多个线程中同时使用一个FMDatabase实例是不明智的。不要让多个线程分享同一个FMDatabase实例,它无法在多个线程中同时使用。 如果在多个线程中同时使用一个FMDatabase实例,会造成数据混乱等问题。所以,请使用 FMDatabaseQueue,它是线程安全的。以下是使用方法:
- 创建队列。
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
- 使用队列
[queue inDatabase:^(FMDatabase *database) {
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_1", [NSNumber numberWithInt:1]];
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_2", [NSNumber numberWithInt:2]];
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_3", [NSNumber numberWithInt:3]];
FMResultSet *result = [database executeQuery:@"select * from t_person"];
while([result next]) {
}
}];
而且可以轻松地把简单任务包装到事务里:
[queue inTransaction:^(FMDatabase *database, BOOL *rollback) {
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_1", [NSNumber numberWithInt:1]];
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_2", [NSNumber numberWithInt:2]];
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_3", [NSNumber numberWithInt:3]];
FMResultSet *result = [database executeQuery:@"select * from t_person"];
while([result next]) {
}
//回滚
*rollback = YES;
}];
FMDatabaseQueue 后台会建立系列化的G-C-D队列,并执行你传给G-C-D队列的块。这意味着 你从多线程同时调用调用方法,GDC也会按它接收的块的顺序来执行。
五、CoreData
- CoreData的简单使用
准备工作
- 创建数据库
新建文件,选择CoreData -> DataModel
添加实体(表),Add Entity
给表中添加属性,点击Attributes下方的‘+’号
- 创建模型文件
新建文件,选择CoreData -> NSManaged Object subclass
根据提示,选择实体
通过代码,关联数据库和实体
- (void)viewDidLoad {
[super viewDidLoad];
/*
* 关联的时候,如果本地没有数据库文件,Coreadata自己会创建
*/
// 1. 上下文
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
// 2. 上下文关连数据库
// 2.1 model模型文件
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];
// 2.2 持久化存储调度器
// 持久化,把数据保存到一个文件,而不是内存
NSPersistentStoreCoordinator *store = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// 2.3 设置CoreData数据库的名字和路径
NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *sqlitePath = [doc stringByAppendingPathComponent:@"company.sqlite"];
[store addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:sqlitePath] options:nil error:nil];
context.persistentStoreCoordinator = store;
_context = context;
}
CoreData的基本操作 (CURD)
- 添加元素 - Create
-(IBAction)addEmployee{
// 创建一个员工对象
//Employee *emp = [[Employee alloc] init]; 不能用此方法创建
Employee *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:_context];
emp.name = @"wangwu";
emp.height = @1.80;
emp.birthday = [NSDate date];
// 直接保存数据库
NSError *error = nil;
[_context save:&error];
if (error) {
NSLog(@"%@",error);
}
}
- 读取数据 - Read
-(IBAction)readEmployee{
// 1.FetchRequest 获取请求对象
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 2.设置过滤条件
// 查找zhangsan
NSPredicate *pre = [NSPredicate predicateWithFormat:@"name = %@",
@"zhangsan"];
request.predicate = pre;
// 3.设置排序
// 身高的升序排序
NSSortDescriptor *heigtSort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:NO];
request.sortDescriptors = @[heigtSort];
// 4.执行请求
NSError *error = nil;
NSArray *emps = [_context executeFetchRequest:request error:&error];
if (error) {
NSLog(@"error");
}
//NSLog(@"%@",emps);
//遍历员工
for (Employee *emp in emps) {
NSLog(@"名字 %@ 身高 %@ 生日 %@",emp.name,emp.height,emp.birthday);
}
}
- 修改数据 - Update
-(IBAction)updateEmployee{
// 改变zhangsan的身高为2m
// 1.查找到zhangsan
// 1.1FectchRequest 抓取请求对象
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 1.2设置过滤条件
// 查找zhangsan
NSPredicate *pre = [NSPredicate predicateWithFormat:@"name = %@", @"zhangsan"];
request.predicate = pre;
// 1.3执行请求
NSArray *emps = [_context executeFetchRequest:request error:nil];
// 2.更新身高
for (Employee *e in emps) {
e.height = @2.0;
}
// 3.保存
NSError *error = nil;
[_context save:&error];
if (error) {
NSLog(@"%@",error);
}
}
- 删除数据 - Delete
-(IBAction)deleteEmployee{
// 删除 lisi
// 1.查找lisi
// 1.1FectchRequest 抓取请求对象
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 1.2设置过滤条件
// 查找zhangsan
NSPredicate *pre = [NSPredicate predicateWithFormat:@"name = %@",
@"lisi"];
request.predicate = pre;
// 1.3执行请求
NSArray *emps = [_context executeFetchRequest:request error:nil];
// 2.删除
for (Employee *e in emps) {
[_context deleteObject:e];
}
// 3.保存
NSError *error = nil;
[_context save:&error];
if (error) {
NSLog(@"%@",error);
}
}
- CoreData的表关联
准备工作
- 创建数据库
新建文件,选择CoreData -> DataModel
添加实体(表),Add Entity , 注意:这里根据关联添加多个实体
给表中添加属性,点击Attributes下方的‘+’号
- 创建模型文件
新建文件,选择CoreData -> NSManaged Object subclass
根据提示,选择实体,注意:这里先选择被关联的实体,最后添加最上层的实体
通过代码,关联数据库和实体
- (void)viewDidLoad {
[super viewDidLoad];
/*
* 关联的时候,如果本地没有数据库文件,Coreadata自己会创建
*/
// 1. 上下文
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
// 2. 上下文关连数据库
// 2.1 model模型文件
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];
// 2.2 持久化存储调度器
// 持久化,把数据保存到一个文件,而不是内存
NSPersistentStoreCoordinator *store = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// 2.3 设置CoreData数据库的名字和路径
NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *sqlitePath = [doc stringByAppendingPathComponent:@"company.sqlite"];
[store addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:sqlitePath] options:nil error:nil];
context.persistentStoreCoordinator = store;
_context = context;
}
基本操作
- 添加元素 - Create
-(IBAction)addEmployee{
// 1. 创建两个部门 ios android
//1.1 iOS部门
Department *iosDepart = [NSEntityDescription insertNewObjectForEntityForName:@"Department" inManagedObjectContext:_context];
iosDepart.name = @"ios";
iosDepart.departNo = @"0001";
iosDepart.createDate = [NSDate date];
//1.2 Android部门
Department *andrDepart = [NSEntityDescription insertNewObjectForEntityForName:@"Department" inManagedObjectContext:_context];
andrDepart.name = @"android";
andrDepart.departNo = @"0002";
andrDepart.createDate = [NSDate date];
//2. 创建两个员工对象 zhangsan属于ios部门 lisi属于android部门
//2.1 zhangsan
Employee *zhangsan = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:_context];
zhangsan.name = @"zhangsan";
zhangsan.height = @(1.90);
zhangsan.birthday = [NSDate date];
zhangsan.depart = iosDepart;
//2.2 lisi
Employee *lisi = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:_context];
lisi.name = @"lisi";
lisi.height = @2.0;
lisi.birthday = [NSDate date];
lisi.depart = andrDepart;
//3. 保存数据库
NSError *error = nil;
[_context save:&error];
if (error) {
NSLog(@"%@",error);
}
}
- 读取信息 - Read
-(IBAction)readEmployee{
// 读取ios部门的员工
// 1.FectchRequest 抓取请求对象
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 2.设置过滤条件
NSPredicate *pre = [NSPredicate predicateWithFormat:@"depart.name = %@",@"android"];
request.predicate = pre;
// 4.执行请求
NSError *error = nil;
NSArray *emps = [_context executeFetchRequest:request error:&error];
if (error) {
NSLog(@"error");
}
//遍历员工
for (Employee *emp in emps) {
NSLog(@"名字 %@ 部门 %@",emp.name,emp.depart.name);
}
}
其他功能与前几种类似,这里不在赘述
- CoreData的模糊查询
- 模糊查询
-(IBAction)readEmployee{
// 1.FectchRequest 抓取请求对象
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 2.设置排序
// 按照身高的升序排序
NSSortDescriptor *heigtSort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:NO];
request.sortDescriptors = @[heigtSort];
// 3.模糊查询
// 3.1 名字以"wang"开头
// NSPredicate *pre = [NSPredicate predicateWithFormat:@"name BEGINSWITH %@",@"wangwu1"];
// request.predicate = pre;
// 名字以"1"结尾
// NSPredicate *pre = [NSPredicate predicateWithFormat:@"name ENDSWITH %@",@"1"];
// request.predicate = pre;
// 名字包含"wu1"
// NSPredicate *pre = [NSPredicate predicateWithFormat:@"name CONTAINS %@",@"wu1"];
// request.predicate = pre;
// like 匹配
NSPredicate *pre = [NSPredicate predicateWithFormat:@"name like %@",@"*wu12"];
request.predicate = pre;
// 4.执行请求
NSError *error = nil;
NSArray *emps = [_context executeFetchRequest:request error:&error];
if (error) {
NSLog(@"error");
}
//遍历员工
for (Employee *emp in emps) {
NSLog(@"名字 %@ 身高 %@ 生日 %@",emp.name,emp.height,emp.birthday);
}
}
- 分页查询
-(void)pageSeacher{
// 1. FectchRequest 抓取请求对象
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 2. 设置排序
// 身高的升序排序
NSSortDescriptor *heigtSort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:NO];
request.sortDescriptors = @[heigtSort];
// 3. 分页查询
// 总有共有15数据
// 每次获取6条数据
// 第一页 0,6
// 第二页 6,6
// 第三页 12,6 3条数据
// 3.1 分页的起始索引
request.fetchOffset = 12;
// 3.2 分页的条数
request.fetchLimit = 6;
// 4. 执行请求
NSError *error = nil;
NSArray *emps = [_context executeFetchRequest:request error:&error];
if (error) {
NSLog(@"error");
}
// 5. 遍历员工
for (Employee *emp in emps) {
NSLog(@"名字 %@ 身高 %@ 生日 %@",emp.name,emp.height,emp.birthday);
}
}
- 多个数据库的使用
- 创建多个数据库,即创建多个DataModel
- 一个数据库对应一个上下文
- 需要根据bundle名创建上下文
- 添加或读取信息,需要根据不同的上下文,访问不同的实体
- 关联数据库和实体
- (void)viewDidLoad {
[super viewDidLoad];
// 一个数据库对应一个上下文
_companyContext = [self setupContextWithModelName:@"Company"];
_weiboContext = [self setupContextWithModelName:@"Weibo"];
}
/**
* 根据模型文件,返回一个上下文
*/
-(NSManagedObjectContext *)setupContextWithModelName:(NSString *)modelName{
// 1. 上下文
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
// 2. 上下文关连数据库
// 2.1 model模型文件
// 注意:如果使用下面的方法,如果 bundles为nil 会把bundles里面的所有模型文件的表放在一个数据库
//NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];
// 改为以下的方法获取:
NSURL *companyURL = [[NSBundle mainBundle] URLForResource:modelName withExtension:@"momd"];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:companyURL];
// 2.2 持久化存储调度器
// 持久化,把数据保存到一个文件,而不是内存
NSPersistentStoreCoordinator *store = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// 2.3 告诉Coredata数据库的名字和路径
NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *sqliteName = [NSString stringWithFormat:@"%@.sqlite",modelName];
NSString *sqlitePath = [doc stringByAppendingPathComponent:sqliteName];
[store addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:sqlitePath] options:nil error:nil];
context.persistentStoreCoordinator = store;
// 3. 返回上下文
return context;
}
- 添加元素
-(IBAction)addEmployee{
// 1. 添加员工
Employee *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:_companyContext];
emp.name = @"zhagsan";
emp.height = @2.3;
emp.birthday = [NSDate date];
// 直接保存数据库
[_companyContext save:nil];
// 2. 发微博
Status *status =[NSEntityDescription insertNewObjectForEntityForName:@"Status" inManagedObjectContext:_weiboContext];
status.text = @"发了一条微博!";
status.createDate = [NSDate date];
[_weiboContext save:nil];
}
参考:
iOS中几种数据持久化方案
iOS学习之iOS沙盒(sandbox)机制和文件操作
iOS 沙盒目录结构及正确使用
IOS 沙盒机制 && 关于document\library\tmp的灵活使用
iOS中几种数据持久化方案:我要永远地记住你!
我要娶你做我的CoreData!
iOS 沙盒路径详解(存储路径的选择)