iOS中数据持久化学习笔记

本文目录结构如下:

  • 一、iOS中的沙盒机制
  • 二、文件系统
  • 三、属性列表 (Property List)
  • 四、偏好设置 (NSUserDefaults)
  • 五、对象归档
  • 六、SQLite3
  • 七、Core Data

为了避免篇幅太长,本文重点介绍前6部分内容,Core Data会在后续博文中详细讲解。

一、iOS中的沙盒机制

什么是沙盒

不管是Mac OS X 还是iOS的文件系统都是建立在UNIX文件系统基础之上的。iOS应用程序只能对自己创建的文件系统读取文件,这个独立、封闭、安全的空间,叫做沙盒。它一般存放着程序包文件(可执行文件)、图片、音频、视频、plist文件、sqlite数据库以及其他文件。每个应用程序都有自己的独立的存储空间(沙盒)。一般来说应用程序之间是不可以互相访问。(提示:在IOS8中已经开放访问)。

沙盒模型到底有哪些好处呢?

  • 安全:别的App无法修改你的程序或数据
  • 保护隐私:别的App无法读取你的程序和数据
  • 方便删除:因为一个App所有产生的内容都在自己的沙盒中,所以删除App只需要将沙盒删除就可以彻底删除程序了

iOS App沙盒中的目录

  • App Bundle :如xxx.app,其实是一个目录,里面有app本身的二进制数据以及资源文件。由于应用程序必须经过签名,
    所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动
  • Documents :最常用的目录,一般需要持久的数据都放在此目录中,可以在当中添加子文件夹,iTunes备份和恢复的时候,会包括此目录。
  • Library :设置程序的默认设置和其他状态信息。下面默认包含下面两个目录 Caches和Preferences。
    • Caches 目录用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。 iTunes不会同步此文件夹,适合存储体积大,不需要备份的非重要数据。
    • Preferences 目录包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好。 iTunes同步该应用时会同步此文件夹中的内容。
  • tmp :iTunes不会同步此文件夹,系统可能在应用没运行时就删除该目录下的文件,所以此目录适合保存应用中的一些临时文件,用完就删除。

模拟器沙盒的位置:

1
2
//位置会有所不同,你可以打印:NSHomeDirectory()来获取
NSLog(@"%@",NSHomeDirectory());

二、文件系统

1、获取沙盒目录

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

[self fileOperation_Directory];
}

/*
* 获取沙盒目录
*/

- (void)fileOperation_Directory {
//获取程序的根目录(home)目录
NSString *homePath = NSHomeDirectory();
NSLog(@"---根目录 : %@",homePath);

//获取Document目录
NSArray *docPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docPath = [docPaths lastObject];
NSLog(@"---Document目录 : %@",docPath);

//获取Library目录
NSArray *libPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libPath = [libPaths lastObject];
NSLog(@"---Library目录 : %@",libPath);

//获取Library中的Cache目录
NSArray *cachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachePath = [cachePaths lastObject];
NSLog(@"---Cache目录 : %@",cachePath);

// //获取Library中的PreferencePanes目录
// NSArray *preferencePaths = NSSearchPathForDirectoriesInDomains(NSPreferencePanesDirectory, NSUserDomainMask, YES);
// NSString *preferencePath = [preferencePaths lastObject];
// NSLog(@"---Preference目录目录 : %@",preferencePath);

//获取temp目录
NSString *tempPath = NSTemporaryDirectory();
NSLog(@"---temp目录 : %@",tempPath);
}

这些代码本身没什么好说的,但大家可能和我一样,都会有个疑问:为什么获取Document、Library目录时,需要用NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);方法?

我们每次重新运行应用程序,会发现打印的根目录会有所不同。例如:

1
2
3
根目录 : /Users/richfitbi/Library/Developer/CoreSimulator/Devices/B04AE26C-5A22-4FB1-A593-69678F42BF96/data/Containers/Data/Application/015BCA36-A207-400C-B511-3E5405A54BFC

根目录 : /Users/richfitbi/Library/Developer/CoreSimulator/Devices/B04AE26C-5A22-4FB1-A593-69678F42BF96/data/Containers/Data/Application/9074D659-52F1-4450-B36B-4711C0650140

这是因为iPhone会为每一个应用程序生成一个私有目录,
并随即生成一个数字字母串作为目录名,在每一次应用程序启动时,这个字母数字串都是不同于上一次。

为了能够方便得到Document等目录的绝对路径,便可使用NSSearchPathForDirectoriesInDomains方法。

1
NSArray *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);

参数说明:

  • directory 目录类型 比如Documents目录 就是NSDocumentDirectory
  • domainMask 在iOS的程序中这个取NSUserDomainMask
  • expandTilde YES,表示将~展开成完整路径

该方法可描述为:
NSSearchPathForDirectoriesInDomains(“想要查找的目录”,“想要从哪个路径区域保护区查找”,,“是否将~展开成完整路径”)

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//查找的区域:
typedef NS_OPTIONS(NSUInteger, NSSearchPathDomainMask) {
NSUserDomainMask = 1, // 用户的主目录
NSLocalDomainMask = 2, // 当前机器的本地目录
NSNetworkDomainMask = 4, // 在网络中公开可用的位置
NSSystemDomainMask = 8, // 被苹果系统提供的,不可更改的位置 (/System)
NSAllDomainsMask = 0x0ffff // 上述所有及未来的位置
};

//想要查找的目录:
NSApplicationDirectory = 1, // 到applications (Applications)目录下
NSDemoApplicationDirectory, // 到Applications/Demos目录下
NSDeveloperApplicationDirectory, // 到Developer/Applications目录下.
NSAdminApplicationDirectory, // 到Applications/Utilities目录下
NSLibraryDirectory, // 到Library目录下
NSDeveloperDirectory, // 到Developer目录下.
NSUserDirectory, // 到用户的主目录下
NSDocumentationDirectory, // 到documentation (Documentation)的目录下
NSDocumentDirectory, // 到documents (Documents)目录下
NSCoreServiceDirectory, // CoreServices目录的位置 (System/Library/CoreServices)
NSAutosavedInformationDirectory NS_ENUM_AVAILABLE(10_6, 4_0) = 11, // 自动保存的文档位置 (Documents/Autosaved)
NSDesktopDirectory = 12, // 本地用户的桌面
NSCachesDirectory = 13, // 本地缓冲区目录(Library/Caches)
NSApplicationSupportDirectory = 14, // 本地应用支持文件目录 (plug-ins, etc) (Library/Application Support)
NSDownloadsDirectory NS_ENUM_AVAILABLE(10_5, 2_0) = 15, // 本地下载downloads目录
NSInputMethodsDirectory NS_ENUM_AVAILABLE(10_6, 4_0) = 16, // 输入方法目录 (Library/Input Methods)
NSMoviesDirectory NS_ENUM_AVAILABLE(10_6, 4_0) = 17, // 电影目录 (~/Movies)
NSMusicDirectory NS_ENUM_AVAILABLE(10_6, 4_0) = 18, // 音乐目录 (~/Music)
NSPicturesDirectory NS_ENUM_AVAILABLE(10_6, 4_0) = 19, // 图片目录 (~/Pictures)
NSPrinterDescriptionDirectory NS_ENUM_AVAILABLE(10_6, 4_0) = 20, // PPDs目录 (Library/Printers/PPDs)
NSSharedPublicDirectory NS_ENUM_AVAILABLE(10_6, 4_0) = 21, // 本地用户分享目录 (~/Public)
NSPreferencePanesDirectory NS_ENUM_AVAILABLE(10_6, 4_0) = 22, // PreferencePanes目录的位置使用系统的偏好设置 (Library/PreferencePanes)
NSApplicationScriptsDirectory NS_ENUM_AVAILABLE(10_8, NA) = 23, // 本地用户 scripts 文件夹,对于所需的应用(~/Library/Application Scripts/code-signing-id)
NSItemReplacementDirectory NS_ENUM_AVAILABLE(10_6, 4_0) = 99, // For use with NSFileManager's URLForDirectory:inDomain:appropriateForURL:create:error:
NSAllApplicationsDirectory = 100, // 应用能够发生的所有路径
NSAllLibrariesDirectory = 101, //资源可以发生的所有目录
NSTrashDirectory NS_ENUM_AVAILABLE(10_8, NA) = 102 // 垃圾存放目录
};
NSHomeDirectory()只能到达用户的主目录

