技 术 文 章 / 超 人
沙盒中持久化保存数据方案
从简单到复杂
沙盒说明
每个
iOS
应用在iOS
手机中都拥有一个独立的沙盒目录(类似于windows
电脑系统里的软件。每个软件都放在自己的文件夹中)。可以用NSHomeDirectory()
来获取当前应用的沙盒目录 。-
每个沙盒目录中都有3个固定文件夹
Documents
、Library
、tmp
。在Xcode5
以下多一个项目名字.app
的文件,而Xcode6
中被删除了(Xcode5
与Xcode6
应用在模拟器中的沙盒路径也不同)。
沙盒文件夹说明:
*
Documents
:保存用户产生的数据,指用户在使用当前应用时保存的一些数据,比如保存在应用中的图片、保存下载的文件等。iTunes
同步设备的时候会备份该目录。获取Documents路径方法NSHomeDirectory();
。
NSSearchPathForDirectoriesInDomains方法说明,用来查找文件路径
参数1:指定收索路径名称
typedef NS_ENUM(NSUInteger, NSSearchPathDirectory) {
NSApplicationDirectory = 1,
// supported applications (Applications)
NSDemoApplicationDirectory,
// unsupported applications, demonstration versions (Demos)
NSDeveloperApplicationDirectory,
// developer applications (Developer/Applications). DEPRECATED - there is no one single Developer directory.
NSAdminApplicationDirectory,
// system and network administration applications (Administration)
NSLibraryDirectory,
// various documentation, support, and configuration files, resources (Library)
NSDeveloperDirectory,
// developer resources (Developer) DEPRECATED - there is no one single Developer directory.
NSUserDirectory,
// user home directories (Users)
NSDocumentationDirectory,
// documentation (Documentation)
NSDocumentDirectory,
// documents (Documents)
NSCoreServiceDirectory,
// location of CoreServices directory (System/Library/CoreServices)
NSAutosavedInformationDirectory
API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) = 11, // location of autosaved documents (Documents/Autosaved)
NSDesktopDirectory = 12,
// location of user's desktop
NSCachesDirectory = 13,
// location of discardable cache files (Library/Caches)
NSApplicationSupportDirectory = 14,
// location of application support files (plug-ins, etc) (Library/Application Support)
NSDownloadsDirectory
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 15, // location of the user's "Downloads" directory
NSInputMethodsDirectory
API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) = 16, // input methods (Library/Input Methods)
NSMoviesDirectory
API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) = 17, // location of user's Movies directory (~/Movies)
NSMusicDirectory
API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) = 18, // location of user's Music directory (~/Music)
NSPicturesDirectory
API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) = 19, // location of user's Pictures directory (~/Pictures)
NSPrinterDescriptionDirectory
API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) = 20, // location of system's PPDs directory (Library/Printers/PPDs)
NSSharedPublicDirectory
API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) = 21, // location of user's Public sharing directory (~/Public)
NSPreferencePanesDirectory
API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) = 22, // location of the PreferencePanes directory for use with System Preferences (Library/PreferencePanes)
NSApplicationScriptsDirectory
NS_ENUM_AVAILABLE(10_8, NA) = 23, // location of the user scripts folder for the calling application (~/Library/Application Scripts/code-signing-id)
NSItemReplacementDirectory
API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) = 99, // For use with NSFileManager's URLForDirectory:inDomain:appropriateForURL:create:error:
NSAllApplicationsDirectory = 100,
// all directories where applications can occur
NSAllLibrariesDirectory = 101,
// all directories where resources can occur
NSTrashDirectory
API_AVAILABLE(macos(10.8), ios(11.0)) API_UNAVAILABLE(watchos, tvos) = 102 // location of Trash directory
};
参数2:查找的目录类型
enum {
NSUserDomainMask = 1, ///< 用户主目录
NSLocalDomainMask = 2, ///< 当前机器
NSNetworkDomainMask = 4, ///< 网络中可见的主机
NSSystemDomainMask = 8, ///< 系统目录,不可修改(/System)
NSAllDomainsMask = 0x0ffff, ///< 所有
};
参数3:是否显示完整路径,YES为展开后完整路径
//获取Documents路径
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
* Library
:
//获取Library路径
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libraryPath = [paths objectAtIndex:0];
** Caches
:保存应用运行时生成的需要持久化的数据,比如SDWebImage
把缓存的图片就存放到该目录下。当程序退出后,该目录保存的文件一直存在。一般存储体积大、不需要备份的非重要数据。iTunes
同步设备时不会备份该目录。
//获取Caches路径
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesPath = [paths objectAtIndex:0];
** Preferences
:保存应用的所有偏好设置,iOS
的Settings(设置)
应用会在该目录中查找应用的设置信息。iTunes
同步设备时会备份该目录
* tmp
:保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。当手机内存爆满时,就算应用没有运行时,系统会清除所有应用该目录下的文件。iTunes
同步设备时不会备份该目录。获取tmp路径方法NSTemporaryDirectory()
;
1. NSUserDefaults(最简单)
-
NSUserDefaults
是一个单例对象,在整个应用程序的生命周期中都只有一个实例。NSUserDefaults
本身也是用.plist保存的,系统把它保存在Libaray目录下的Preference文件夹中。 -
NSUserDefaults
保存的数据类型有:NSNumber
, 基本数据类型(int
,NSInter
,float
,double
,CGFlat
......),NSString
,NSData
,NSArray
,NSDictionary
,NSURL
。 -
NSUserDefaults
一般保存配置信息,比如用户名
、密码
、是否保存用户名和密码、是否离线下载等一些配置条件信息
。
基本用法:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
//保存值(key值同名的时候会覆盖)
[defaults setObject:@"用户名" forKey:@"Key名称"];
//立即保存
[defaults synchronize];
//取值
NSString *username = [defaults objectForKey:@"Key名称];
同样,保存还有一些方法,比如:
//保存NSInteger
[defaults setInteger:(NSInteger) forKey:(nonnull NSString *)];
//保存BOOL
[defaults setBool:(BOOL) forKey:(nonnull NSString *)];
//保存NSURL
[defaults setURL:(nullable NSURL *) forKey:(nonnull NSString *)];
//保存float
[defaults setFloat:(float) forKey:(nonnull NSString *)];
//保存double
[defaults setDouble:(double) forKey:(nonnull NSString *)];
取值另外方法:
//取值
[defaults integerForKey:(nonnull NSString *)];
[defaults boolForKey:(nonnull NSString *)];
[defaults URLForKey:(nonnull NSString *)];
[defaults floatForKey:(nonnull NSString *)];
[defaults doubleForKey:(nonnull NSString *)];
删除方法:
//删除指定key的数据
[defaults removeObjectForKey:(nonnull NSString *)];
synchronize
。立即保存,苹果文档这么说:
Writes any modifications to the persistent domains to disk and updates all unmodified persistent domains to what is on disk.
Return Value YES if the data was saved successfully to disk, otherwise NO.(大概意思是,使用synchronize后会立即把所有执行了存值方法但未在内存中修改值的值修改。该方法返回YES表示修改成功,NO表示修改失败)
2. plist文件保存(简单实用)
其实NSUserDefaults本身也是保存的一份plist文件在Library下的Preferences文件夹中
- 一般在
iOS
用plist
保存,plist
本身就是XML
文件,名字后缀为.plist
。
plist
主要保存的数据类型为NSString
、NSNumber
、NSData
、NSArray
、NSDictionary
、NSMutableArray
、NSMutableDictionary
。 - 具体实现:
/* 例如保存在Documents路径下 */
//Documents文件路径
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
//拼接Documents+文件名,plistName就是文件名字。比如@"data.plist"
NSString *filePath = [self.path stringByAppendingPathComponent:plistName];
//把字典写入到plist文件,比如文件path为:~/Documents/data.plist
[dictionary writeToFile:path atomically:YES];
//把数组写入到plist文件中
[array writeToFile:path atomically:YES];
- 读取数据
NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:(nonnull NSString *)]];
NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:(nullable NSString *) ofType:(nullable NSString *)]];
NSArray *array = [NSArray arrayWithContentsOfURL:[NSURL fileURLWithPath:(nonnull NSString *)]];
NSArray *array = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:(nullable NSString *) ofType:(nullable NSString *)]];
3. 归档(序列化)
- 一般保存自定义的对象(例如归档聊天历史数据或者状态数据),但是只有遵守NSCoding的类才能只用归档。
- 准守NSCoding协议必须要实现以下两个require方法:
//归档会触发
(void)encodeWithCoder:(NSCoder *)aCoder
//解归
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
档会触发
- Coding类具体实现:
@interface Coding : NSObject<NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
#import "ModelClass.h"
//必须导入
#import <objc/runtime.h>
@implementation Coding
/**
* 根据类动画获取类的所有属性
*
* @param cls <#cls description#>
*
* @return <#return value description#>
*/
- (NSArray *)perperiesWithClass:(Class)cls
{
NSMutableArray *perperies = [NSMutableArray array];
//定义属性数量
unsigned int outCount;
//动态获取cls类的所有属性,并把值叠加到outCount,每有一个属性累加1
objc_property_t *properties = class_copyPropertyList(cls, &outCount);
//遍历cls类的所有属性
for (int i = 0; i < outCount; i++)
{
objc_property_t property = properties[i];
const char *name = property_getName(property);
NSString *s = [[NSString alloc] initWithUTF8String:name];
[perperies addObject:s];
}
return perperies;
}
/**
* 归档会触发
*
* @param aCoder <#aCoder description#>
*/
- (void)encodeWithCoder:(NSCoder *)aCoder
{
for (NSString *perperty in [self perperiesWithClass:[self class]])
{
[aCoder encodeObject:perperty forKey:perperty];
}
}
/**
* 解归档会触发
*
* @param aDecoder <#aDecoder description#>
*
* @return <#return value description#>
*/
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init])
{
for (NSString *perperty in [self perperiesWithClass:[self class]])
{
[self setValue:[aDecoder decodeObjectForKey:perperty] forKey:perperty];;
}
}
return self;
}
@end
- 归档具体使用:
//创建需要归档的数据
ModelClass *model1 = [ModelClass new];
model1.name = @"用户名1";
model.password = @"****";
model1.age = 18;
//创建需要归档的数据
ModelClass *model2 = [ModelClass new];
model2.name = @"用户名2";
mode2.password = @"****";
model2.age = 20;
NSArray *array = @[model1, model2];
/* 保存对象转化为二进制数据(一定是可变对象)归档的data和解档的data必须是同一个对象。*/
NSMutableData *data = [NSMutableData data];
/* 归档 */
//1.初始化
NSKeyedArchiver *archivier = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
//2.归档
[archivier encodeObject:array forKey:@"key"];
//3.完成归档
[archivier finishEncoding];
/* 归档 */
//1.初始化解归档对象,解档的data跟归档data是同一个对象
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
//2.解归档
NSArray *persons = [unarchiver decodeObjectForKey:@"key"];
//3.完成解归档
[unarchiver finishDecoding];
4. CoreData
下面是CoreData堆栈图
适用于保存用户数据,聊天数据等比较复杂数量较多属性较多的数据
CoreData是iOS5时苹果推出的,实际时对SQL的一种封装
提供了一种“对象-关系映射”的功能,能将
OC
对象转化成数据,保存Sqlite
中。CoreData
的好处就是能够合理管理内存,避免sql
语句的麻烦(不用写sql
语句)。
构成
-
NSManagedObjectContext
:
被管理的数据上下文,主要作用:插入、查询、删除。 -
NSManagedObjectModel
:
数据库所有的表结构和数据结构,包含各个实体的定义的信息。主要作用就是添加实体、实体属性,建立属性之间的关系。 -
NSPersistentStoreCoordinator
持久化存储助理对象,相当于数据库的连接器。主要作用就是设置存储的名字、位置、存储方式。 -
NSFetchRequest
相当于select
语句。查询封装对象。 -
NSEntityDescription
实体结构对象,相当于表格结构。后缀为xxx.xcdatamodeld
文件,编译后为xxx.momd
的文件。
创建工程的时候,勾上Use Core Data
。如图所示:
- 勾选
Use Core Data
后,项目的AppDelegate
文件中已经生成好了相应CoreData
代码
#import <CoreData/CoreData.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>@property (strong, nonatomic) UIWindow *window;
/* 上下文 */
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
/* 管理数据模型*/
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
/*持久化的数据的对象*/
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
/** * 保存上下文 */
- (void)saveContext;
/** * 获取documents路径
*
* @return <#return value description#>
*/
- (NSURL *)applicationDocumentsDirectory;
@end
-
创建实体对象,并添加属性。
实体结构
创建模型。选择项目->右击->New File
...->iOS
下面的Core Data
->NSManagedObject subclass
->选择CoreData
文件点击Next
->选择要生成的对象模型,点击Next
->Create
。具体操作如下截图:
- 创建模型
- 创建类
- 选择要生成的CoreData文件
- 选择要生成的对象
- 生成好的模型
具体实现代码如下:
*
保存数据
- (NSManagedObjectContext *)context
{
AppDelegate *app = [UIApplication sharedApplication].delegate;
return app.managedObjectContext;
}
//创建Person对象
/*insertNewObjectForEntityForName:就是创建的实体名字。inManagedObjectContext:上下文,appDelegate里面已经创建完成。*/
Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:[self context]];
//赋值
[person setValue:@"小王" forKey:@"name"];
[person setValue:@(35) forKey:@"age"];
//保存
if (![[self context] save:nil])
{
//TODO:保存失败
}
*
查询
//创建查询对象
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
#if 0
//条件查询 //
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age<=35"];
//查询名字带有王的
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name like[cd]'*王*'"];
//设置查询条件 request.predicate = predicate;
#endif
//排序 NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];
//设置排序条件
request.sortDescriptors = @[sort];
//执行查询
NSArray *objectArray = [[self context] executeFetchRequest:request error:nil];
//遍历查询结果
for (Person *p in objectArray)
{
NSLog(@"%@ - %@",[p valueForKey:@"name"],[p valueForKey:@"age"]);
}
*
修改
//先查询要修改的对象
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
//设置查询条件 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name='小王' and age = 35"];
request.predicate = predicate;
//执行查询
NSArray *objectArray = [[self context] executeFetchRequest:request error:nil];
//遍历要修改的对象
for (Person *p in objectArray)
{
//修改(修改内存数据,没有同步数据库)
[p setValue:@(45) forKey:@"age"];
}
//同步数据库 [[self context] save:nil];
*
删除
//查询要删除的数据
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
//设置查询条件 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name='小王'"];
request.predicate = predicate;
//执行查询
NSArray *objectArray = [[self context] executeFetchRequest:request error:nil];
//遍历删除 for (Person *p in objectArray)
{
//删除内存中的数据 [[self context] deleteObject:p];
}
//同步数据库 [[self context] save:nil];
CoreData最重要的是关系,由于时间关系。当app
更新版本,并且表结构有修改,需要版本升级和数据迁移操作,否则app
就是崩掉。
4. 数据库
- iOS用的
sqlite3
, 使用sqlite3
需要配置库文件libsqlite3.tbd
或者导入libsqlite3.0.tbd
,这两个库导入任何一个都可以,如图所示。 - 导入库
保存大量数据可以优先考虑用数据库,sql语句对查询操作有优化作用,所以从查询速度或者插入效率都是很高的。
首先需要对常用的sql语句了解。这里就不在介绍了,可以看下这个教程:http://www.w3school.com.cn/sql/
sqlite
使用步骤:
*
指定数据库路径。
*
创建sqlite3
*
对象并且打开数据库。
*
创建表。
*
对数据库操作,包括增删改查。
*
关闭数据库。具体实现:
*
数据库路径
//返回数据库路径,保存到Cache目录下
-(NSString *)databasePath{
NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
return [path stringByAppendingPathComponent:@"contacts.db"];
}
*
创建sqlite3
对象并且打开数据库,如果数据库打开成功,就创建表。
//数据库对象 sqlite3 *contactDB;
const char *path = [[self databasePath] UTF8String];
if (sqlite3_open(path, &contactDB) == SQLITE_OK)
{
char *errMsg; const char *sql_stmt = "CREATE TABLE IF NOT EXISTS CONTACTS(ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT, ADDRESS TEXT,PHONE TEXT)";
//执行语句
if (sqlite3_exec(contactDB, sql_stmt, NULL, NULL, &errMsg) !=
SQLITE_OK) {
//创建表失败
}
}
else
{
//打开数据库失败
} sqlite3_close(contactDB);
*
代码解释:
sqlite3_open
:
打开指定路径的数据库,如果数据库不存在,就会创建一个新的数据库。
SQLITE_OK
是一个常量,表示打开数据库成功。下面是苹果的定义:
define SQLITE_OK 0 /* Successful result */
& contactDB
:就是数据库对象。
& sqlite3_exec
:就是执行sql语句方法。
& sqlite3_close
:关闭数据库,一般暂时不用数据库的时候手动关闭,防止资源浪费。
*
保存数据到数据库
//是一个抽象类型,是一个句柄,在使用过程中一般以它的指针进行操作sqlite3_stmt *statement;
//数据库路径
const char *path = [[self databasePath] UTF8String];
//使用的时候打开数据库
if (sqlite3_open(path, &contactDB) == SQLITE_OK)
{
NSString *insertSQL = [NSString stringWithFormat:@"INSERT INTO CONTACTS (name,address,phone) VALUES(\"%@\",\"%@\",\"%@\")",name.text,address.text,phone.text];
const char *insert_stmt = [insertSQL UTF8String];
// 这个函数将sql文本转换成一个准备语句(prepared statement)对象,同时返回这个对象的指针。这个接口需要一个数据库连接指针以及一个要准备的包含SQL语句的文本。它实际上并不执行这个SQL语句,它仅仅为执行准备这个sql语句 sqlite3_prepare_v2(contactDB, insert_stmt, -1, &statement, NULL);
//执行这个sql
if (sqlite3_step(statement) == SQLITE_DONE)
{
//TODO:已存储到数据库;
} else
{
//TODO:保存失败
}
//销毁statement对象 sqlite3_finalize(statement); //关闭数据库
sqlite3_close(contactDB);
}
*
查询操作
//数据库路径
const char *path = [[self databasePath] UTF8String];
//查询结果集对象句柄sqlite3_stmt *statement;
//打开数据库
if (sqlite3_open(path, &contactDB) == SQLITE_OK)
{
//查询的sql语句
NSString *querySQL = [NSString stringWithFormat:@"SELECT address,phone from contacts where name=\"%@\"",name.text];
const char *query_stmt = [querySQL UTF8String];
//执行查询sql语句
if (sqlite3_prepare_v2(contactDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
{
//遍历每条数据
if (sqlite3_step(statement) == SQLITE_ROW)
{
//获取每条数据的字段。
NSString *addressField = [[NSString alloc] initWithUTF8String:(const char *)sqlite3_column_text(statement, 0)];
address.text = addressField; NSString *phoneField = [[NSString alloc] initWithUTF8String:(const char *)sqlite3_column_text(statement, 1 )];
phone.text = phoneField;
//TODO:已查到结果
} else
{
//TODO:未查到结果
}
sqlite3_finalize(statement);
} sqlite3_close(contactDB);
}
沙盒外持久化保存数据方案
1. SSKeyChain
- <下次更新>