(译)如何使用NSCoding和NSFileManager来保存你的应用程序数据

免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!

原文链接地址:http://www.raywenderlich.com/1914/how-to-save-your-app-data-with-nscoding-and-nsfilemanager

 

注:本文由sking-tree翻译!

 

教程截图:

 

  在iOS中,有这些办法可以实现数据持久化:

  Plist, SQLite, Core Data 以及 NSCoding。

 

  如果数据量大,或者说数据结构复杂,Core Data 通常是最好的选择。

 

  在这篇教程中,我们通过扩展一个之前的例子:”Scary Bugs” 来让它支持数据持久化。

  例子在这里:How To Create A Simple iPhone App Tutorial Series

 

  在这里,我们会向你介绍如何用NSCoding持久化数据,以及如何用NSFileManager来有效地保存文件。

  如果你没有Scary Bugs的工程,可以从这里直接下载。

 

实现NSCoding

  NSCoding是一个可以由你自行实现的协议,通过扩展你的数据类(data class)来支持encode和decode功能就可以了。它们的任务是把数据写到数据缓存,最后持久保存到磁盘中。

  听上去很复杂,但其实实现NSCoding真的很容易!很多时候我总感觉它很好使。

  下面我们来看看到底有多容易:

// Modify @interface line to include the NSCoding protocol
@interface ScaryBugData : NSObject <NSCoding> {

 然后可以把下面的代码加到实现类.m的最后

复制代码
#pragma mark NSCoding 
#define kTitleKey       @"Title"
#define kRatingKey      @"Rating" 

- (void) encodeWithCoder:(NSCoder *)encoder {  
      [encoder encodeObject:_title forKey:kTitleKey]; 
      [encoder encodeFloat:_rating forKey:kRatingKey];
} 

- (id)initWithCoder:(NSCoder *)decoder {   

     NSString *title = [decoder decodeObjectForKey:kTitleKey];  
     float rating = [decoder decodeFloatForKey:kRatingKey];  
     return [self initWithTitle:title rating:rating];

}
复制代码

 

   完成了!

  我们不过是现实了两个方法: encodeWithCoder, initWithCoder. 分别是负责编码和解码的功能。

  在encodeWithCoder中,我们传入一个NSCoder对象,通过helper 方法把它编码成细小的数据片。

  这些helper方法有: encodeObject,encodeFloat,encodeInt等等。

  在每一次encode的时候需要提供一个key用于以后decode的时候查找。

  通常,我们会对这些类加一个field,减一个field。为了让你的程序更健壮,在你decode一个field的时候,最好判断一下它的值是不是nil或者零,然后给它赋一个合适的默认值。

  推荐一篇关于NSCoding的文章,你值得一读。article by Mike Ash 

在磁盘中保存/读取

  前面所做到是让数据类实现encode和decode。

  但我们还要让它可以在磁盘中存取。

  为了实现这一点,需要为这个数据文件指明一个路径。而从效率的角度上考虑,我们不会马上读取数据文件上的数据 ---- 我们会在第一次实际访问数据的时候读取,通过实现一个“get data ”方法。

  我们需要有这些方法:

  - 修改数据后,把修改存到原文件

  - 删除文件

  - 第一次初始化时保存文件(新建)

  在ScaryBugDoc.h 中做以下修改:

复制代码
// Inside @interface
NSString *_docPath;
 
// After @interface
@property (copy) NSString *docPath;
- (id)init;
- (id)initWithDocPath:(NSString *)docPath;
- (void)saveData;
- (void)deleteDoc;
复制代码

   然后在ScaryBugDoc.m中做以下修改:

  1)处理初始化

复制代码
// At top of file
#import "ScaryBugDatabase.h"
#define kDataKey        @"Data"
#define kDataFile       @"data.plist"
 
// After @implementation
@synthesize docPath = _docPath;
 
// Add to dealloc
[_docPath release];
_docPath = nil;
 
// Add new methods
- (id)init {
    if ((self = [super init])) {        
    }
    return self;
}
 
- (id)initWithDocPath:(NSString *)docPath {
    if ((self = [super init])) {
        _docPath = [docPath copy];
    }
    return self;
}
复制代码

  这里引入了一个目前还没编写的类:ScaryBugDatabase.h 先别管它。

  然后定义了两个常量:用于保存数据的key 和保存的文件名。

  最后,加入了两个init的方法。传统的init没什么特别, initWithDocPath接收了传入的路径参数。

  因为在程序运行时docPath可能是nil的,这意味着文件还没有被保存过。所以在save的时候要新建一个file来保存。

  2)创建文件

 