这里不知道为何,NSSearchPathDomainMask中没有preference目录对应的枚举,而是有一个NSPreferencePanesDirectory,希望哪位高人可以解释以下。

2、NSString关于路径的处理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* NSString类中,路径的处理方法
* - (NSArray *)pathComponents; //获得组成此路径的各个组成部分,结果("/","Users","apple","testfile.txt"
* - (NSString *)lastPathComponent; 提取路径的最后一个组成部分,结果:testfile.txt
* - (NSString *)stringByDeletingLastPathComponent; 删除路径的最后一个组成部分,结果:/Users/apple
* - (NSString *)stringByAppendingPathComponent:(NSString *)str; 在原路径的末尾添加新元素,结果:/Users/apple/testfile.txt/app.txt
* - (NSString *)pathExtension; 获取路径最后部分的扩展名,结果:text
* - (NSString *)stringByDeletingPathExtension; 删除路径最后部分的扩展名,结果:/Users/apple/testfile
* - (NSString *)stringByAppendingPathExtension:(NSString *)str; 路径最后部分追加扩展名,结果:/User/apple/testfile.txt.jpg
*/
- (void)filePath_NSStringPathExtensions {
NSString *path = @"/Uesrs/apple/testfile.txt";
NSLog(@"路径 : %@\n",[path pathComponents]);

NSLog(@"路径的各个组成部分 : %@",[path pathComponents]);
NSLog(@"路径的最后一个组成部分 : %@",[path lastPathComponent]);
NSLog(@"删除路径的最后一个组成部分 : %@",[path stringByDeletingLastPathComponent]);
NSLog(@"在原路径的末尾添加新元素 : %@",[path stringByAppendingPathComponent:@"app.txt"]);
NSLog(@"获取路径最后部分的扩展名 : %@",[path pathExtension]);
NSLog(@"删除路径最后部分的扩展名 : %@",[path stringByDeletingPathExtension]);
NSLog(@"路径最后部分追加扩展名 : %@",[path stringByAppendingPathExtension:@"jpg"]);
}

3、NSBundle

bundle是一个目录,其中包含了程序会使用到的资源。 这些资源包含了如图像、声音、编译好的代码、nib文件(用户也会把bundle称为plug-in)。对应bundle,cocoa提供了类NSBundle。

我们的程序是一个bundle。在Finder中,一个应用程序看上去和其他文件没有什么区别。但是实际上它是一个包含了nib文件,编译代码,以及其他资源的目录. 我们把这个目录叫做程序的main bundle。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
* NSBundle用法
*/

- (void)use_NSBundle {
//1、bundle路径
NSBundle *myBundle = [NSBundle mainBundle];
NSLog(@".app路径---%@",myBundle);
NSLog(@".app应用包(文件)的详细信息---%@",[myBundle infoDictionary]);

//2、bundle下的目录结构不同于project文档结构
NSString *imgPathInGroup = [[NSBundle mainBundle] pathForResource:@"star_32x32" ofType:@"png"];
NSLog(@"imgPathInGroup:%@",imgPathInGroup);

NSString *imgPathInFolder = [[NSBundle mainBundle] pathForResource:@"map-marker-bubble-pink-small@3x" ofType:@"png"];
NSLog(@"imgPathInFolder:%@",imgPathInFolder);

//3、Assets.xcassets中的素材
// (1)只支持png格式的图片
// (2) 图片只支持[UIImage imageNamed]的方式实例化,但是不能从Bundle中加载
// (3) 在编译时,Images.xcassets中的所有文件会被打包为Assets.car的文件

// NSString *assetsImgPathInFolder = [[NSBundle mainBundle] pathForResource:@"wangdianchaxun@3x" ofType:@"png"];
// NSLog(@"assetsImgPathInFolder:%@",assetsImgPathInFolder);


//4、获取其他bundle下的内容
NSString *amapBundlePath = [[NSBundle mainBundle] pathForResource:@"AMap" ofType:@"bundle"];
NSLog(@"amapBundlePath: %@", amapBundlePath);

NSString *redPinImgPath = [[NSBundle bundleWithPath:amapBundlePath] pathForResource:@"redPin@3x" ofType:@"png" inDirectory:@"images"];
NSLog(@"path: %@", redPinImgPath);

NSString *amap3DBundlePath = [[NSBundle mainBundle] pathForResource:@"AMap3D" ofType:@"bundle" inDirectory:@"AMap.bundle"];
NSLog(@"amap3DBundlePath: %@", amap3DBundlePath);

NSString *tglDataPath = [[NSBundle bundleWithPath:amap3DBundlePath] pathForResource:@"tgl" ofType:@"data"];
NSLog(@"tglDataPath: %@", tglDataPath);

}

需要说明的是:

1、bundle路径每次运行都会改变,原因上文介绍NSSearchPathForDirectoriesInDomains方法时已经阐述。

2、bundle下的目录结构不同于project文档结构。不管把图片放在根目录,或者放在未做文件关联的Group下(实际还是根目录),还是放在文件夹下,编译完之后,都会直接放在.app包根目录下,不会在.app包内生成文件夹。.app包位置可通过上面代码打印结果查看。

图1:工程文件Xcode中结构
工程文件Xcode中结构

图2:工程文件Finder中结构
工程文件Finder中结构

图3:工程名.app包内结构
工程名.app包内结构

3、如上图所示的avatar.png图片,同名文件,编译后,会只保留一个,会保留你最晚添加的那个文件。

4、对于Assets.xcassets中的素材:

- 只支持png格式的图片
- 图片只支持[UIImage imageNamed]的方式实例化,但是不能从Bundle中加载
- 在编译时,Images.xcassets中的所有文件会被打包为Assets.car的文件

5、[[NSBundle mainBundle] infoDictionary] 返回内容如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
BuildMachineOSBuild = 14F27;
CFBundleDevelopmentRegion = en;
CFBundleExecutable = FileReadWrite;
CFBundleIdentifier = "lyl.FileReadWrite";
CFBundleInfoDictionaryVersion = "6.0";
CFBundleInfoPlistURL = "Info.plist -- file:///Users/richfitbi/Library/Developer/CoreSimulator/Devices/B04AE26C-5A22-4FB1-A593-69678F42BF96/data/Containers/Bundle/Application/3F1A9136-B0D4-4373-AEC4-A208CFDF8039/FileReadWrite.app/";
CFBundleName = FileReadWrite;
CFBundleNumericVersion = 16809984;
CFBundlePackageType = APPL;
CFBundleShortVersionString = "1.0";
CFBundleSignature = "????";
CFBundleSupportedPlatforms = (
iPhoneSimulator
);
CFBundleVersion = 1;
DTCompiler = "com.apple.compilers.llvm.clang.1_0";
DTPlatformBuild = "";
DTPlatformName = iphonesimulator;
DTPlatformVersion = "9.0";
DTSDKBuild = 13A340;
DTSDKName = "iphonesimulator9.0";
DTXcode = 0700;
DTXcodeBuild = 7A220;
LSRequiresIPhoneOS = 1;
MinimumOSVersion = "9.0";
UIDeviceFamily = (
1
);
UILaunchStoryboardName = LaunchScreen;
UIMainStoryboardFile = Main;
UIRequiredDeviceCapabilities = (
armv7
);
UISupportedInterfaceOrientations = (
UIInterfaceOrientationPortrait,
UIInterfaceOrientationLandscapeLeft,
UIInterfaceOrientationLandscapeRight
);
}

我们可以根据这个Dictionary获取大量信息,例如
[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]]
可任意获取版本信息。

4、NSData

NSData是用来包装数据的。NSData存储的是二进制数据,屏蔽了数据之间的差异,文本、音频、图像等数据都可用NSData来存储。

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
26
27
28
29
30
31
32
33
/*
* NSData转换
*/

