ios - 读写应用程序的几种方式之NSUserDefaults
iOS应用程序中存储数据的几种方式
在iOS系统中对数据做持久化存储的方式一般有5中方式:
- 文件写入
- 对象归档
- SQLite数据库
- CoreData
-
NSUserDefaults
闪存和沙盒
在iPhone/iPad设备上包含闪存,它的功能和一个硬盘的功能等价。当设备断电后数据还能被保存下来。应用程序可以将文件保存下来,并能从闪存中读取它们。但是我们的应用程序不能访问整个闪存,闪存上上面的一部分专门给你的应用程序,这就是你应用程序的沙盒(sandbox)。每个应用程序只能看到自己的沙盒,这就防止了对其他应用程序的文件进行读取的活动。
用户默认设置 - NSUserDefaults
Apple将整个首选项系称为应用程序首选项,用户可以通过它定制应用程序。应用程序首选项系统负责如下任务:
- 将首选项持久化到设备中
- 将各个应用程序的首选项彼此分开
- 通过iTune将应用程序首选项备份到计算机,以免在需要恢复设备时用户丢失首选项。
我们可以通过一个易于使用的API与应用程序首选项交互,该API主要由单例类NSUserDefaults组成。
类NSUserDefaults的工作原理类似于NSDictionary,主要差别在于NSUserDefaults是单例类,且在它可以存储的对象类型方面受到很多的限制。应用程序的所有首选项都是以“键-值”对的方式存储在NSUserDefaults单例类中。
访问应用程序首选项
访问应用程序首选项必须获取指向应用程序的NSUserDefaults单例的引用:
NSUserDefaults * userDefault = [NSUserDefaults standardUserDefaults];
然后就可以读写默认设置数据库了。提供了以下方法写入数据以及访问数据:
- (void)setObject:(nullable id)value forKey:(NSString *)defaultName;
- (void)setInteger:(NSInteger)value forKey:(NSString *)defaultName;
- (void)setFloat:(float)value forKey:(NSString *)defaultName;
- (void)setDouble:(double)value forKey:(NSString *)defaultName;
- (void)setBool:(BOOL)value forKey:(NSString *)defaultName;
- (void)setURL:(nullable NSURL *)url forKey:(NSString *)defaultName NS_AVAILABLE(10_6, 4_0);
具体使用哪一个函数取决于要存储的数据类型。函数setObject:forKey可以存储NSString、NSData、NSArray以及其他常见的对象类型。例如使用键age存储一个整数20,用键oc存储一个字符串HelloWorld:
//存储一个整数
[userDefault setInteger:20 forKey:@"age"];
//存储一个字符串
[userDefault setObject:@"HelloWorld" forKey:@"oc"];
但是当我们将数据写入默认设置数据库时,并不一定会立即保存这些数据。为了确保所有数据都写入了用户默认设置,可以使用synchronize方法:
[userDefault synchronize];
示例代码
NSUserDefaults * userDefault = [NSUserDefaults standardUserDefaults];
//存储一个整数
[userDefault setInteger:20 forKey:@"age"];
//存储一个字符串
[userDefault setObject:@"HelloWorld" forKey:@"oc"];
// 马上存储
[userDefault synchronize];
NSInteger integer =[[NSUserDefaults standardUserDefaults] integerForKey:@"age"];
NSString * string = [[NSUserDefaults standardUserDefaults] stringForKey:@"oc"];
NSLog(@"integer-%ld",integer);
NSLog(@"string-%@", string);
打印结果
对NSUserDefaults的简单封装
虽然该通过这样的方实现了对数据用键值对的方式进行存取,但是每次存取都要写一大堆的相同的代码。为了实现封装,可以新建一个类,在这个类实现对NSUserDefaults存储,读取和删除:
#import "LMHUserDefault.h"
@implementation LMHUserDefault
// 存储用户偏好设置到NSUserDefults
+ (void)setUserData:(id)data forKey:(NSString*)key
{
if (data == nil)
{
return;
}
else
{
[[NSUserDefaults standardUserDefaults] setObject:data forKey:key];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
//读取用户偏好设置
+ (id)readUserDataWithKey:(NSString*)key
{
id temp = [[NSUserDefaults standardUserDefaults] objectForKey:key];
if(temp != nil)
{
return temp;
}
return nil;
}
//删除用户偏好设置
+ (void)removeUserDataWithkey:(NSString*)key
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
}
那么在要用到的地方只需要将这个类的头文文件导入,然后调用相应的类方法即可。以上的代码就可以用下面的方式实现:
- (void)viewDidLoad {
[super viewDidLoad];
//存储一个整数
[LMHUserDefault setUserData:@(20) forKey:@"age"];
//存储一个字符串
[LMHUserDefault setUserData:@("HelloWorld") forKey:@"oc"];
// 读取
NSInteger integer = (NSInteger)[LMHUserDefault readUserDataWithKey:@"age"];
NSString * string = (NSString *)[LMHUserDefault readUserDataWithKey:@"oc"];
// 打印
NSLog(@"age----%ld", integer);
NSLog(@"oc-----%@", string);
}
打印结果:
直接访问文件系统
通过NSUserDefaults存储的对象有一些限制,但是铜鼓直接范文文件系统的方式可以存储任何类型的数据。
直接访问文件系统是指打开文件并读写其内容。例如从Internet下载的文件、应用程序创建的文件扥,但并非能存储到任何地方。因为iOS中有沙盒的限制。
应用程序目录中有四个位置专门用来存取应用程序的数据:
-
Library/Caches
- 用户缓存从网络获取的数据、通过大量计算得到的数据。
- 该目录存放的数据将在应用程序关闭时得以保留,所以** 如果希望下载的文件永久有效(除非用户卸载软件),那么可以放在这里。**
-
Library/Preference
-
Documents
- 应用程序数据的主要存储位置
- 设备与iTunes同步的时候,该目录将备份到计算机中,因此不能将大文件放在这里,备份起来很麻烦
- 另外,审核的时候如果发现将下载的东西放在这里,审核不会通过
-
tmp
- 应用程序储存临时文件夹。
- 由于是临时文件,所以不靠谱,里面的东西随时都可能被清理掉。
获取文件路径
每个文件都有路径,这个路径是文件在文件系统中的准确位置。要让应用程序能够读写其沙盒中的文件,需要指定该文件的完整路径。在Core Foundation中提供了一个名为NSS eachPathForDirectoriesInDomains的C语言函数,它返回的是指向应用程序的目录Documents或Library/Caches路径。由于这个函数可以返回多个目录,因此该函数调用的结果是一个NSArray对象。使用该函数来获取指向目录Documents或Library/Caches路径时,它返回的数组将只包含一个NSString;要从数组中获取该路径的NSString,可以将数组的索引设置为0.
获取Library/Caches的文件路径
// 获取文件路径
NSString * cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0];
另外NSString的stringByAppendingPathComponent可以将两个路径拼接起来。
获取Documents的文件路径
如果要获取Documents中的特定文件路径,只需要将NSCachesDirectory换成NSDocumentDirectory即可:
// 获取Documents目录中的文件路径
NSString * docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
获取tmp的文件路径
获取tmp的文件路径可以通过Core Foundation中的另一套C语言函数NSTemporaryDirectory
// 获取tmp的文件路径
NSString * tmpFile = NSTemporaryDirectory();
读取数据
要读写文件中的数据,首先要检查文件是否存在,如果不存在则需要的话需要先创建它。
// 检查myPath的文件是否存在
if ([[NSFileManager defaultManager] fileExistsAtPath:myPath]) {
// 存在
}else{
// 不存在
}
然后可以使用类NSFileHandle提供的方法获取文件的引用,再进行读取和写入数据,NSFileHandle中提供的获取文件引用的方法:
+ (nullable instancetype)fileHandleForReadingAtPath:(NSString *)path;
+ (nullable instancetype)fileHandleForWritingAtPath:(NSString *)path;
+ (nullable instancetype)fileHandleForUpdatingAtPath:(NSString *)path;
+ (nullable instancetype)fileHandleForReadingFromURL:(NSURL *)url error:(NSError **)error NS_AVAILABLE(10_6, 4_0);
+ (nullable instancetype)fileHandleForWritingToURL:(NSURL *)url error:(NSError **)error NS_AVAILABLE(10_6, 4_0);
+ (nullable instancetype)fileHandleForUpdatingURL:(NSURL *)url error:(NSError **)error NS_AVAILABLE(10_6, 4_0);
如果要写入数据,可以使用NSFileHandle中的writeData方法.
在最后还要关闭手柄。所以总体而言,分为四个步骤:
- 检查指定文件是否存在
- 获取该文件的引用
- 读取文件/写入文件
- 关闭手柄
// 检查myPath的文件是否存在
if ([[NSFileManager defaultManager] fileExistsAtPath:myPath]) {
// 存在
}else{
// 不存在
}
// 获取文件引用
NSFileHandle * fileHandle = [NSFileHandle fileHandleForWritingAtPath:myPath];
// 写入数据
[fileHandle writeData:stringData];
// 关闭手柄
[fileHandle closeFile];
使用SQlite3存储和读取数据
SQLite3是嵌入在IOS中的关系型数据库,对于存储大型的数据很有效。SQLite3不必将每个对象都加载到内存中。在iOS中,对SQLite3有如下基本操作:
- 打开或者创建数据库
// 打开或创建数据库
sqlite3 * database;
if (sqlite3_open([self.databaseFilePath UTF8String], &database) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"打开数据库失败!");
}
- 关闭数据库
// 关闭数据库
sqlite3_close(database);
- 创建一个表格
// 创建数据库表
NSString * createSQL = @"CREATE TABLE IF NOT EXISTS FIELDS (TAG INTEGER PRIMARY KEY, FIELD_DATA TEXT);";
char * errorMsg;
if (sqlite3_exec(database, [createSQL UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"创建数据库表格错误:%s", errorMsg);
}
- 查询操作
// 执行查询
NSString * query = @"SELECT TAG, FIELD_DATA FROM FIELDS ORDER BY TAG";
sqlite3_stmt * statement;
if (sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil) == SQLITE_OK) {
// 依次读取数据数据库表格FIELDS中每行的内容,并显示在对应的textFiled上
while (sqlite3_step(statement) == SQLITE_ROW) {
// 获取数据
int tag = sqlite3_column_int(statement, 0);
char * rowData = (char *)sqlite3_column_text(statement, 1);
// 根据tag获得对应的textField
UITextField * textfield = (UITextField *)[self.view viewWithTag:tag];
// 设置文本
textfield.text = [[NSString alloc] initWithUTF8String:rowData];
}
sqlite3_finalize(statement);
}
- 使用约束变量
实际操作中经常使用叫做约束变量的东西来构造SQL字符串,从而进行增删改查的操作。
// 向表格插入4行数据
for (int i = 0; i < 4; i++) {
//根据tag获取textfiel
UITextField * textfield = (UITextField *)[self.view viewWithTag:i];
// 使用约束变量插入数据
char * updata = "INSERT OR REPLACE INTO FIELDS(TAG, FIELDS_DATA) VALUES(?, ?);";
sqlite3_stmt * stmt;
if (sqlite3_prepare_v2(database, updata, -1, &stmt, nil) == SQLITE_OK) {
sqlite3_bind_int(stmt, 1, i);
sqlite3_bind_text(stmt, 2, [textfield.text UTF8String], -1, NULL);
}
char * errorMsg = NULL;
if (sqlite3_step(stmt) != SQLITE_OK) {
NSAssert(0, @"更新数据库表FIELDS出错:%s",errorMsg);
}
sqlite3_finalize(stmt);
}