Сохранение состояния объектов

В этот раз я расскажу о том, как сохранять состояние объекта. Этот процесс еще называется сериализация. Это не рассказ про Core Data, это более простой механизм, который позволяет сохранять состояние объектов и иерархий объектов без особых проблем.
Это будет небольшой подготовкой к следующей теме – сохранение и восстановление состояния приложения в iOS между перезапусками.

Итак, есть задача – сохранить и восстановить состояние некоего объекта Objective-C. Для этого у нас есть следующие компоненты:

  • Протокол NSCoding
  • Класс NSCoder
  • Классы NSKeyedArchiver и NSKeyedUnarchiver


Протокол NSCoding
Для того, чтобы объект можно было сохранять, он должен реализовывать протокол NSCoding. Сам протокол определяет два обязательных метода.

- (void)encodeWithCoder:(NSCoder *)encoder;
- (id)initWithCoder:(NSCoder *)decoder;

Первый из этих методов вызывается, когда объект должен быть сохранен (при сериализации), второй – при восстановлении объекта (при десериализации).
Перед тем, как начать писать реализацию этих методов, нужно посмотреть, что же делает класс NSCoder.

Класс NSCoder
Класс NSCoder содержит методы для сохранения и восстановления различных типов значений.

- (void)encodeObject:(id)objv forKey:(NSString *)key;
- (id)decodeObjectForKey:(NSString *)key;

// also some primitive types
- (void)encodeInt:(int)intv forKey:(NSString *)key;
- (void)encodeDouble:(double)realv forKey:(NSString *)key;
- (int)decodeIntForKey:(NSString *)key;
- (double)decodeDoubleForKey:(NSString *)key;
// ... many more

Общая идея здесь такова – вы должны сохранить значения всех важных для класса свойств, переменных, а при восстановлении – установить их обратно. При сохранении вы используйте строковые ключи (при восстановлении вы должны использовать тот же ключ, что и при сохранении; ключы должны быть уникальны в пределах вашего объекта).

Объект NSCoder, который передается в качестве параметра методов протокола NSCoding, уже создан и готов сохранять/восстанавливать значения.

Объекты, которые NSCoder может сохранять, также должны реализовывать протокол NSCoding (и у этих объектов будут вызваны соответствующие методы). Многие стандартные классы уже реализуют NSCoding, например, NSString, NSDate, NSArray, NSDictionary и другие.
Нужно отметить, что коллекции (например, NSArray) могут содержать различные типы объектов. При сохранении коллекции, все элементы этой коллекции также должны поддерживать протокол NSCoding, иначе – произойдет ошибка во время выполнения.

Итак, приступим к реализации методов протокола NSCoding. Для примера возьмем простой класс с парой свойств.

@interface PersonData : NSObject <NSCoding>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

@end

#define kNameKey @"name"
#define kAgeKey  @"age"

@implementation PersonData 

- (void)encodeWithCoder:(NSCoder *)encoder
{
    [encoder encodeObject:self.name forKey:kNameKey];
    [encoder encodeInt:self.age forKey:kAgeKey]; 
}

- (id)initWithCoder:(NSCoder *)decoder
{
    self = [super init];
    if (self)
    {
        _name = [decoder decodeObjectForKey:kNameKey];
        _age = [decoder decodeIntForKey:kAgeKey];
    }
    return self;
}

@end

Вот такая реализация нашего класса подготовила его к сохранению и восстановлению.

Есть один важный момент, на который нужно обратить внимание. Класс NSObject не следует протоколу NSCoding! Значит, методы -encodeWithCoder: и -initWithCoder: были впервые в этой иерархии реализованы в этом классе. В случае, если мы наследуемся от класса, который уже следует протоколу NSCoding, мы должны вызывать родительскую реализацию методов.

@interface EmployeeData : PersonData

@property (nonatomic, assign) float salary;

@end

#define kSalaryKey @"salary"

@implementation EmployeeData 

- (void)encodeWithCoder:(NSCoder *)encoder
{
    [super encodeWithCoder:encoder];
    [encoder encodeFloat:self.salary forKey:kSalaryKey]; 
}

- (id)initWithCoder:(NSCoder *)decoder
{
    self = [super initWithCoder:decoder];
    if (self)
    {
        _salary = [decoder decodeFloatForKey:kSalaryKey];
    }
    return self;
}

@end

В классе-наследнике мы реализуем только сохранение свойств, добавленных в этом классе. Сохранение остальных уже реализовано в базовом классе, и мы просто используем эту реализацию.

Теперь, когда класс полностью готов, мы можем использовать стандартный механизм сохранения и восстановления объектов.
Классы NSKeyedArchiver и NSKeyedUnarchiver
С помощью методов класса NSKeyedArchiver можно сохранить объект в виде NSData или напрямую в файл:

+ (NSData *)archivedDataWithRootObject:(id)rootObject;
+ (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path;

А с помощью методов класса NSKeyedUnarchiver можно создать объект:

+ (id)unarchiveObjectWithData:(NSData *)data;
+ (id)unarchiveObjectWithFile:(NSString *)path;

Я бы предложил использовать только методы, работающие с NSData. Объекты NSData можно сохранять в файл атомарно, что позволит избежать проблем, если во время сохранения объекта произойдет ошибка и файл будет сохранен частично.

EmployeeData * employee = ...;

NSData * archivedEmployeeData = [NSKeyedArchiver archivedDataWithRootObject:employee];

EmployeeData * unarchivedEmployee = [NSKeyedUnarchiver unarchiveObjectWithData:archivedEmployeeData];

Кстати, необъектные данные тоже можно сохранять, поскольку класс NSValue также следует протоколу NSCoding.

Документация Apple поможет по-лучше узнать про протокол NSCoding, класс NSCoder, классы NSKeyedArchiver и NSKeyedUnarchiver.

В следующий раз мы рассмотрим одну из “фишек” iOS 6 – сохранение состояния приложения между перезапусками с помощью схожей техники.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s