- (void)transform_NSData {
//1、NSString->NSData
NSString *aString = @"1234abcd";
NSData *aData = [aString dataUsingEncoding: NSUTF8StringEncoding];

//2、NSData-> NSString
NSString *aString2 = [[NSString alloc] initWithData:aData encoding:NSUTF8StringEncoding];
NSLog(@"%@",aString2);

//3、NSData->UIImage
NSString *path = [[NSBundle mainBundle] bundlePath];
NSString *name = [NSString stringWithFormat:@"avatar.png"];
NSString *finalPath = [path stringByAppendingPathComponent:name];
NSData *imageData = [NSData dataWithContentsOfFile: finalPath];
UIImage *aimage = [UIImage imageWithData: imageData];

//4、NSData与NSArray NSDictionary

//注意下面两种方式获取的路径是不同的
//app包中的plist路径,文件只读
NSString *plistPath1 = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@"plist"];

//沙盒中的plist路径,文件可读写(注意我们演示用的Info.plist不在Documents中,只存在于bundle中)
NSString *plistPath2 = [NSString stringWithFormat:@"%@/%@/%@", NSHomeDirectory(),@"Documents",@"Info.plist"];
NSLog(@"\n\n%@\n\n%@",plistPath1,plistPath2);

//NSData *plistData = [NSData dataWithContentsOfFile: plistPath1];
NSDictionary *dic1 = [NSDictionary dictionaryWithContentsOfFile:plistPath1];
NSLog(@"%@",dic1);
}

5、NSFileManager

用于执行一般的文件系统操作。
主要功能包括:从一个文件中读取数据;向一个文件中写入数据;删除文件;复制文件;移动文件;比较两个文件的内容;测试文件的存在性;读取/更改文件的属性等等。

没什么好说的,直接上代码,注意一下”文件移动”就行了。代码很凌乱,有兴趣的同学可以整理下,形成自己的工具库。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/*
* NSFileManager用法
*/

- (void)use_NSFileManager {
NSFileManager *fileManager = [NSFileManager defaultManager];

NSArray *docPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docPath = [docPaths lastObject];

//-----------------------创建文件夹(文件夹已经存在,也是提示创建成功)
//NSString *folderPath1 = [NSString stringWithFormat:@"%@/%@/%@", NSHomeDirectory(),@"Documents",@"MyFolder1"];
NSString *folderPath1 = [docPath stringByAppendingPathComponent:@"MyFolder1"];
//判断文件夹是否存在
BOOL isDir = YES;
if ([fileManager fileExistsAtPath:folderPath1 isDirectory:&isDir]) {
NSLog(@"MyFolder1文件夹已存在");
}

BOOL folder1CreateSuccess = [fileManager createDirectoryAtPath:folderPath1 withIntermediateDirectories:YES attributes:nil error:nil];
if (folder1CreateSuccess) {
NSLog(@"文件夹创建成功");
}else {
NSLog(@"文件夹创建失败");
}

//-----------------------创建文件(文件已经存在,也是提示创建成功。若文件已存在,会覆盖原有数据)
NSString *file1Path = [folderPath1 stringByAppendingPathComponent:@"test1.txt"];
//判断文件是否存在
if ([fileManager fileExistsAtPath:file1Path]) {
NSLog(@"test1.txt文件已存在");
}

NSString *file1Text = @"abcdefg";
NSData *file1Data = [file1Text dataUsingEncoding: NSUTF8StringEncoding];
BOOL file1CreateSuccess = [fileManager createFileAtPath:file1Path contents:file1Data attributes:nil];
if (file1CreateSuccess) {
NSLog(@"文件创建成功: %@" ,file1Path);
}else {
NSLog(@"文件创建失败");
}
//-----------------------再创建一个文件夹、文件
NSString *folderPath2 = [docPath stringByAppendingPathComponent:@"MyFolder2"];
folderPath2 = [folderPath2 stringByAppendingPathComponent:@"MyFolder2Sub"];
//第二个参数:YES时表示会创建路径内的父层文件夹,NO时如果父层文件夹不存在会创建失败
[fileManager createDirectoryAtPath:folderPath2 withIntermediateDirectories:YES attributes:nil error:nil];

NSString *file2Path = [folderPath2 stringByAppendingPathComponent:@"test2.txt"];
NSString *file2Text = @"hijklmn";
NSData *file2Data = [file2Text dataUsingEncoding: NSUTF8StringEncoding];
[fileManager createFileAtPath:file2Path contents:file2Data attributes:nil];
//-----------------------写入内容
NSString *content = @"测试写入内容!";
//会覆盖原有内容
BOOL res = [content writeToFile:file1Path atomically:YES encoding:NSUTF8StringEncoding error:nil];
if (res) {
NSLog(@"文件写入成功");
}else
NSLog(@"文件写入失败");
//-----------------------读取内容
//NSFileManager-读取内容
NSData *fileManagerGetData = [fileManager contentsAtPath:file1Path];
NSLog(@"NSFileManager-读取内容 : %@",[[NSString alloc] initWithData:fileManagerGetData encoding:NSUTF8StringEncoding]);

//NSData-读取内容
NSData *nsDataGetData = [NSData dataWithContentsOfFile:file1Path];
NSLog(@"NSData-读取内容 : %@",[[NSString alloc] initWithData:nsDataGetData encoding:NSUTF8StringEncoding]);

//NSString-读取内容
NSString *nsStringcontent = [NSString stringWithContentsOfFile:file1Path encoding:NSUTF8StringEncoding error:nil];
NSLog(@"NSString-读取内容 : %@",nsStringcontent);
// + (id)stringWithContentsOfFile:(NSString *)path usedEncoding:(NSStringEncoding *)enc error:(NSError **)error;
// 是自动判断encode,如果打开成功,把encode放在enc 里,返回给调用者。
// 声明一个NSStringEncoding 类型(其实就是NSUInteger)然后送指针给方法就是了。例如
// NSStringEncoding enc;
// NSString *string = [NSString stringWithContentsOfFile:path usedEncoding:&enc error:nil];
// 成功之后你可以检查 enc 以确定 string 的编码。

//-----------------------获取文件大小
NSDictionary *attrDic = [fileManager attributesOfItemAtPath:file1Path error:nil];
NSNumber *fileSize = [attrDic objectForKey:NSFileSize];
NSLog(@"fileSize : %llu",[fileSize unsignedLongLongValue]);
//-----------------------获取目录文件信息
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:docPath];
NSString *path = nil;
while ((path = [dirEnum nextObject]) != nil){ NSLog(@"%@",path);
}

//==================================================================//
///////下面代码设计文件移动、删除等功能,因此最好打断点查看////////////////
//==================================================================//

//-----------------------移动文件(重命名)
NSString *toPath = [docPath stringByAppendingPathComponent:@"MyFolder3"];
[fileManager createDirectoryAtPath:toPath withIntermediateDirectories:YES attributes:nil error:nil];
//在此时,文件结构:
//Documents
//-----MyFolder1
// -----test1.txt
//-----MyFolder2
// -----MyFolder2Sub
// -----test2.txt
//-----MyFolder3
//现在,想把test2.txt文件,移动到MyFolder3下,srcPath为test2.txt路径没有问题,但对于dstPath,正常思维,应该是MyFolder3的路径即可。但你要真这么想,肯定会移动失败。因为方法dstPath参数描述中明确说明:This path must include the name of the file or directory in its new location。也就是说,该方法另一层含义是移动文件并且重命名,如果你保持原有的文件名,那么就好像只是移动了文件。
NSError *error;
toPath = [toPath stringByAppendingPathComponent:@"test2.txt"];
BOOL isMoveSuccess = [fileManager moveItemAtPath:file2Path toPath:toPath error:&error];
if (isMoveSuccess) {
NSLog(@"文件移动成功");
}else {
NSLog(@"%@",error.localizedDescription);
}
//-----------------------不移动文件,只重命名
NSString *toPath2 = [docPath stringByAppendingPathComponent:@"MyFolder3"];
[fileManager moveItemAtPath:[toPath2 stringByAppendingPathComponent:@"test2.txt"] toPath:[toPath2 stringByAppendingPathComponent:@"test3.txt"] error:&error];
//-----------------------复制文件(重命名),和移动文件原理相同
NSString *srcPath3 = [docPath stringByAppendingPathComponent:@"MyFolder3"];
srcPath3 = [srcPath3 stringByAppendingPathComponent:@"test3.txt"];
NSString *dstPath3 = [docPath stringByAppendingPathComponent:@"MyFolder2"];
dstPath3 = [dstPath3 stringByAppendingPathComponent:@"test2.txt"];

