啊左
代码手工艺人

         iOS中的永久存储,也就是在关机重新启动设备,或者关闭应用时,不会丢失数据。在实际开发应用时,往往需要持久存储数据的,这样用户才能在对应用进行操作后,再次启动能看到自己更改的结果与痕迹。iOS开发中,我们需要数据持久化这一种技术,也需要不断在实际开发的工作与学习中完善数据持久化这一开发技术。

【本次开发环境: Xcode:7.2     iOS Simulator:iphone6S plus   By:啊左】    

 (本节2个项目demo的下载:属性列表Demo对象的归档解档Demo

 

本文将介绍4种数据持久化的方法:

1、属性列表

2、对象的归档、解档

3、数据库 SQLite3 的运用

4、Core Data 的运用

当然,iOS开发中,持久化数据的方法不局限于以上这4种方法,还可以使用啊左的博客:ios开发--应用设置及用户默认设置【1、bundle的运用】这篇博客介绍的应用设置的存储方法;

也可以使用传统的C语言I/O调用(比如:fopen() )的读取与写入数据,可以使用Cocoa的底层文件管理工具,只不过这两种方法都需要开发者写入很多代码,本文不作介绍,如果需要的话,读者可以上网找一下。

 

在介绍4种持久化存储方式前,我们需要先介绍3个有关的文件夹,以及沙盒机制:

  • Documents:应用会将数据存储在这个文件夹里,但是基于NSUserDefaults 的首选项设置除外;
  • Library:基于NSUserDefaults的首选项设置存储在 Library/Preferences 文件夹中,且Library下面有Preferences和Caches目录;
  • tmp:供应用存储临时文件,当iOS设备进行同步操作时,iTunes并不会备份这个文件夹的文件,但是在不需要这些文件的时候,应用需要删除tmp中的这些文件,以免占用文件系统空间;

什么是沙盒机制

  我们手中的iphone/ipad设备上包含着闪存(flash memory),它的功能和一个硬盘功能等价。当设备断电后数据依然能够被保存下来,应用程序可以把数据文件保存到山村上,并且读取它们。但是,需要注意的是,我们所开发的应用程序是无法访问整个闪存的,因为闪存上面会专门有一部分给我们,这一部分就是属于我们开发的整个应用程序的沙盒(sandbox)了。iOS系统下,每个应用都只能看到自己的沙盒,这就防止对其他应用程序的数据文件进行读写活动。就像我们的应用程序也能够看见一些系统拥有的高级别目录,但是却无法进行任何的写入操作;

那么,如何获取属于自己的目录?

1、获取Documents目录

由于iOS中应用的数据存储是沙盒机制,因此读取和写入文件,我们需要调用C函数 “NSSearchPathForDirectoriesInDomains()”来查找各种目录,(这个C函数可以基于Mac OS X平台的Cocoa共享)

如检索Documents目录路径的代码:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *pathDirectory = [paths objectAtIndex:0];
//或者NSString *pathDirectory = [paths lastObject];

第一个常量NSDocumentDirectory表示正在查找沙盒Document目录的路径(如果参数为NSCachesDirectory则表示沙盒Cache目录),第二个常量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不遵循),因此大多数类,还是比较容易实现归档操作的;

1、遵循NSCoding协议、NSCopying协议

NSCoding协议声明了2个方法:一个是将对象编码到归档中,另一个是对归档的解码来恢复我们之前归档的对象,使用方法与NSUserDefaults相似也可以用KVC对对象和原生数据类型(如int和float)进行编码和解码。

NSCopying协议用于允许复制对象,使得使用数据模型对象时具备较大的灵活性;

2、归档、解档

归档:创建一个NSKeyedArchiver实例,用于将对象归档到一个NSMutableData实例中,此时NSMutableData包含编码的数据,再使用键/码对需要的对象进行归档,最后告知完成,写入文件系统;

解档:也与归档对象步骤类似,创建一个NSData实例用于装载数据,并创建一个NSKeyedUnarchiver实例,对数据解码,然后使用先前用的键进行读取对象,最后告知程序解档完成;

 

这样说有点干,囧~  还是上代码吧:

a."linePesist"类的创建

在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对象中;

b.“ViewController”类实现

创建可归档的数据对象之后,我们便可以使用此来进行持久化的存储。点击“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应用。效果应该也与我们运行属性列表时的一样。

 

好了,属性列表、归档解档对象的存储方法我们介绍到这里,读者可以对比下有什么不同,呃...最明显的应该是归档的代码量多些,但是相应的,归档会具有非常好的伸缩性,至少从代码上面看,是这样的。

在下一节,我们将介绍另外2种方法:数据库 SQLite3 的运用、Core Data 的运用

posted on 2016-01-19 15:48  啊左  阅读(6322)  评论(5编辑  收藏  举报