转:相对复杂的App仅靠内存的数据肯定无法满足,数据写磁盘作持久化存储是几乎每个客户端软件都需要做的。简单如“是否第一次打开”的BOOL值,大 到游戏的进度和状态等数据,都需要进行本地持久化存储。这些数据的存储本质上就是写磁盘存文件,原始一点可以用iOS本身支持有 NSFileManager这样的API,或者干脆C语言fwrite/fread,Cocoa Touch本身也提供了一些存储方式,如NSUserDefaults,CoreData等。总的来说,iOS平台数据持久存储方法大致如下所列:
ObjC是C的一个超集,所以最笨的方法我们可以直接用C作文件读写来实现数据存储:
1. 写入文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// File path
const
char
* pFilePath = [_path cStringUsingEncoding:
NSUTF8StringEncoding
];
// Create a new file
FILE * pFile = fopen(pFilePath,
"w+"
);
if
(pFile ==
NULL
) {
NSLog
(
@"Open File ERROR!"
);
return
;
}
const
char
* content = [_textField.text cStringUsingEncoding:
NSUTF8StringEncoding
];
fwrite(content,
sizeof
(content), 1, pFile);
fclose(pFile);
|
2. 读取文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// File path
const
char
* pFilePath = [_path cStringUsingEncoding:
NSUTF8StringEncoding
];
// Create a new file
FILE * pFile = fopen(pFilePath,
"r+"
);
if
(pFile ==
NULL
) {
NSLog
(
@"Open File ERROR!"
);
return
;
}
int
fileSize = ftell(pFile);
NSLog
(
@"fileSize: %d"
, fileSize);
char
* content[20];
fread(content, 20, 20, pFile);
NSString
* aStr = [
NSString
stringWithFormat:
@"%s"
, &content];
if
(aStr !=
nil
&& ![aStr isEqualToString:
@""
]) {
_textField.text = aStr;
}
fclose(pFile);
|
但是既然在iOS平台作开发,我们当然不至于要到使用C的原生文件接口这种地步,下面就介绍几种iOS开发中常用的数据本地存储方式。使用起来最简 单的大概就是Cocoa提供的NSUserDefaults了,Cocoa会为每个app自动创建一个数据库,用来存储App本身的偏好设置,如:开关音 效,音量调整之类的少量信息。NSUserDefaults是一个单例,生命后期由App掌管,使用时用 [NSUserDefaults standardUserDefaults] 接口获取单例对象。NSUserDefaults本质上是以Key-Value形式存成plist文件,放在App的 Library/Preferences目录下,对于已越狱的机器来说,这个文件是不安全的,所以**千万不要用NSUserDefaults来存储密码之类的敏感信息**,用户名密码应该使用**KeyChains**来存储。
1.写入数据
1
2
3
4
5
6
7
|
// 获取一个NSUserDefaults对象
NSUserDefaults
* aUserDefaults = [
NSUserDefaults
standardUserDefaults];
// 插入一个key-value值
[aUserDefaults setObject:_textField.text forKey:
@"Text"
];
// 这里是为了把设置及时写入文件,防止由于崩溃等情况App内存信息丢失
[aUserDefaults synchronize];
|
2.读取数据
1
2
3
|
NSUserDefaults
* aUserDefaults = [
NSUserDefaults
standardUserDefaults];
// 获取一个key-value值
NSString
* aStr = [aUserDefaults objectForKey:
@"Text"
];
|
使用起来很简单吧,它的接口跟 NSMutableDictionary 一样,看它的头文件,事实上在内存里面也是用dictionary来存的。写数据的时候记得用 synchronize 方法写入文件,否则 crash了数据就丢了。
上一节提到NSUserDefaults事实上是存成Plist文件,只是Apple帮我们封装好了读写方法而已。NSUserDefaults的 缺陷是存储只能是Library/Preferences/<Application BundleIdentifier>.plist 这个文件,如果我们要自己写一个Plist文件呢? 使用NSFileManger可以很容易办到。事实上Plist文件是XML格式的,如果你存储的数据是Plist文件支持的类型,直接用 NSFileManager的writToFile接口就可以写入一个plist文件了。 ### Plist文件支持的数据格式有: NSString, NSNumber, Boolean, NSDate, NSData, NSArray, 和NSDictionary. 其中,Boolean格式事实上以[NSNumber numberOfBool:YES/NO];这样的形式表示。NSNumber支持float和int两种格式。
1. 首先创建plist文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 文件的路径
NSString
* _path = [[
NSTemporaryDirectory
() stringByAppendingString:
@"save.plist"
] retain];
// 获取一个NSFileManger
NSFileManager
* aFileManager = [
NSFileManager
defaultManager];
if
(![aFileManager fileExistsAtPath:_path]){
// 文件不存在,创建之
NSMutableDictionary
* aDefaultDict = [[
NSMutableDictionary
alloc] init];
// 插入一个值,此时数据仍存在内存里
[aDefaultDict setObject:
@"test"
forKey:
@"TestText"
];
// 使用NSMutableDictionary的写文件接口自动创建一个Plist文件
if
(![aDefaultDict writeToFile:_path atomically:
YES
]) {
NSLog
(
@"OMG!!!"
);
}
[aDefaultDict release];
}
|
2. 写入文件
1
2
3
4
5
6
|
// 写入数据
NSMutableDictionary
* aDataDict = [
NSMutableDictionary
dictionaryWithContentsOfFile:_path];
[aDataDict setObject:_textField.text forKey:
@"TestText"
];
if
(![aDataDict writeToFile:_path atomically:
YES
]) {
NSLog
(
@"OMG!!!"
);
}
|
3. 读取文件
1
2
3
4
5
|
NSMutableDictionary
* aDataDict = [
NSMutableDictionary
dictionaryWithContentsOfFile:_path];
NSString
* aStr = [aDataDict objectForKey:
@"TestText"
];
if
(aStr !=
nil
&& aStr.length > 0) {
_textField.text = aStr;
}
|
上面介绍的几种方法中,直接用C语言的接口显然是最不方便的,拿出来的数据还得自己进行类型转换。NSUserDefaults和Plist文件支 持常用数据类型,但是不支持自定义的数据对象,好像Cocoa提供了NSCoding和NSKeyArchiver两个工具类,可以把我们自定义的对象编 码成二进制数据流,然后存进文件里面,下面的Sample为了简单我直接用cocoa的接口写成plist文件。 如果要使用这种方式进行存储,首先自定义的对象要继承NSCoding的delegate。
1
2
3
4
5
6
7
8
9
10
11
12
|
@interface
WSNSCodingData :
NSObject
<
NSCoding
>
然后继承两个必须实现的方法encodeWithCoder:和initWithCoder:
- (
void
)encodeWithCoder:(
NSCoder
*)enoder {
[enoder encodeObject:data forKey:kDATA_KEY];
}
- (
id
)initWithCoder:(
NSCoder
*)decoder {
data = [[decoder decodeObjectForKey:kDATA_KEY]
copy
];
return
[
self
init];
}
|
这里data是我自己定义的WSNSCodingData这个数据对象的成员变量,由于数据在使用过程中需要持续保存在内存中,所以类型为 copy,或者retain也可以,记得在dealloc函数里面要realease。这样,我们就定义了一个可以使用NSCoding进行编码的数据对 象。
保存数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
- (
void
)saveData {
if
(aData ==
nil
) {
aData = [[WSNSCodingData alloc] init];
}
aData.data = _textField.text;
NSLog
(
@"save data...%@"
, aData.data);
// 这里init的NSMutableData是临时用来存储数据的
NSMutableData
* data = [[
NSMutableData
alloc] init];
// 这个NSKeyedArchiver则是进行编码用的
NSKeyedArchiver
* archiver = [[
NSKeyedArchiver
alloc] initForWritingWithMutableData:data];
[archiver encodeObject:aData forKey:DATA_KEY];
[archiver finishEncoding];
// 编码完成后的NSData,使用其写文件接口写入文件存起来
[data writeToFile:_path atomically:
YES
];
[archiver release];
[data release];
NSLog
(
@"save data: %@"
, aData.data);
}
|
读取数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
- (
void
)loadData {
NSLog
(
@"load file: %@"
, _path);
NSData
* codedData = [[
NSData
alloc] initWithContentsOfFile:_path];
if
(codedData ==
nil
)
return
;
// NSKeyedUnarchiver用来解码
NSKeyedUnarchiver
* unarchiver = [[
NSKeyedUnarchiver
alloc] initForReadingWithData:codedData];
// 解码后的数据被存在一个WSNSCodingData数据对象里面
aData = [[unarchiver decodeObjectForKey:DATA_KEY] retain];
[unarchiver finishDecoding];
[unarchiver release];
[codedData release];
if
(aData.data !=
nil
) {
_textField.text = aData.data;
}
}
|
所以其实使用NSCoding和NSKeyedArchiver事实上也是写plist文件,只不过对复杂对象进行了编码使得plist支持更多数据类型而已。
如果App涉及到的数据多且杂,还涉及关系查询,那么毋庸置疑要使用到数据库了。Cocoa本身提供了CoreData这样比较重的数据库框架,下 一节会讲到,这一节讲一个轻量级的数据库——SQLite。 SQLite是C写的的,做iOS开发只需要在工程里面加入需要的框架和头文件就可以用了,只是我们得用C语言来进行SQLite操作。 关于SQLite的使用参考了这篇文章:http://mobile.51cto.com/iphone-288898.htm但是稍微有点不一样。
1. 在编写SQLite代码之前,我们需要引入SQLite3头文件:
1
|
#import <sqlite3.h>
|
2. 然后给工程加入 libsqlite3.0.dylib 框架。 3. 然后就可以开始使用了。首先是打开数据库:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
- (
void
)openDB {
NSArray
* documentsPaths =
NSSearchPathForDirectoriesInDomains
(
NSDocumentDirectory
,
NSUserDomainMask
,
YES
);
NSString
* databaseFilePath = [[documentsPaths objectAtIndex:0] stringByAppendingPathComponent:
@"mydb"
];
// SQLite存的最终还是文件,如果没有该文件则会创建一个
if
(sqlite3_open([databaseFilePath UTF8String], &_db) == SQLITE_OK) {
NSLog
(
@"Successfully open database."
);
// 如果没有表则创建一个表
[
self
creatTable];
}
}
|
3.关闭数据库,在dealloc函数里面调用:
1
2
3
|
- (
void
)closeDB {
sqlite3_close(_db);
}
|
4.创建一个表:
1
2
3
4
5
6
7
8
9
10
11
12
|
- (
void
)creatTable {
char
* errorMsg;
const
char
* createSql=
"create table if not exists datas (id integer primary key autoincrement,name text)"
;
if
(sqlite3_exec(_db, createSql,
NULL
,
NULL
, &errorMsg) == SQLITE_OK) {
NSLog
(
@"Successfully create data table."
);
}
else
{
NSLog
(
@"Error: %s"
,errorMsg);
sqlite3_free(errorMsg);
}
}
|
5. 写入数据库
1
2
3
4
5
6
7
8
9
10
|
- (
void
)saveData {
char
* errorMsg;
// 向 datas 表中插入 name = _textFiled.text 的数据
NSString
* insertSQL = [
NSString
stringWithFormat:
@"insert into datas (name) values('%@')"
, _textField.text];
// 执行该 SQL 语句
if
(sqlite3_exec(_db, [insertSQL cStringUsingEncoding:
NSUTF8StringEncoding
],
NULL
,
NULL
, &errorMsg)==SQLITE_OK) {
NSLog
(
@"insert ok."
);
}
}
|
6. 读取数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
- (
void
)loadData {
[
self
openDB];
const
char
* selectSql=
"select id,name from datas"
;
sqlite3_stmt * statement;
if
(sqlite3_prepare_v2(_db, selectSql, -1, &statement,
nil
)==SQLITE_OK) {
NSLog
(
@"select ok."
);
}
while
(sqlite3_step(statement) == SQLITE_ROW) {
int
_id = sqlite3_column_int(statement, 0);
NSString
* name = [[
NSString
alloc] initWithCString:(
char
*)sqlite3_column_text(statement, 1) encoding:
NSUTF8StringEncoding
];
NSLog
(
@"row>>id %i, name %@"
,_id,name);
_textField.text = name;
}
sqlite3_finalize(statement);
}
|
大型数据存储和管理。 XCode自带有图形化工具,可以自动生成数据类型的代码。 最终存储格式不一定存成SQLite,可以是XML等形式。