[fileManager copyItemAtPath:srcPath3 toPath:dstPath3 error:&error];

//-----------------------删除文件
[fileManager removeItemAtPath:[docPath stringByAppendingPathComponent:@"MyFolder2"] error:&error];
}

6、NSFileHandle

  • NSFileHandle类主要对文件内容进行读取和写入操作
  • NSFileManager类主要对文件的操作(删除、修改、移动、复制等等)

NSFileHandle类可以实现如下功能:

  1. 打开一个文件,执行读、写或更新(读写)操作;
  2. 在文件中查找指定位置;
  3. 从文件中读取特定数目的字节,或将特定数目的字节写入文件中
  4. 另外,NSFileHandle类提供的方法也可以用于各种设备或套接字。

一般而言,我们处理文件时都要经历以下三个步骤:

  1. 打开文件,获取一个NSFileHandle对象(以便在后面的I/O操作中引用该文件)。
  2. 对打开文件执行I/O操作。
  3. 关闭文件。
1
2
3
4
5
6
7
8
NSFileHandle *fileHandle = [[NSFileHandle alloc]init];
fileHandle = [NSFileHandle fileHandleForReadingAtPath:path]; //打开一个文件准备读取
fileHandle = [NSFileHandle fileHandleForWritingAtPath:path];
fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:path];
fileData = [fileHandle availableData]; // 从设备或者通道返回可用的数据
fileData = [fileHandle readDataToEndOfFile];
[fileHandle writeData:fileData]; //将NSData数据写入文件
[fileHandle closeFile]; //关闭文件

常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+ (id)fileHandleForReadingAtPath:(NSString *)path  打开一个文件准备读取     

+ (id)fileHandleForWritingAtPath:(NSString *)path 打开一个文件准备写入

+ (id)fileHandleForUpdatingAtPath:(NSString *)path 打开一个文件准备更新

- (NSData *)availableData; 从设备或通道返回可用的数据

- (NSData *)readDataToEndOfFile; 从当前的节点读取到文件的末尾

- (NSData *)readDataOfLength:(NSUInteger)length; 从当前节点开始读取指定的长度数据

- (void)writeData:(NSData *)data; 写入数据

- (unsigned long long)offsetInFile; 获取当前文件的偏移量

- (void)seekToFileOffset:(unsigned long long)offset; 跳到指定文件的偏移量

- (unsigned long long)seekToEndOfFile; 跳到文件末尾

- (void)truncateFileAtOffset:(unsigned long long)offset; 将文件的长度设为offset字节

- (void)closeFile; 关闭文件

三、属性列表(Property List)

属性列表提供了一个方便的方式来存储简单的结构化数据。它是以XML格式存储的。您不能使用属性列表保存所有类型的数据。在属性列表中的项目的数据类型,包括:数组(Array)、字典(Dictionary)、字符串(String)等等。

属性列表常被IOS用于保存应用程序设置属性,但这并不意味着你不使用它到其他用途。但它是被设计用于存储少量的数据。

所谓用属性列表进行数据持久化, 就是针对数组、字典集合类调用writeToFile:atomically方法和initWithContentsOfFile方法来写入数据。数组、字典只能将BOOL、NSNumber、NSString、NSData、NSDate、NSArray、NSDictionary写入属性列表plist文件

1、属性列表的结构

当我们新建一个Contacts.plist文件之后,Open as Source Code,查看它的结构如下:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>

</dict>
</plist>

我们可以在Open as Property List的查看模式下,选中root,可以看到type列可以更改root为Dictionary或是Array。更改成Array之后,Open as Source Code,查看它的结构如下:

1
2
3
4
5
<?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">
<array/>
</plist>

plist文件root下添加其它节点时,可以看到type列可以选择:Dictionary、Array、Boolean、Data、Date、Number、String类型。

2、用属性列表保存数据

工程下的plist文件,我们不能在项目运行时修改,因此需要将项目资源的Contacts.plist文件中数据复制到沙箱Documents目录下再进行增删改查操作。

当我们用String、NSData类型直接写入文件时,会破坏plist文件结构:
比如:

1
2
3
4
- (void)writeStringDataToPList {
NSString *content = @"abcd";
[content writeToFile:self.operateFilePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}

运行之后,你会发现plist文件原本的XML结构没有了,成了纯文本文件。因此,我们应该用属性列表保存的数据,应该是NSDictionary、NSArray。

示例代码如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#import "ViewController.h"

@interface ViewController ()

@property(nonatomic, strong) NSString *operateFilePath;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.operateFilePath = [self applicationDocumentsDirectoryFile];
[self createEditableCopyOfDatabaseIfNeeded];
[self writeArrayDataToPList];
}

//获取放置在沙箱Documents目录下的文件的完整路径
- (NSString *)applicationDocumentsDirectoryFile
{
NSString *documentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *path = [documentDirectory stringByAppendingPathComponent:@"Contacts.plist"];
NSLog(@"Document path :%@",path);
return path;
}

//对文件进行预处理,判断在Documents目录下是否存在plist文件,如果不存在则从资源目录下复制一个。
-(void)createEditableCopyOfDatabaseIfNeeded
{
NSFileManager *fileManager = [NSFileManager defaultManager];

BOOL dbexits = [fileManager fileExistsAtPath:self.operateFilePath];
if (!dbexits) {
NSLog(@"resourcePath :%@",[[NSBundle mainBundle] resourcePath]);
NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Contacts.plist"];

NSError *error;
BOOL success=[fileManager copyItemAtPath:defaultDBPath toPath:self.operateFilePath error:&error];

if (!success) {
NSAssert1(0,@"错误写入文件:‘%@’",[error localizedDescription]);
}
}
}

