iOS中的永久存储,也就是在关机重新启动设备,或者关闭应用时,不会丢失数据。在实际开发应用时,往往需要持久存储数据的,这样用户才能在对应用进行操作后,再次启动能看到自己更改的结果与痕迹。ios开发中,我们需要数据持久化这一种技术,也需要不断在实际开发的工作与学习中完善数据持久化这一开发技术。
本文将介绍4种数据持久化的方法:
也可以使用 传统的C语言I/O调用(比如:fopen() )的读取与写入数据 ,可以使用Cocoa的底层文件管理工具 ,只不过这两种方法都需要开发者写入很多代码,本文不作介绍,如果需要的话,读者可以上网找一下。
在介绍4种持久化存储方式前,我们需要先介绍3个有关的文件夹:
如何获取?
1、获取Documents目录
由于iOS中应用的数据存储是沙盒机制,因此读取和写入文件,我们需要调用C函数 “ NSSearchPathForDirectoriesInDomains()” 来查找各种目录,(这个C函数可以基于Mac OS X平台的Cocoa共享)
如检索Documents目录路径的代码:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *pathDirectory = [paths objectAtIndex:0];
第一个常量NSDocumentDirectory表示我们正在查找目录的路径,第二个常量NSUserDomainMask表明我们希望将搜索限制在应用的沙盒内;(在Mac OS X中,此常量表示我们希望该函数查看用户的主目录,因此才会有这个命名;)
返回的是一个数据paths,为什么位于索引0就是我们需要的Documents目录?因为每一个应用只有一个Documents目录,因此只有一个目录符合这个条件; 接下来,我们可以为刚才检索到的目录pathDirectory的结尾加一个字符串来创建一个文件名,如下:
NSString *filename = [pathDirectory stringByAppendingPathComponent:@"data.txt"]; //注意是stringByAppendingPathComponent,不要拼错。
这个时候我们得到的filename字符串就可以进行创建、读取、写入文件了。
2、获取tmp目录:
可以用 NSTemporaryDirectory ()的Foundation函数返回一个字符串,该字符串包含到应用临时目录的完整路径。 同上,在结尾附上文件名就可以创建指向该目录下的文件路径了。
NSString *tmpPath = NSTemporaryDirectory(); NSString *temFile = [tmpPath stringByAppendingPathComponent:@"tempFile.txt"];
-------------------------------------
下面介绍数据持久化方法的具体实现:
一、属性列表
在【1、bundle的运用】中,我们使用了属性列表来指定应用的默认设置与相应的数据存储,并且方便使用Xcode或者Property List Editor应用手动编辑它们,只要字典或者数据包含特定可序列化对象,就可以NSDictionary和NSArray实例写入属性列表或者从属性列表创建相应的对象;
什么是序列化对象?
序列化对象(Serialized objects),是指可以被转换为字节流以便于存储到文件中或者通过网络进行传输的对象;
虽然说任何对象都可以被序列化,但是只有某些特定的对象才能放置到某个集合类(例如:NSArray、 NSMutableArray、 NSDictionary、 NSData等 )中,并使用该集合类的方法在属性列表存储中使用,其他的对象也可以使用归档的方法进行存储(在对象的归档、解档我们会进行详细介绍)。
那我们开始构建第一个使用属性列表存储数据的简单应用:
具体的功能效果如【图1】,可以让用户在4个文本框中输入数据,应用退出时会把这些字段保存到属性列表中,并在下次启动时重现加载恢复上次的数据;
【图1 效果图】
在Xcode中,使用Single View Application模板创建一个新项目,命名为persistence1 。
在“Main.storyboard”中拖入4个标签、4个文本框控件,拖动并对齐标签与文本框,并依次修改标签文本如【图1】,“ ViewController.h ”中添加一个装载4个文本框的数组“ lineFields ” :
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (strong,nonatomic)IBOutletCollection(UITextField)NSArray *lineFields; @end
打开辅助编辑器,通过control键将4个文本框连接到 lineFields 这个数组,确保连接顺序为从顶部到底部!
在项目导航面板中,点击" ViewController.m " ,将以下代码添加到 @implementation 与 @end 的中间,这个方法在后面会一直调用:
//获取属性列表路径中数据文件的完整路径 dataFilepath //需要加载和保存数据的代码都可以调用该方法. -(NSString *)dataFilepath { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *pathDirectory = [paths objectAtIndex:0]; return [pathDirectory stringByAppendingPathComponent:@"data.txt"]; }
接下来,在viewDidload中添加代码,并添加相应的响应器方法:
- (void)viewDidLoad { [super viewDidLoad]; NSString *filePath = [self dataFilepath]; //判断是否存在属性列表文件 if([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { //存在,则把数据赋值给文本框 NSArray *ar= [[NSArray alloc]initWithContentsOfFile:filePath]; for(int i =0;i<4;i++) { UITextField *textField = self.lineFields[i]; textField.text = ar[i]; } } //如果应用进入后台: UIApplication *app = [UIApplication sharedApplication]; [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(applicationWillResignActiveNotification:) name:UIApplicationWillResignActiveNotification object:app]; } //应用进入后台时执行: -(void)applicationWillResignActiveNotification:(NSNotification *)notification { NSString *pathFile = [self dataFilepath]; //我们不是用迭代数组的形式,而是用了便捷的方法,使用NSarrry类中的valueForKey方法,把lineFields中包含@“text”值的数组赋值给array. NSArray *array = [self.lineFields valueForKey:@"text"]; //把字符串数组写入文件。 [array writeToFile:pathFile atomically:YES]; }
这段代码的意思是,首先检查完整路径下的数据文件是否存在,不存在的话就不加载了;
若存在,则把数组中的对象复制到4个文本框中,根据刚才我们创建数组“lineFields”的时候,与文本框的连接顺序,就可以把数据赋值给文本框了。
然后在应用终止或者进入后台之前进行数据的保存处理,所以我们使用通知中心,订阅了名为 “ UIApplicationWillResignActiveNotification ” 的通知,并在后面实现了“ applicationWillResignActiveNotification ”这个方法。
当用户按下手机的“Home键”,或者其他事件发生(比如来电)导致应用进入后台的情况,便调用此方法,把字符串数组写入我们创建的属性列表文件里面。
好了,我们已经完成了 GUI 界面的基础设计以及代码的编程了,接下来,按下“command+R”运行它;
如果没有其他问题的话,我们可以分别键入4个文本框,然后点击Home键(也就是command+shift+H)、双击Home键(按住command+shift,双击H),或者在Xcode中终止应用退出模拟器(相当于手机重启),以验证数据在应用得到永久保存了。
总结:属性列表的序列化很实用,也相对比较简单,但是也会有点限制,就是只能将一小部分对象保存在属性列表中,接下来我们介绍下强大的归档解档对象的数据储存方法;
就像我们前面属性列表的介绍,归档(archiving)也是指另一种形式的序列化。但强大的一点是,它是任何对象都可以实现的更常规的储存数据类型;
在进行归档、解档的开发中,我们需要一起实现的,还有NSCoding和NSCopying协议,需要说明的是,标量(如int或float)以及大多数Foundation和Cocoa Touch类都遵循NSCoding协议(有例外,如UIImage不遵循),因此大多数类,还是比较容易实现归档操作的;
NSCoding协议声明了2个方法:一个是将对象编码到归档中,另一个是对归档的解码来恢复我们之前归档的对象,使用方法与NSUserDefaults相似也可以用KVC对对象和原生数据类型(如int和float)进行编码和解码。
NSCopying协议用于允许复制对象,使得使用数据模型对象时具备较大的灵活性;
归档:创建一个NSKeyedArchiver实例,用于将对象归档到一个NSMutableData实例中, 此时 NSMutableData 包含编码的数据, 再使用键/码对需要的对象进行归档,最后告知完成,写入文件系统;
解档:也与归档对象步骤类似,创建一个NSData实例用于装载数据,并创建一个NSKeyedUnarchiver实例,对数据解码,然后使用先前用的键进行读取对象,最后告知程序解档完成;
这样说有点干,囧~ 还是上 代码 吧:
在Xcode中,使用Single View Application模板创建一个新项目,命名为persistence2 ,没错,还是跟属性列表一样的应用模板。
但是需要创建一个新文件,按command+N,或者从File菜单中依次选择New->New File。出现新建文件向导后,选择Cocoa Touch,然后选择Objective-C class,单击Next,将类命名为 “ linePesist ”,并在“Subclass of”一栏中选择NSObject,单击Next,再单击Create。该类做为我们的数据模型,并且将用于存储属性列表应用的字典中的数据。
单击“ linePesist.h ”,修改代码如下:
#import <Foundation/Foundation.h> //遵循NSCoding、NSCopying协议 @interface linePesist : NSObject<NSCoding,NSCopying> @property (nonatomic,copy)NSArray *array; @end
这是一个拥有数组类型的简单数据模型,数组可以用于我们放置文本框的数据字段。
接下来,我们进行“ linePesist.m ”的编辑:
#import "linePesist.h" #define CodeStr @"CodeStr" //用于归档解档的时候用的键名 @implementation linePesist /* 通过遵循NSCoding和NSCoping中的方法,创建可归档的数据对象。 */ #pragma mark -- Coding //编码 -(void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.array forKey:CodeStr]; } //解码 -(id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if(self) { self.array = [aDecoder decodeObjectForKey:CodeStr]; } return self; } #pragma mark -- Coping -(id)copyWithZone:(NSZone *)zone { linePesist *copy = [[[self class]allocWithZone:zone] init]; NSMutableArray *muAr = [[NSMutableArray alloc]init]; for(id line in self.array) { [muAr addObject:[line copyWithZone:zone]]; } copy.array = muAr; return copy; } @end
用预定义的“CodeStr”做为编码解码的键,存储4个文本框的字符串,然后用同样的“CodeStr”键进行解码,将4个字符串复制到copyWithZone创建的linePesist对象中;
创建可归档的数据对象之后,我们便可以使用此来进行持久化的存储。点击“ViewController.m”编辑界面,并进行以下除划掉的部分的代码编辑。
#import "ViewController.h" #import "linePesist.h" //导入数据模型类 #define CodeString @"CodeString" @implementation ViewController -(NSString *)dataFile { NSArray *ar = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES); NSString *fielpath = [ar objectAtIndex:0]; return [pathDirectory stringByAppendingPathComponent:@"data.txt"]; return [fielpath stringByAppendingPathComponent:@"data.archive"]; //改修后缀名,以免与属性列表创建的文件重复,而加载成旧的的文件。 不用查字典了。。archive表归档 } - (void)viewDidLoad { [super viewDidLoad]; NSString *filepath = [self dataFile]; NSLog(@"%@",filepath); if([[NSFileManager defaultManager]fileExistsAtPath:filepath]) { NSArray *ar= [[NSArray alloc]initWithContentsOfFile:filePath]; for(int i =0;i<4;i++) { UITextField *textField = self.lineFields[i]; textField.text = ar[i]; } //创建2个实例 NSData *data = [[NSData alloc]initWithContentsOfFile:filepath]; NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]initForReadingWithData:data]; //把已归档的对象读取。赋值给linepesist linePesist *linepesist =[unarchiver decodeObjectForKey:CodeString]; [unarchiver finishDecoding]; //完成解档 for(int i = 0;i<4;i++) { //把解档的数据分别赋值给文本框 UITextField *textField = self.fourLines[i]; textField.text = linepesist.array[i]; //记得是.text } } UIApplication *app = [UIApplication sharedApplication]; [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(applicationWillResignActiveNotification:) name:UIApplicationWillResignActiveNotification object:app]; } // 应用回到后台,数据归档、写入文件中 -(void)applicationWillResignActiveNotification:(NSNotification*)notfication { NSArray *array = [self.lineFields valueForKey:@"text"]; [array writeToFile:pathFile atomically:YES]; NSString *pathField = [self dataFile]; linePesist *linepesit = [[linePesist alloc]init]; linepesit.array = [self.fourLines valueForKey:@"text"]; //创建2个实例 NSMutableData *data = [[NSMutableData alloc]init]; NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]initForWritingWithMutableData:data]; //使用键/值编码对希望包含在归档中的对象进行归档。 [archiver encodeObject:linepesit forKey:CodeString]; [archiver finishEncoding]; //与属性列表一样,需要在最后写入文件,因为属性列表与归档都是一种序列化,最后仍需要写入文件。 [data writeToFile:pathField atomically:YES]; } @end
除了存储数据的方式不一样, GUI 界面与上一个版本的一致。 运行这个版本的persistence应用。效果应该也与我们运行属性列表时的一样。
好了,属性列表、归档解档对象的存储方法我们介绍到这里,读者可以对比下有什么不同,呃...最明显的应该是归档的代码量多些,但是相应的,归档会具有非常好的伸缩性,至少从代码上面看,是这样的。