复制代码
- (BOOL)createDataPath {
 
    if (_docPath == nil) {
        self.docPath = [ScaryBugDatabase nextScaryBugDocPath];
    }
 
    NSError *error;
    BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:_docPath withIntermediateDirectories:YES attributes:nil error:&error];
    if (!success) {
        NSLog(@"Error creating data path: %@", [error localizedDescription]);
    }
    return success;
 
}
复制代码

 

  这里又用到 ScaryBugDatabase 这个helper类,还是先别管它。

  这里的目的就是找出一个未被使用的路径,然后创建这个路径的目录。

  创建成功会返回success,失败意味着路径已存在。

  3)重写读取数据的方法

 

复制代码
- (ScaryBugData *)data {
 
    if (_data != nil) return _data;
 
    NSString *dataPath = [_docPath stringByAppendingPathComponent:kDataFile];
    NSData *codedData = [[[NSData alloc] initWithContentsOfFile:dataPath] autorelease];
    if (codedData == nil) return nil;
 
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:codedData];
    _data = [[unarchiver decodeObjectForKey:kDataKey] retain];    
    [unarchiver finishDecoding];
    [unarchiver release];
 
    return _data;
 
}
复制代码

  -  当data属性被访问时,我们检查它是否已被读到内存中(是的话直接返回_data就可以了)。否则就从disk中读取吧

  -  把路径和文件名连接起来,得到文件的保存位置路径,然后用NSData的initWithContentsOfFile来读取数据。

  -  反序列化数据。从已经读到内存的data中初始化unarchiver,然后用它的decode方法解码内存中的数据。这样做它就知道你的数据缓存中有ScaryBugDoc对象,然后调用这个类的initWithCoder方法来实例化这个数据。

  4)保存修改

 

复制代码
- (void)saveData {
 
    if (_data == nil) return;
 
    [self createDataPath];
 
    NSString *dataPath = [_docPath stringByAppendingPathComponent:kDataFile];
    NSMutableData *data = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];          
    [archiver encodeObject:_data forKey:kDataKey];
    [archiver finishEncoding];
    [data writeToFile:dataPath atomically:YES];
    [archiver release];
    [data release];
 
}
复制代码

  这里跟第三点的逻辑刚好相反。(前面是通过某路径查找数据文件,这里是有了数据把它写到某路径下)

  首先调用前面写的createDataPath获取路径信息,然后通过NSKeyedArchiver把data encode后写到disk。

 

  5)增加删除文件的方法

 

复制代码
- (void)deleteDoc {
 
    NSError *error;
    BOOL success = [[NSFileManager defaultManager] removeItemAtPath:_docPath error:&error];
    if (!success) {
        NSLog(@"Error removing document path: %@", error.localizedDescription);
    }
 
}
复制代码

 

  这里最后一个部分:如果用户在table view中删除了某个记录,我们也要实际在disk中移除相关的文件。

 

  增删改的方法有了,我们还缺少两部分: ScaryBugDatabase 对象以及把他们整合起来。

 

Scary Bug Database

  前面你已经知道一个必须实现在ScaryBugDatabase.h 中的方法:

 

// Add to bottom of file
+ (NSMutableArray *)loadScaryBugDocs;
+ (NSString *)nextScaryBugDocPath;

 

  我们要创建两个静态方法:

  1.  读取所以bug 文档,以NSMutableArray的形式返回

  2.  之前用到的,获取下一个可用路径

 

  下面我们来一点一点地实现:

  1)   写一个获取文档根目录的helper方法:

 

复制代码
// Add to top of file
#import "ScaryBugDoc.h"
 