- (void)writeStringDataToPList {
NSString *content = @"abcd";
[content writeToFile:self.operateFilePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}

- (void)writeArrayDataToPList {
NSArray *array = @[@(123), @"中国", @(3), @(3LL), @(-3), @(3.0), @(3.1), @(3.1f)];
[array writeToFile:self.operateFilePath atomically:YES];

//打印文件内容
NSString *stringcontent = [NSString stringWithContentsOfFile:self.operateFilePath encoding:NSUTF8StringEncoding error:nil];
NSLog(@"NSString-读取内容 : %@",stringcontent);
//文件内容转换回Array
NSArray *arrayContent = [NSArray arrayWithContentsOfFile:self.operateFilePath];
NSLog(@"arrayContent : %@",arrayContent);
}

- (void)writeDictionaryDataToPList {
NSDictionary *dic = @{@"first" : @"中国",
@"second" : @(3),
@"third" : @(3LL),
@"fourth" : @(-3),
@"fifth" : @(3.0),
@"sixth" : @(3.1),
@"seventh" : @(3.1f),
};
[dic writeToFile:self.operateFilePath atomically:YES];
//打印文件内容
NSString *stringcontent = [NSString stringWithContentsOfFile:self.operateFilePath encoding:NSUTF8StringEncoding error:nil];
NSLog(@"NSString-读取内容 : %@",stringcontent);
//文件内容转换回Dictionary
NSDictionary *dicContent = [NSDictionary dictionaryWithContentsOfFile:self.operateFilePath];
NSLog(@"arrayContent : %@",dicContent);
}

运行writeArrayDataToPList方法之后,打印结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NSString-读取内容 : <?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">
<array>
true<integer>123</integer>
true<string>中国</string>
true<integer>3</integer>
true<integer>3</integer>
true<integer>-3</integer>
true<real>3</real>
true<real>3.1000000000000001</real>
true<real>3.0999999046325684</real>
</array>
</plist>
arrayContent : (
123,
"\U4e2d\U56fd",
3,
3,
"-3",
3,
"3.1",
"3.099999904632568"
)

你会发现对于浮点数,保存后不准确了,这个问题的根源大家自行google,或看下唐巧大神的这篇文章。如果只是涉及存取,建议大家还是存字符串比较好。如果涉及精确计算,可以参考iOS开发:Objective-C精确的货币计算

另外需要注意的是,如果集合或者字典中包含Dictionary、Array、Boolean、Data、Date、Number、String之外的任何类型,都无法写入plist文件,您可以自行实验一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//该方法无法写入plist文件
- (void)writeDictionaryHaveObjectDataToPList {
Person *p1 = [[Person alloc] init];
p1.personName = @"Jack";
p1.personPhoneNumber = @"13100000000";
Person *p2 = [[Person alloc] init];
p2.personName = @"Rose";
p2.personPhoneNumber = @"13788888888";

NSDictionary *dic = @{@"first" : p1,
@"second" : p2,
};
[dic writeToFile:self.operateFilePath atomically:YES];
//打印文件内容
NSString *stringcontent = [NSString stringWithContentsOfFile:self.operateFilePath encoding:NSUTF8StringEncoding error:nil];
NSLog(@"NSString-读取内容 : %@",stringcontent);
//文件内容转换回Dictionary
NSDictionary *dicContent = [NSDictionary dictionaryWithContentsOfFile:self.operateFilePath];
NSLog(@"arrayContent : %@",dicContent);
}

四、偏好设置(NSUserDefaults)

NSUserDefaults适合存储轻量级的本地数据,用来保存应用程序设置和属性,用户保存的数据如用户名、密码。用户再次打开程序或开机后这些数据仍然存在。

使用NSUserDefaults保存的数据,会保存在特定的plist文件中,实际上就是上文我们所说的使用属性列表进行数据持久化。只不过自己建立的plist文件,需要手动创建文件,读取文件,很麻烦,而使用NSUserDefaults则不用管这些东西,就像读字符串一样,直接读取就可以了。

NSUserDefaults支持的数据格式有:NSNumber(Integer、Float、Double),NSString,NSDate,NSArray,NSDictionary,BOOL类型。如果要存储其他类型,则需要转换为前面的类型,才能用NSUserDefaults存储。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

[self saveNSUserDefaults];

[self readNSUserDefaults];
}

//保存数据到NSUserDefaults
-(void)saveNSUserDefaults
{
NSString *myString = @"lyl";
int myInteger = 100;
float myFloat = 50.0f;
double myDouble = 20.0;
BOOL isMe = YES;
NSDate *myDate = [NSDate date];
NSArray *myArray = [NSArray arrayWithObjects:@"hello", @"world", nil];
NSDictionary *myDictionary = [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:@"lyl", @"30", nil] forKeys:[NSArray arrayWithObjects:@"name", @"age", nil]];

//将上述数据全部存储到NSUserDefaults中
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//存储时,除NSNumber类型使用对应的类型意外,其他的都是使用setObject:forKey:
[userDefaults setInteger:myInteger forKey:@"myInteger"];
[userDefaults setFloat:myFloat forKey:@"myFloat"];
[userDefaults setDouble:myDouble forKey:@"myDouble"];
[userDefaults setBool:isMe forKey:@"isMe"];
[userDefaults setObject:myString forKey:@"myString"];
[userDefaults setObject:myDate forKey:@"myDate"];
[userDefaults setObject:myArray forKey:@"myArray"];
[userDefaults setObject:myDictionary forKey:@"myDictionary"];

//这里建议同步存储到磁盘中,但是不是必须的
[userDefaults synchronize];

}

