• 数据存储之归档解档 NSKeyedArchiver NSKeyedUnarchiver


    在构建应用程序时,有一个重要的问题是如何在每次启动之间持久化数据,以便重现最后一次关闭应用前的状态。在iOS和OS X上,苹果提供了三种选择:Core Data、属性列表(Property List)和带键值的编码(NSKeyedArchiver)。当涉及到建模、查询、遍历、持久化等复杂的对象图时,Core Data无可替代。但并非所有应用程序都需要查询数据、处理复杂对象图,有时候使用NSKeyedArchiver更为简单。

    1. 使用NSKeyedArchiver

    如果要将各种类型的对象存储到文件中,而不仅仅是字符串、数组、字典类型,利用NSKeyedArchiver类创建带健(keyed)的档案来完成将非常灵活。

    在带健的档案中,会为每个归档对象提供一个名称,即健(key)。根据这个key可以从归档中检索该对象。这样,就可以按照任意顺序将对象写入归档并进行检索。另外,如果向类中添加了新的实例变量或删除了实例变量,程序也可以进行处理。

    NSKeyedArchiver存储在硬盘上的数据是二进制格式:

     
    KeyedArchiverBinaryData.png

    你可以通过文本编辑器打开二进制文件,但一般来说没有必要。二进制文件是为计算机而设计,比纯文本文件占用磁盘空间小,并且加载速度也更快。例如,Interface Builder通常以二进制格式存储NIB文件。

    下面我们结合代码来学习归档与解档:

    创建Single View Application模板的demo,demo名称为KeyedArchiver。在storyboard中添加四个UILabel、四个UITextField和两个UIButton。布局如下:

     
    KeyedArchiverStoryboard.png

    当点击Archive按钮时,把NameAge对应的文本框内容归档到/Library/Application Support内的文件夹。当点击Unarchiver按钮时,把刚创建归档程序读入执行程序中,并对应的显示到下面的两个文本框中。

    拖拽文本框IBOutlet属性到ViewController.m接口部分,拖转两个UIButton的IBAction到实现部分,分别命名为archiver:unarchiver:。完成后如下:

    @interface ViewController ()
    @property (weak, nonatomic) IBOutlet UITextField *nameArchiver;
    @property (weak, nonatomic) IBOutlet UITextField *ageArchiver;
    @property (weak, nonatomic) IBOutlet UITextField *nameUnarchiver;
    @property (weak, nonatomic) IBOutlet UITextField *ageUnarchiver;
    
    @end
    
    - (IBAction)archiver:(UIButton *)sender {
        
    }
    
    - (IBAction)unarchiver:(UIButton *)sender {
        
    }
    

    在声明部分添加一个NSString类型的documentsPath对象,并使用懒加载初始化。该对象为沙盒中DocumentsApplication Support目录。这样只需要获取一次路径就可以重复使用,有助于提高性能。

    @interface ViewController ()
    
    ...
    @property (strong, nonatomic) NSString *documentsPath;
    
    @end
    
    - (NSString *)documentsPath {
        if (!_documentsPath) {
            NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
            if (paths.count > 0) {
                _documentsPath = paths.firstObject;
                
                //  如果目录不存在,则创建该目录。
                if (! [[NSFileManager defaultManager] fileExistsAtPath:_documentsPath]) {
                    NSError *error;
                    // 创建该目录
                    if(! [[NSFileManager defaultManager] createDirectoryAtPath:_documentsPath withIntermediateDirectories:YES attributes:nil error:&error])
                    {
                        NSLog(@"Failed to create directory. error: %@",error);
                    }
                }
            }
        }
        return _documentsPath;
    }
    

    在初始化documentsPath时,使用NSSearchPathForDirectoriesInDomain()方法获取Library/Application Support/目录,如果目录不存在,则创建该目录。

    对于NSStringNSArrayNSDictionaryNSSetNSDateNSNumberNSData之类的基本Objective-C类对象,都可以直接使用NSKeyedArchiver归档和NSKeyedUnarchiver读取归档文件。

    更新archiver:方法,当点击Archiver按钮时对nameArchiverageArchiver中的文本进行归档。

    - (IBAction)archiver:(UIButton *)sender {
        // A 使用archiveRootObject: toFile: 方法归档
        // 1.修改当前目录为self.documentsPath
        NSFileManager *sharedFM = [NSFileManager defaultManager];
        [sharedFM changeCurrentDirectoryPath:self.documentsPath];
    
        // 2.归档
        if (![NSKeyedArchiver archiveRootObject:self.nameArchiver.text toFile:@"nameArchiver"]) {
            NSLog(@"Failed to archive nameArchiver");
        }
        if (![NSKeyedArchiver archiveRootObject:self.ageArchiver.text toFile:@"ageArchiver"]) {
            NSLog(@"Failed to archive ageArchiver");
        }
    }
    

    上述代码分步说明如下:

    1. 使用NSFileManager修改当前工作目录为self.documentsPath
    2. 使用archiveRootObject: toFile:方法将文本框中的文本进行归档,该方法返回值为BOOL类型,归档成功返回YES,归档失败返回NO。这里的toFile:参数@"nameArchiver"@"ageArchiver"均为相对路径,相对于1中设定的当前路径。

    这篇文章会多次用到文件系统和NSFileManager,如果你还不熟悉,可以查看我的另一篇文章:使用NSFileManager管理文件系统

    再更新unarchiver:方法,当点击Unarchiver按钮时读取归档文件,并对应地显示到nameUnarchiverageUnarchiver中。

    - (IBAction)unarchiver:(UIButton *)sender {
        // A 使用unarchiveObjectWithFile: 读取归档
        // 1.获取归档路径
        NSString *nameArchiver = [self.documentsPath stringByAppendingPathComponent:@"nameArchiver"];
        NSString *ageArchiver = [self.documentsPath stringByAppendingPathComponent:@"ageArchiver"];
        
        // 2.读取归档,并将其显示在对应文本框。
        self.nameUnarchiver.text = [NSKeyedUnarchiver unarchiveObjectWithFile:nameArchiver];
        self.ageUnarchiver.text = [NSKeyedUnarchiver unarchiveObjectWithFile:ageArchiver];
    }
    

    上述代码的分步说明如下:

    1. 使用stringByAppendingPathComponent:方法获取归档路径,这里也可以使用归档方法中设置当前路径的方法,两种方法效果一样。
    2. 使用NSKeyedUnarchiver类的unarchiverObjectWithFile:方法从路径中读取归档,并赋值给对应文本框。

    运行demo,在上面两个UITextField中输入文本,点击Archiver按钮即可把文本框中的文本归档。点击Unarchiver按钮即可读取归档数据,并将其显示到对应文本框。

     
    KeyedArchiverA.gif

    2. 编码方法和解码方法

    前面我们说过,对于NSStringNSArray等基本的Objective-C类对象,都可以直接使用NSKeyedArchiverNSKeyedUnarchiver进行归档和解档。而对于其他类型的对象,则必须告知系统如何编码你的对象,以及如何解码。这时你的类必须遵守NSCoding协议,该协议只有两个必须实现的方法encodeWithCoder:initWithCoder:

    为遵守面向对象的设计原则,被编码、解码的对象负责对其实例变量进行编码和解码。编码器通过调用encodeWithCoder:initWithCoder:方法指导对象编码、解码其实例。encodeWithCoder:指导对象编码其实例变量至该方法参数中的编码器,该方法可能会被调用多次;initWithCoder:指导对象用参数中的数据初始化自身,它会替换任何其他初始化方法,并且每个对象仅发送一次。必须遵守NSCoding协议、实现这两个方法,该类才可以对其实例进行编码、解码。

    继续上面的demo,添加一个模版为Cocoa Touch Class,类名为Person,父类为NSObject的文件。

    Person.h中添加以下属性和方法:

    @interface Person : NSObject
    
    @property (strong, nonatomic) NSString *name;
    @property (assign, nonatomic) NSInteger age;
    
    - (void)setName:(NSString *)name age:(NSInteger)age;
    
    @end
    

    Person.m实现setName: age:方法。

    - (void)setName:(NSString *)name age:(NSInteger)age {
        self.name = name;
        self.age = age;
    }
    

    下面是解码方法和编码方法。

    // 1.编码方法
    - (void)encodeWithCoder:(NSCoder *)aCoder {
        [aCoder encodeObject:self.name forKey:@"PersonName"];
        [aCoder encodeInteger:self.age forKey:@"PersonAge"];
    }
    
    // 2.解码方法
    - (instancetype)initWithCoder:(NSCoder *)aDecoder {
        if (self = [super init]) {
            self.name = [aDecoder decodeObjectForKey:@"PersonName"];
            self.age = [aDecoder decodeIntegerForKey:@"PersonAge"];
        }
        return self;
    }
    

    上述代码的分步说明如下:

    1. 该程序向编码方法encodeWithCoder:传入一个NSCoder对象作为参数。由于Person类直接继承自NSObject,所以无需担心编码继承的实例变量。如果的确担心,并且知道类的父类符合NSCoding协议,那么在编码方法开始处添加[super encodeWithCoder: encoder],确保继承的实例变量也被编码。另外,不同类型对象使用不同编码方法。如果编码NSString类型对象,使用encodeObject: forKey:方法,如果编码NSInteger类型对象,使用encodeInteger: forKey:方法。这里的键名是任意的,只要跟解码时的一致即可。为防止子类和父类使用相同键而导致冲突,可以像这里定义的一样,制定键名时将类名放在键名前加以区分。
    2. 解码过程与编码刚好相反。传递给initWithCoder:的参数也是NSCoder对象,不用担心这个参数,只要记住它是想要从归档中提取的对象即可。如果担心解码继承的实例变量,且该父类遵守NSCoding协议,可以用self = [super initWithCoder: decoder];开始解码方法。只要键与编码时相同就可以解码。

    进入ViewController.m方法,导入Person.h,并在接头部分添加以下属性:

    @interface ViewController ()
    
    ...
    @property (strong, nonatomic) Person *person;
    
    @end
    

    最后记得在viewDidLoad方法中初始化该方法。

    注释掉archiver:方法内代码,并添加以下代码,以便归档Person类。

    - (IBAction)archiver:(UIButton *)sender {
        // B 使用initForWritingWithData: 归档。
        // 1.把当前文本框内文本传送给person。
        [self.person setName:self.nameArchiver.text age:[self.ageArchiver.text integerValue]];
        
        // 2.使用initForWritingWithMutableData: 方法归档内容至mutableData。
        NSMutableData *mutableData = [NSMutableData data];
        NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:mutableData];
        [archiver encodeObject:self.person forKey:@"person"];
        [archiver finishEncoding];
        
        // 3.把归档写入Library/Application Support/Data目录。
        NSString *filePath = [self.documentsPath stringByAppendingPathComponent:@"Data"];
        if (![mutableData writeToFile:filePath atomically:YES]) {
            NSLog(@"Failed to write file to filePath");
        }
    }
    

    上述代码的分布说明如下:

    1. 把当前文本框内文本传送给person。记得在`viewDidLoad
    2. 先创建一个空缓冲区,其大小将随着程序执行的需要而扩展。通过intiForWritingWithMutableData:方法以指定归档数据的存储空间为mutableData,现在可以向archiver对象发送编码消息,以便归档对象,这里可以归档多个对象。所有对象都归档后必须向archiver发送finishEncoding消息。在此之后,就不能编码其他对象了。此时,你预留的mutableData区域包含归档对象。
    3. 使用writeToFile: atomically:方法把归档后的对象写入文件,该方法返回值为BOOL类型,写入成功时返回YES;操作失败时返回NOatomically:参数为YES表示希望首先将文件写入到临时备份中,且一旦成功,将把该备份重命名为指定目录名。这是一种安全措施,可以避免文件在操作过程中因系统崩溃而致使原文件、新文件均损坏。如果参数为NO,则会直接在指定目录写入文件。

    同样,注释掉unarchiver:中原来代码,并添加以下代码读取归档。

    - (IBAction)unarchiver:(UIButton *)sender {
        ...
        // B 使用initForReadingWithData: 读取归档。
        // 1.从Library/Application Support/Data目录获取归档文件。
        NSString *filePath = [self.documentsPath stringByAppendingPathComponent:@"Data"];
        NSData *data = [NSData dataWithContentsOfFile:filePath];
        
        // 2.使用initForReadingWithData: 读取归档。
        NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
        self.person = [unarchiver decodeObjectForKey:@"person"];
        [unarchiver finishDecoding];
        
        // 3.把读取到的内容显示到对应文本框。
        self.nameUnarchiver.text = self.person.name;
        self.ageUnarchiver.text = [@(self.person.age) stringValue];
    }
    

    读取归档时,首先通过dataWithContentsOfFile:方法获取归档文件,之后使用initForReadingWithData:读取归档,在解码结束时,一定要向unarchiver发送finishDecoding消息结束解码。最后将读取到的归档内容显示到对应文本框。

    运行app,可以像之前一样对文本框内容进行归档、解档。

    如果想要了解属性列表及通过代码练习,可以查看这篇文章:使用偏好设置、属性列表、归档解档保存数据、恢复数据

    你也可以尝试注释掉Person.m中的编码方法和解码方法再次运行demo,点击按钮时会在控制台看到出错消息。

    你也可以通过添加观察者,在应用程序进入后台时(通知为UIApplicationDidEnterBackgroundNotifiaction)归档文件,这样即使app被终止,数据也不会丢失。你可以自行完成,如果遇到问题,可以通过文章底部网址获取源码查看。

    3. 使用归档程序复制对象

    可以使用归档功能实现深复制,可以将对象归档到一个缓冲区,然后把它从缓冲区解归档,这样就实现了深复制。如下所示:

        NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"one"], [NSMutableString stringWithString:@"two"], nil];
        
        NSData *data = [NSKeyedArchiver archivedDataWithRootObject:mutableArray];
        NSMutableArray *mutableArray2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    

    如果你对其是否进行了深复制有疑惑,可以通过修改其中一个数组的元素,查看另一个数组内元素是否改变来验证。

    深入了解深复制、浅复制,查看深复制、浅复制、copy、mutableCopy这篇文章。

    4. 三种归档方法的区别

    在这篇文章中使用了archiveRootObject: toFile:initForWritingWithMutableData:archivedDataWithRootObject:三种类型归档方法,它们区别如下:

    1. archiveRootObject: toFile:不能决定如何处理归档的数据,直接被写入了文件。
    2. initForWritingWithMutableData:归档的数据可以通过网络分发,除此之外还可以把多个对象归档到一个缓冲区。
    3. archivedDataWithRootObject:这种方法归档的数据可以通过网络分发,非常灵活。

    总之,只是方便与灵活的区别。

    Demo名称:KeyedArchiver
    源码地址:https://github.com/pro648/BasicDemos-iOS

    参考资料:

    1. NSCoding / NSKeyed​Archiver
    2. Differences with archiveRootObject:toFile: and writeToFile:
    3. Saving Application Data



    作者:pro648
    来源:简书

  • 相关阅读:
    Nginx+Tomcat 集群部署
    Android5.0新特性——CardView 使用
    Android-SQLite版本问题
    Android UI ListView的使用
    Android
    Android四大组件之Activity一(组件的概念、Intent、监听)
    JAVA内部类使用
    Android 第一个程序 及 环境搭配
    Android-AsyncTask异步任务(获取手机联系人)
    Android-Application
  • 原文地址:https://www.cnblogs.com/jiuyi/p/10569550.html
Copyright © 2020-2023  润新知