// After @implementation, add new function
+ (NSString *)getPrivateDocsDir {
 
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    documentsDirectory = [documentsDirectory stringByAppendingPathComponent:@"Private Documents"];
 
    NSError *error;
    [[NSFileManager defaultManager] createDirectoryAtPath:documentsDirectory withIntermediateDirectories:YES attributes:nil error:&error];   
 
    return documentsDirectory;
 
}
复制代码

   一个保存你的app数据的最常用的位置,就是“Documents”,获取它的具体值,可以把

  NSDocumentDirectory 传入到NSSearchPathForDirectoriesInDomains 中。

  然而,我不打算把数据保存到这里。因为在后面的教程中,我还会把这个app的功能扩展到支持io4的文件分享功能。

  这个分享的功能会把document目录下的所有东西展示给用户,但在后面的内容中你会明白我们并不想用户看到目录下的内容。

  在Apple官方的规范Storing Private Data中提到,推荐保存的位置是library的子目录。

  而我也是这样做的。

  所以,path的值会是 /Library/Private Documents。如果不存在的话,就创建。

  2)读取所有文档的Helper方法

 

复制代码
+ (NSMutableArray *)loadScaryBugDocs {
 
    // Get private docs dir
    NSString *documentsDirectory = [ScaryBugDatabase getPrivateDocsDir];
    NSLog(@"Loading bugs from %@", documentsDirectory);
 
    // Get contents of documents directory
    NSError *error;
    NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:documentsDirectory error:&error];
    if (files == nil) {
        NSLog(@"Error reading contents of documents directory: %@", [error localizedDescription]);
        return nil;
    }
 
    // Create ScaryBugDoc for each file
    NSMutableArray *retval = [NSMutableArray arrayWithCapacity:files.count];
    for (NSString *file in files) {
        if ([file.pathExtension compare:@"scarybug" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
            NSString *fullPath = [documentsDirectory stringByAppendingPathComponent:file];
            ScaryBugDoc *doc = [[[ScaryBugDoc alloc] initWithDocPath:fullPath] autorelease];
            [retval addObject:doc];
        }
    }
 
    return retval;
 
}
复制代码

  - 这里首先获取了文档目录,用contentsOfDirectoryAtPath来获取目录下的所有文档。

  - 过滤文件:要求文件以scarybug为后缀

  - 找到以后拼接出文档的完整路径,并创建出文档对象的实例。

  3)获取下一个有效文档路径的helper方法:

 

复制代码
+ (NSString *)nextScaryBugDocPath {
 
    // Get private docs dir
    NSString *documentsDirectory = [ScaryBugDatabase getPrivateDocsDir];
 
    // Get contents of documents directory
    NSError *error;
    NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:documentsDirectory error:&error];
    if (files == nil) {
        NSLog(@"Error reading contents of documents directory: %@", [error localizedDescription]);
        return nil;
    }
 
    // Search for an available name
int maxNumber =0;
    for (NSString *file in files) {
        if ([file.pathExtension compare:@"scarybug" options:NSCaseInsensitiveSearch] == NSOrderedSame) {            
            NSString *fileName = [file stringByDeletingPathExtension];
            maxNumber = MAX(maxNumber, fileName.intValue);
        }
    }
 
    // Get available name
    NSString *availableName = [NSString stringWithFormat:@"%d.scarybug", maxNumber+1];
    return [documentsDirectory stringByAppendingPathComponent:availableName];
 
}
复制代码

   跟前面类似了,遍历整个目录,找出“#.scarybug”格式的文件,得到已用的最大号码,最后把最大号码+1.

集成测试吧!

  这里就简单了,打开ScaryBugsAppDelegate.m,修改一下:

复制代码
// Add to top of file
#import "ScaryBugDatabase.h"
 
// Comment out the code to load the sample ScaryBugDoc data in the beginning of application:didFinishLaunchingWithOptions, and replace it with the following:
NSMutableArray *loadedBugs = [ScaryBugDatabase loadScaryBugDocs];
RootViewController *rootController = (RootViewController *) [navigationController.viewControllers objectAtIndex:0];
rootController.bugs = loadedBugs;
复制代码

  把保存的地方改一下:

// In titleFieldValueChanged
[_bugDoc saveData];
 
// In rateView:ratingDidChange
[_bugDoc saveData];

 

  因为我们的文档还是比较小的,每次修改都保存在性能上还是没问题的。

  但如果你的文档稍大一些,你可能就要周期性地在后台自动保存一下,或者在用户关掉app和app进入后台的时候。

  最后,在RootViewController.m中,处理删除的修改:

 

// In tableView:commitEditingStyle:forRowAtIndexPath, before removing the object from the bugs array:
ScaryBugDoc *doc = [_bugs objectAtIndex:indexPath.row];
[doc deleteDoc];

  好了,编译运行。