//从NSUserDefaults中读取数据
-(void)readNSUserDefaults
{
NSUserDefaults *userDefaultes = [NSUserDefaults standardUserDefaults];

//读取数据到各个label中
//读取整型int类型的数据
NSInteger myInteger = [userDefaultes integerForKey:@"myInteger"];
NSLog(@"%@",[NSString stringWithFormat:@"%ld",myInteger]);

//读取浮点型float类型的数据
float myFloat = [userDefaultes floatForKey:@"myFloat"];
NSLog(@"%@", [NSString stringWithFormat:@"%f",myFloat]);

//读取double类型的数据
double myDouble = [userDefaultes doubleForKey:@"myDouble"];
NSLog(@"%@",[NSString stringWithFormat:@"%f",myDouble]);

//读取NSString类型的数据
NSString *myString = [userDefaultes stringForKey:@"myString"];
NSLog(@"%@", myString);

//读取NSDate日期类型的数据
NSDate *myDate = [userDefaultes valueForKey:@"myDate"];
NSDateFormatter *df = [[NSDateFormatter alloc] init];
[df setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSLog(@"%@",[NSString stringWithFormat:@"%@",[df stringFromDate:myDate]]);

//读取数组NSArray类型的数据
NSArray *myArray = [userDefaultes arrayForKey:@"myArray"];
NSString *myArrayString = [[NSString alloc] init];
for(NSString *str in myArray)
{
NSLog(@"str= %@",str);
myArrayString = [NSString stringWithFormat:@"%@ %@", myArrayString, str];
[myArrayString stringByAppendingString:str];
// [myArrayString stringByAppendingFormat:@"%@",str];
NSLog(@"myArrayString=%@",myArrayString);
}
NSLog(@"%@",myArrayString);

//读取字典类型NSDictionary类型的数据
NSDictionary *myDictionary = [userDefaultes dictionaryForKey:@"myDictionary"];
NSString *myDicString = [NSString stringWithFormat:@"name:%@, age:%ld",[myDictionary valueForKey:@"name"], [[myDictionary valueForKey:@"age"] integerValue]];
NSLog(@"%@",myDicString);
}
@end
  1. NSUserDefaults保存的数据,默认会存储在 /Library/Prefereces/[projectName].plist中,开发时可以通过Finder前往如下位置查看

    1
    2
    > /Users/richfitbi/Library/Developer/CoreSimulator/Devices/9DBB8396-CFC2-4320-A3D8-6FA2826DDF29/data/Containers/Data/Application/
    >

请根据自己的目录结构自行调整。

  1. 使用偏好设置对数据进行保存之后, 它保存到系统的时间是不确定的,会在将来某一时间点自动将数据保存到Preferences文件夹下面,如果需要即刻将数据存储,可以使用[defaults synchronize];

  2. 所有的信息都写在一个文件中,即/Library/Prefereces/[projectName].plist中。

五、对象归档(NSKeyedArchiver)

属性列表、偏好设置使用都有局限性,就是无法保存自定义的数据类。要解决这个问题,我们使用归档方法。归档方法实际就是 用 NSKeyedArchiver 对 自定义类进行编解码成 NSMutableData 后,再对NSMutableData实行序列化。具体的编解码是由NSCoder实现的。

1、名词解释

在开始讲NSKeyedArchiver之前,我们先解释一些名词:

  • Archives/UnArchives
  • Serializations/UnSerializations
  • Encoding/Decoding
  • Boxing/UnBoxing

(1)归档(Archives)和序列化(Serializations)

归档,在其他语言中又叫“序列化”,就是将对象保存到硬盘;解档,在其他语言又叫“反序列化”就是将硬盘文件还原成对象。其实归档就是数据存储的过程。

事实上在iOS中,归档(Archives)和序列化(Serializations)有着不同的内涵。我们看一下官方文档Archives and Serializations Programming Guide中的Introduction,翻译过来就是:

归档(Archives)和序列化(Serializations)是将对象转换成字节流的两种方式。字节流便于保存和网络传输。当字节流解码后,可恢复对象的数据结构。归档详细记录了数据和值的关联关系,而序列化仅仅记录属性列表的简单层级关系。

说白了,归档(Archives)和序列化(Serializations)都是将对象转换成字节流以便保存或传输,区别在于:

  • 归档(Archives):自定义对象转换为字节流
  • 序列化(Serializations):某些特定类型(NSDictionary, NSArray, NSString, NSDate, NSNumber,NSData)的数据转换为字节流(通常将其保存为plist文件)

相应的相反行为

  • 解档(UnArchives):字节流转换为自定义对象
  • 反序列化(UnSerializations):字节流(通常将其保存为plist文件)转换为某些特定类型(NSDictionary, NSArray, NSString, NSDate, NSNumber,NSData)

(2)编码(Encoding)和解码(Decoding)

归档时,我们需要对数据进行编码(Encoding),比如

1
2
3
[archiver encodeCGSize:size1 forKey:@"size"];
[archiver encodeObject:number1 forKey:@"number"];
[archiver encodeObject:str1 forKey:@"string"];

解档时,我们需要对数据进行解码(Decoding),比如

1
2
3
size2 = [unarchiver decodeCGSizeForKey:@"size"];
number2 = [unarchiver decodeObjectForKey:@"number"];
str2 = [unarchiver decodeObjectForKey:@"string"];

(3)装箱(Boxing)和拆箱(UnBoxing)

我们知道,数组和字典中只能存储对象类型,其他基本类型和结构体是无法直接放到数组和字典中。把基本类型或结构体转化为Object就是一个装箱(Boxing)的过程,反过来将这个Object对象转换为基本数据类型或结构体的过程就是拆箱(UnBoxing)。

一般将基本数据类型装箱成NSNumber类型。是NSObject的子类,提供了大量方法来简化装箱、拆箱的操作。如+(NSNumber *)numberWithInt:(int)value;方法是对int类型数据装箱,而-(int)intValue方式是把NSNumber类型拆箱为int类型。

对于结构体,则装箱成NSValue类型。对于常用的结构体Foundation已经为我们提供好了具体的装箱方法:

1
2
3
4
5
+(NSValue *)valueWithPoint:(NSPoint)point;

+(NSValue *)valueWithSize:(NSSize)size;

+(NSValue *)valueWithRect:(NSRect)rect;

对应的拆箱方法:

1
2
3
4
5
-(NSPoint)pointValue;

-(NSSize)sizeValue;

-(NSRect)rectValue;

而对于自定义结构体,我们需要使用NSValue如下方法进行装箱:

1
+(NSValue *)valueWithBytes:(const void *)value objCType:(const char *)type;

调用下面的方法进行拆箱:

1
-(void)getValue:(void *)value;

完整例子如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#import <Foundation/Foundation.h>

typedef struct {
int year;
int month;
int day;
} Date;

void test1(){
CGPoint point1=CGPointMake(10, 20);
NSValue *value1=[NSValue valueWithPoint:point1];//对于系统自带类型一般都有直接的方法进行包装
NSArray *array1=[NSArray arrayWithObject:value1];//放到数组中
NSLog(@"%@",array1);
/*结果:
(
"NSPoint: {10, 20}"
)
*/


NSValue *value2=[array1 lastObject];
CGPoint point2=[value2 pointValue];//同样对于系统自带的结构体有对应的取值方法
NSLog(@"x=%f,y=%f",point2.x,point2.y);//结果:x=10.000000,y=20.000000
}

void test2(){
//如果我们自己定义的结构体包装
Date date={2015,9,21};
char *type=@encode(Date);
NSValue *value3=[NSValue value:&date withObjCType:type];//第一参数传递结构体地址,第二个参数传递类型字符串
NSArray *array2=[NSArray arrayWithObject:value3];
NSLog(@"%@",array2);
/*结果:
(
"<df070000 09000000 15000000>"
)
*/


Date date2;
[value3 getValue:&date2];//取出对应的结构体,注意没有返回值
//[value3 objCType]//取出包装内容的类型
NSLog(@"%i,%i,%i",date2.year,date2.month,date2.day); //结果:2014,2,28
}


int main(int argc, const char * argv[]) {
@autoreleasepool {
test1();
test2();
}
return 0;
}

2、NSKeyedArchiver用法

要针对更多对象归档或者需要归档时能够加密的话就需要使用NSKeyedArchiver进行归档和解档,使用这种方式归档的范围更广而且归档内容是密文存储。

  • 从归档范围来讲NSKeyedArchiver适合所有ObjC对象,但是对于自定义对象我们需要实现NSCoding协议;

  • 从归档方式来讲NSKeyedArchiver分为简单归档和复杂对象归档

    • 简单归档就是针对单个对象可以直接将对象作为根对象(不用设置key)

    • 复杂对象就是针对多个对象,存储时不同对象需要设置不同的Key。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#import "ViewController.h"

@interface ViewController ()

@property(nonatomic,strong) NSString *documentPath;

@end

@implementation ViewController

- (NSString *)documentPath {
if (!_documentPath) {
_documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
}
return _documentPath;
}


- (void)viewDidLoad {
[super viewDidLoad];

//[self test1];
[self test2];
}


/**
* 系统简单对象归档
*/

- (void)test1 {
//NSString归档
NSString *str1 = @"Hello,world!";
NSString *filePath1 = [self.documentPath stringByAppendingPathComponent:@"archiver1.arc"];
[NSKeyedArchiver archiveRootObject:str1 toFile:filePath1];

//NSString解档
NSString *str2= [NSKeyedUnarchiver unarchiveObjectWithFile:filePath1];
NSLog(@"str2=%@",str2);

//NSArray归档
NSArray *array1 = @[@"A",@(123),@(123.5)];
NSString *filePath2 = [self.documentPath stringByAppendingPathComponent:@"archiver2.arc"];
[NSKeyedArchiver archiveRootObject:array1 toFile:filePath2];

//NSArray解档
NSArray *array2 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath2];
[array2 enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"array2[%lu]=%@",idx,obj);
}];
}

/**
* 系统复杂对象归档(多对象归档)
*/

- (void)test2 {
//归档
NSString *filePath3 = [self.documentPath stringByAppendingPathComponent:@"archiver3.arc"];

int int1 = 64;
CGSize size1 = {320.0,960.0};
NSNumber *number1 = @3.14;
NSString *str1 = @"Hello,world!";
NSArray *array1 = @[@"A",@"B",@"C"];
NSDictionary *dic1 = @{@"name":@"LYL",@"age":@30};

//同时对多个对象进行归档
NSMutableData *data1 = [[NSMutableData alloc]init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data1];//定义归档对象
[archiver encodeInt:int1 forKey:@"int"];//对int1归档并指定一个key以便以后读取
[archiver encodeCGSize:size1 forKey:@"size"];
[archiver encodeObject:number1 forKey:@"number"];
[archiver encodeObject:str1 forKey:@"string"];
[archiver encodeObject:array1 forKey:@"array"];
[archiver encodeObject:dic1 forKey:@"dic"];

[archiver finishEncoding];//结束归档

[data1 writeToFile:filePath3 atomically:YES];//写入文件

//解档
int int2;
CGSize size2;
NSNumber *number2;
NSString *str2;
NSArray *array2;
NSDictionary *dic2;

NSData *data2 = [[NSData alloc] initWithContentsOfFile:filePath3];//读出数据到NSData
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]initForReadingWithData:data2];
int2 = [unarchiver decodeInt32ForKey:@"int"];
//int2 = [unarchiver decodeInt64ForKey:@"int"];
size2 = [unarchiver decodeCGSizeForKey:@"size"];
number2 = [unarchiver decodeObjectForKey:@"number"];
str2 = [unarchiver decodeObjectForKey:@"string"];
array2 = [unarchiver decodeObjectForKey:@"array"];
dic2 = [unarchiver decodeObjectForKey:@"dic"];

[unarchiver finishDecoding];

NSLog(@"int2=%i,size=%@,number2=%@,str2=%@,array2=%@,dic2=%@",int2,NSStringFromCGSize(size2),number2,str2,array2,dic2);

}

@end

接下来看一下自定义的对象如何归档,上面说了如果要对自定义对象进行归档那么这个对象必须实现NSCoding协议,在这个协议中有两个方法都必须实现:

1
2
3
-(void)encodeWithCoder:(NSCoder *)aCoder;//通过给定的Archiver对消息接收者进行编码;

-(id)initWithCoder:(NSCoder *)aDecoder;//从一个给定的Unarchiver的数据返回一个初始化对象;

这两个方法分别在归档和解档时调用。

Person.h

1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>

@interface Person : NSObject<NSCoding>

@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) int age;
@property (nonatomic,assign) float height;
@property (nonatomic,assign) NSDate *birthday;

@end

Person.m

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
26
27
28
29
30
31
32
33
34
#import "Person.h"

@implementation Person

#pragma mark 解码
-(id)initWithCoder:(NSCoder *)aDecoder{
NSLog(@"decode...");
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [aDecoder decodeInt32ForKey:@"age"];
self.height = [aDecoder decodeFloatForKey:@"heiht"];
self.birthday = [aDecoder decodeObjectForKey:@"birthday"];
}
return self;
}

#pragma mark 编码
-(void)encodeWithCoder:(NSCoder *)aCoder{
NSLog(@"encode...");
[aCoder encodeObject:_name forKey:@"name"];
[aCoder encodeInt32:_age forKey:@"age" ];
[aCoder encodeFloat:_height forKey:@"height"];
[aCoder encodeObject:_birthday forKey:@"birthday"];

}

#pragma mark 重写描述
-(NSString *)description{
NSDateFormatter *formater1 = [[NSDateFormatter alloc]init];
formater1.dateFormat = @"yyyy-MM-dd";
return [NSString stringWithFormat:@"name=%@,age=%i,height=%.2f,birthday=%@",_name,_age,_height,[formater1 stringFromDate:_birthday]];
}

@end

ViewController.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)test3 {
//归档
Person *person1 = [[Person alloc] init];
person1.name = @"Kenshin";
person1.age = 28;
person1.height = 1.72;
NSDateFormatter *formater1 = [[NSDateFormatter alloc] init];
formater1.dateFormat = @"yyyy-MM-dd";
person1.birthday = [formater1 dateFromString:@"1986-08-08"];

NSString *filePath4 = [self.documentPath stringByAppendingPathComponent:@"archiver4.arc"];

[NSKeyedArchiver archiveRootObject:person1 toFile:filePath4];

//解档
Person *person2= [NSKeyedUnarchiver unarchiveObjectWithFile:filePath4];
NSLog(@"%@",person2);

}

注意:对自定义对象进行归档

  • 必须遵循并实现NSCoding协议
  • 保存文件的扩展名可以任意指定
  • 继承时必须先调用父类的归档解档方法

六、SQLite3

之前的所有存储方法,都是覆盖存储。如果想要增加一条数据就必须把整个文件读出来,然后修改数据后再把整个内容覆盖写入文件。所以它们都不适合存储大量的内容。要存储大量数据,我们需要使用SQLite数据库。

Mac上自带安装了SQLite3 ,如果你之前接触过关系型数据库,可以通过命令行来对SQLite进行初步的认识:

1
2
3
4
5
6
7
8
Last login: Thu Oct 15 16:52:02 on ttys000
RichfitBIdeMacBook-Pro:~ richfitbi$ sqlite3 test.db
SQLite version 3.8.10.2 2015-05-20 18:17:19
Enter ".help" for usage hints.
sqlite> create table if not exists names(id integer primary key asc, name text);
sqlite> insert into names(name) values('LYL');
sqlite> select * from names;
1|LYL

SQLite数据库的几个特点:

  1. 基于C语言开发的轻型数据库
  2. 在iOS中需要使用C语言语法进行数据库操作、访问(无法使用ObjC直接访问,因为libsqlite3框架基于C语言编写)
  3. SQLite中采用的是动态数据类型,即使创建时定义了一种类型,在实际操作时也可以存储其他类型,但是推荐建库时使用合适的类型(特别是应用需要考虑跨平台的情况时)
  4. 建立连接后通常不需要关闭连接(尽管可以手动关闭)

SQLite数据类型:

  • integer : 整数
  • real : 实数(浮点数)
  • text : 文本字符串
  • blob : 二进制数据,比如文件,图片之类的

其中,主键必须设置成integer

iOS中操作SQLite数据库步骤

在iOS中操作SQLite数据库可以分为以下几步(注意先要添加库文件:libsqlite3.dylib,IOS9 之后变为libsqlite3.tbd, 并导入主头文件):

  1. 打开数据库,利用sqlite3_open()打开数据库会指定一个数据库文件保存路径,如果文件存在则直接打开,否则创建并打开。打开数据库会得到一个sqlite3类型的对象,后面需要借助这个对象进行其他操作。
  2. 执行SQL语句,执行SQL语句又包括有返回值的语句和无返回值语句。
  3. 对于无返回值的语句(如增加、删除、修改等)直接通过sqlite3_exec()函数执行;
  4. 对于有返回值的语句则:
    首先通过sqlite3_prepare_v2()进行sql语句评估(语法检测),
    然后通过sqlite3_step()依次取出查询结果的每一行数据,对于每行数据都可以通过对应的sqlite3_column_类型()型方法获得对应列的数据,如此反复循环直到遍历完成。当然,最后需要sqlite3_finalize()释放句柄。

在整个操作过程中无需管理数据库连接,对于嵌入式SQLite操作是持久连接(尽管可以通过sqlite3_close()关闭),不需要开发人员自己释放连接。纵观整个操作过程,其实与其他平台的开发没有明显的区别,较为麻烦的就是数据读取,在iOS平台中使用C进行数据读取采用了游标的形式,每次只能读取一行数据,较为麻烦。

示例

下面通过一个简单的例子来体验一下如何在iOS中操作SQLite数据库:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#import "ViewController.h"
#import "sqlite3.h"

#define kDatabaseName @"database.sqlite3"

@interface ViewController ()

@property (copy, nonatomic) NSString *databaseFilePath;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

[self testDatabase];
}


- (void)testDatabase{
//1.获取数据库文件路径
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSLog(@"paths: %@",paths);
NSString *documentsDirectory = [paths objectAtIndex:0];
self.databaseFilePath = [documentsDirectory stringByAppendingPathComponent:kDatabaseName];
//2.打开或创建数据库。sqlite3_open:把一个文件名称传递给他,它会自动检测这个文件是否存在,如果不存在的话,会自动创建相应的文件。
sqlite3 *database;
NSInteger result = sqlite3_open([self.databaseFilePath UTF8String], &database);
if (result == SQLITE_OK) {
//3.创建一个数据库表
const char *createTableSql="CREATE TABLE IF NOT EXISTS t_person (id integer PRIMARY KEY AUTOINCREMENT,name text NOT NULL,age integer NOT NULL);";
char *errmsg = NULL;
sqlite3_exec(database, createTableSql, NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@"创表失败:%s---%s---%d", errmsg,__FILE__,__LINE__);
} else {
NSLog(@"创表成功!");
}
//4.插入数据
char *errmsg2 = NULL;
for (int i=0; i<20; i++) {
NSString *name = [NSString stringWithFormat:@"Person %d",i];
int age = arc4random_uniform(20)+10;
NSString *insertSql = [NSString stringWithFormat:@"INSERT INTO t_person (name,age) VALUES ('%@',%d);",name,age];
sqlite3_exec(database, insertSql.UTF8String, NULL, NULL, &errmsg2);
if (errmsg2) {//如果有错误信息
NSLog(@"插入数据失败--%s",errmsg);
}else {
NSLog(@"插入数据成功");
}
}

//sqlite3_exec() 方法可以执行任何SQL语句,比如创表、更新、插入和删除操作。但是一般不用它执行查询语句,因为它不会返回查询到的数据。

//5.查询
const char *selectSql = "SELECT id,name,age FROM t_person WHERE age<20;";
sqlite3_stmt *stmt = NULL;
//进行查询前的准备工作。
//sqlite3_prepare_v2(sqlite3 *db,const char *zSql,int nByte,sqlite3_stmt **ppStmt,const char **pzTail);第一个参数为数据库的句柄,第二个参数为sql语句,第三个参数为sql的长度(如果设置为-1,则代表系统会自动计算sql语句的长度),第四个参数用来取数据,第五个参数为尾部一般用不上可直接写NULL
if (sqlite3_prepare_v2(database, selectSql, -1, &stmt, NULL)==SQLITE_OK) {
NSLog(@"查询语句没有问题");
//每调用一次sqlite3_step函数,stmt就会指向下一条记录
while (sqlite3_step(stmt) == SQLITE_ROW) {//找到一条记录
//取出数据
//(1)取出第0列字段的值(int类型的值)
int ID = sqlite3_column_int(stmt, 0);
//(2)取出第1列字段的值(text类型的值)
const unsigned char *name = sqlite3_column_text(stmt, 1);
//(3)取出第2列字段的值(int类型的值)
int age = sqlite3_column_int(stmt, 2);
// NSLog(@"%d %s %d",ID,name,age);
printf("%d %s %d\n",ID,name,age);
}
}else {
NSLog(@"查询语句有问题");
}
sqlite3_finalize(stmt);

//6 带占位符插入数据
char *insertSql2 = "insert into t_person(name, age) values(?, ?);";
sqlite3_stmt *stmt2;
if (sqlite3_prepare_v2(database, insertSql2, -1, &stmt2, NULL) == SQLITE_OK) {
sqlite3_bind_text(stmt2, 1, "母鸡", -1, NULL);
sqlite3_bind_int(stmt2, 2, 27);
}

if (sqlite3_step(stmt2) != SQLITE_DONE) {
NSLog(@"带占位符插入数据错误");
}
sqlite3_finalize(stmt2);
}
else{
sqlite3_close(database);
NSAssert(0, @"打开数据库失败!");
}

}