Loading bugs from /Users/rwenderlich/Library/Application Support/
   iPhone Simulator/4.0.2/Applications/
   D13C7304-25FB-4EDC-B23D-62A084AD90B4/Library/Private Documents

  你应该能在console中看到这些信息。

  如果你用Finder打开,你会看到理所当然的空目录。

  而在你用app创建了一个bug以后,你就可以看到一个新的Private Documents目录:

 

  还有,你如果好奇地打开plist看看:

  如你所见,我们采用了NSCoding + NSKeyedArchiver, 的方式实现,数据被保存到一个plist中,这是一个“半可读”的格式。这在我们debug的时候很方便。

  关闭app,一定是关闭,不是home键退出。

  再次打开,你能够看到从你的目录下存取的第一个bug了!

  但你也会发现,图像并没有保存到。(因为的确还没有)

保存/读取图像

   接下来我们要把大图很小图保存到disk。

  怎么做呢?

  考虑到前面的做法,你可能会想用NSCoding的方式,把图片转成NSData然后encodeObject:forKey:。

  然后调用decodeObjectForKey / UIImage initWithData 来读取。

  但这通常不是最好的办法。

 

  因为可以的话,应该尽量避免把文件拆散保存。

  如果我们把图片保存成property,在app启动的时候所有图片也会读到内存中。

  这意味着你的启动需要更长时间和更大的内存空间。

  首先,增加一个保存图像的方法到ScaryBugDoc.h:

 

// After @interface
- (void)saveImages;

  在caryBugDoc.m中实现:

复制代码
// Add to top of file
#define kThumbImageFile @"thumbImage.jpg"
#define kFullImageFile  @"fullImage.jpg"
 
// Add new functions
- (UIImage *)thumbImage {
 
    if (_thumbImage != nil) return _thumbImage;
 
    NSString *thumbImagePath = [_docPath stringByAppendingPathComponent:kThumbImageFile];
    return [UIImage imageWithContentsOfFile:thumbImagePath];
 
}
 
- (UIImage *)fullImage {
 
    if (_fullImage != nil) return _fullImage;
 
    NSString *fullImagePath = [_docPath stringByAppendingPathComponent:kFullImageFile];
    return [UIImage imageWithContentsOfFile:fullImagePath];
 
}
复制代码

  1. 检查图片是否已读到内存中

  2. 不是的话,从disk中读出图片

  3.  我们没有在实例变量中缓存需要读取的图片 (译:指retain)。以防用户在detail view中把所有大图都读一次后,阻塞内存。取而代之的,我们将要更频繁地读取图片。(译:因为是autorelease)

  4.  如果频繁读取成为问题,那就把实例变量retain。然后在 low memory的情况下再清除缓存。

 

  这里是保存图片的方法实现:

 

复制代码
- (void)saveImages {
 
    if (_thumbImage == nil || _fullImage == nil) return;
 
    [self createDataPath];
 
    NSString *thumbImagePath = [_docPath stringByAppendingPathComponent:kThumbImageFile];
    NSData *thumbImageData = UIImagePNGRepresentation(_thumbImage);
    [thumbImageData writeToFile:thumbImagePath atomically:YES];
 
    NSString *fullImagePath = [_docPath stringByAppendingPathComponent:kFullImageFile];
    NSData *fullImageData = UIImagePNGRepresentation(_fullImage);
    [fullImageData writeToFile:fullImagePath atomically:YES];
 
    self.thumbImage = nil;
    self.fullImage = nil;
 
}
复制代码

  这里在把图片写到disk以后,把变量赋值为nil。(原因刚刚说过了)

  然后在合适的地方调用保存图片的方法:

 

// In imagePickerController:didFinishPickingMediaWithInfo, after _imageView.image = fullImage:
[_bugDoc saveImages];

 编译运行,再来一次创建。

  你可以看到图片被保存下来了:

(译)如何使用NSCoding和NSFileManager来保存你的应用程序数据_第1张图片

 

  (译)如何使用NSCoding和NSFileManager来保存你的应用程序数据_第2张图片

何去何从?

  这里是这篇教程的源代码:sample project。

你可能感兴趣的:(File,文档,interface,disk,attributes,bugs)