@end

重要函数说明

总结一下几个重要函数:

  • 打开数据库。
1
2
3
4
5
6
7
8
//把一个文件名称传递给他,它会自动检测这个文件是否存在,如果不存在的话,会自动创建相应的文件
int sqlite3_open(

const char *filename, // 数据库的文件路径

sqlite3 **ppDb // 数据库实例

);
  • sqlite3_exec() 可以执行任何SQL语句,比如创表、更新、插入和删除操作。但是一般不用它执行查询语句,因为它不会返回查询到的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//执行任何SQL语句
int sqlite3_exec(

sqlite3*, // 一个打开的数据库实例

const char *sql, // 需要执行的SQL语句

int (*callback)(void*,int,char**,char**), // SQL语句执行完毕后的回调

void *, // 回调函数的第1个参数

char **errmsg // 错误信息

);
  • sqlite3_prepare_v2()进行查询前的准备工作(验证SQL语句)。
1
2
3
4
5
6
7
8
9
10
11
12
13
int sqlite3_prepare_v2(

sqlite3 *db, // 数据库实例

const char *zSql, // 需要检查的SQL语句

int nByte, // SQL语句的最大字节长度(如果设置为-1,则代表系统会自动计算sql语句的长度)

sqlite3_stmt **ppStmt, // sqlite3_stmt实例,用来获得数据库数据

const char **pzTail //一般用不上可直接写NULL

);
  • sqlite3_step() 遍历查询结果。参数为sqlite3_step(sqlite3_stmt*),stmt可以理解为查询出的一行数据。每调用一次sqlite3_step函数,stmt就会指向下一行数据。
  • sqlite3_column_类型() 该方法用于获得对应列的数据。
1
2
3
4
//(1)取出第0列字段的值(int类型的值)
int ID = sqlite3_column_int(stmt, 0);
//(2)取出第1列字段的值(text类型的值)
const unsigned char *name = sqlite3_column_text(stmt, 1);

数据类型分为以下几种:

1
2
3
4
5
6
7
8
9
double sqlite3_column_double(sqlite3_stmt*, int iCol);  // 浮点数据

int sqlite3_column_int(sqlite3_stmt*, int iCol); // 整型数据

sqlite3_int64 sqlite3_column_int64(sqlite3_stmt*, int iCol); // 长整型数据

const void *sqlite3_column_blob(sqlite3_stmt*, int iCol); // 二进制文本数据

const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol); // 字符串数据
  • sqlite3_bind_text():大部分绑定函数都只需要前3个参数
  1. 第1个参数是sqlite3_stmt *类型
  2. 第2个参数指占位符的位置,第一个占位符的位置是1,不是0
  3. 第3个参数指占位符要绑定的值
  4. 第4个参数指在第3个参数中所传递数据的长度,对于C字符串,可以传递-1代替字符串的长度
  5. 第5个参数是一个可选的函数回调,一般用于在语句执行后完成内存清理工作
  • sqlite_finalize():销毁sqlite3_stmt *对象

PreparedStatement方式处理SQL请求

上面代码最后部分提到了“带占位符插入数据”,即PreparedStatement方式处理SQL请求,过程如下:

1、验证语句:sqlite3_prepare_v2

2、 绑定sqlite3_bind_XXX():大部分绑定函数都只需要前3个参数。

  • 第1个参数是sqlite3_stmt *类型
  • 第2个参数指占位符的位置,第一个占位符的位置是1,不是0
  • 第3个参数指占位符要绑定的值
  • 第4个参数指在第3个参数中所传递数据的长度,对于C字符串,可以传递-1代替字符串的长度
  • 第5个参数是一个可选的函数回调,一般用于在语句执行后完成内存清理工作

3、执行过程:

1
int sqlite3_step(sqlite3_stmt*);

可能的返回值:

  • SQLITE_BUSY: 数据库被锁定,需要等待再次尝试直到成功。
  • SQLITE_DONE: 成功执行过程(需要再次执行一遍以恢复数据库状态)
  • SQLITE_ROW: 返回一行结果(使用sqlite3_column_xxx(sqlite3_stmt*,, int iCol)得到每一列的结果。
  • SQLITE_ERROR: 运行错误,过程无法再次调用(错误内容参考sqlite3_errmsg函数返回值)
  • SQLITE_MISUSE: 错误的使用了本函数(一般是过程没有正确的初始化)

4、结束的时候清理statement对象:

1
int sqlite3_finalize(sqlite3_stmt *pStmt);

应该在关闭数据库之前清理过程中占用的资源。

5、重置过程的执行

1
int sqlite3_reset(sqlite3_stmt *pStmt);

过程将回到没有执行之前的状态,绑定的参数不会变化。

其他工具函数

  • 得到结果总共的列数
1
int sqlite3_column_count(sqlite3_stmt *pStmt);

如果过程没有返回值,如update,将返回0

  • 得到当前行中包含的数据个数
1
int sqlite3_data_count(sqlite3_stmt *pStmt);

如果sqlite3_step返回SQLITE_ROW,可以得到列数,否则为零。

  • 得到数据行中某个列的数据的类型
1
int sqlite3_column_type(sqlite3_stmt*, int iCol);

返回值:SQLITE_INTEGER,SQLITE_FLOAT,SQLITE_TEXT,SQLITE_BLOB,SQLITE_NULL

实际开发中,会对这些操作进行封装,这里就不再赘述了,大家可以参考:iOS开发系列—数据存取。当然在实际开发中,我们用的更多的还是FMDB,我会在以后的博客中进行总结。

七、Core Data

Core Data是IOS中的ORM(对象关系映射),类似Java中的Hibernate。

对于从未接触过Core Data的朋友,强烈推荐下斯坦福大学公开课中关于Core Data的讲解。由于Core Data内容比较多,我会在后续博文中再介绍。

参考:

iOS中常用的四种数据持久化方法简介

IOS持久化数据——(保存数据的一系列方法 )

IOS 四种保存数据的方式

iOS开发-文件管理

iOS开发之查找目录

NSBundle介绍

iOS开发备忘录:属性列表文件数据持久化

iOS开发UI篇—ios应用数据存储方式(XML属性列表-plist)

iOS开发UI篇—ios应用数据存储方式(归档)

iOS开发UI篇—ios应用数据存储方式(偏好设置)

iOS中几种数据持久化方案:我要永远地记住你!

iOS开发系列—Objective-C之Foundation框架

iOS持久化

iOS开发系列—数据存取

iOS开发数据库篇—SQLite常用的函数