Всем доброго хабрадня!
Сегодня я бы хотел рассказать о некоторых аспектах сохранения настроек и прочих данных программы в OS X и/или iOS. Как обычно, у нас есть несколько вариантов: Core Data, «голый» SQLite, свои бинарные форматы, свои текстовые форматы, NSUserDefaults и, как Вы уже наверняка слышали, файлы типа PLIST, то есть XML Property List.
Вкратце, plist-файлы представляют из себя обычный XML, но с некоторыми оговорками. К примеру, порядок тегов в нём обусловлен некоторыми правилами: они идут парами «ключ-значение», но теги типа «ключ» и теги типа «значение» располагаются на одном уровне. Типичный пример:
Плисты умеют хранить основные типы данных Cocoa: NSString, NSNumber (int, float, BOOL), NSDate, NSArray, NSDictionary и NSData. Этим типам соответствуют следующие теги:
Сегодня я бы хотел рассказать о некоторых аспектах сохранения настроек и прочих данных программы в OS X и/или iOS. Как обычно, у нас есть несколько вариантов: Core Data, «голый» SQLite, свои бинарные форматы, свои текстовые форматы, NSUserDefaults и, как Вы уже наверняка слышали, файлы типа PLIST, то есть XML Property List.
Вкратце, plist-файлы представляют из себя обычный XML, но с некоторыми оговорками. К примеру, порядок тегов в нём обусловлен некоторыми правилами: они идут парами «ключ-значение», но теги типа «ключ» и теги типа «значение» располагаются на одном уровне. Типичный пример:
<key>identifier</key>
<string>j3qq4-h7h2v</string>
Плисты умеют хранить основные типы данных Cocoa: NSString, NSNumber (int, float, BOOL), NSDate, NSArray, NSDictionary и NSData. Этим типам соответствуют следующие теги:
, , , <true/>, <false/>, , , , . Собственно, plist состоит из тегов , за которыми следуют перечисленные теги со значением.
Под катом - описание дополнительных ограничений и, что самое главное, API для работы с такими файлами.
Наверняка Вы уже обратили внимание на возможность хранить в plist'е массивы и словари и у Вас возникли закономерные вопросы: "а как это?", "а если в массиве мои объекты?", "а если в словаре ещё словари?" и подобные им. Если не возникло, значит эту часть статьи можно пропустить без ущерба для понимания.
Дело в том, что массивы и словари при сериализации в плист проходятся рекурсивно, то есть, получается всего лишь ещё один уровень вложенности на каждый массив или словарь внутри другого контейнера. Отсюда и вытекают ограничения на содержимое: только типы, поддающиеся сериализации. То есть, массив вьюшек Вы таким способом не сериализуете, даже не пытайтесь. Но многие свои типы можете: достаточно имплементировать протокол NSCoding и получить NSData из своего объекта с помощью NSKeyedArchiver. А уж NSData и в плисте сохранить легко. Опробовать такой метод сериализации и десериализации своих объектов я оставляю Вам в качестве домашнего задания.
Ещё один интересный момент. Для ускорения чтения и записи плисты часто делают двоичными, переводят в формат bplist (Binary Plist), что снижает их удобочитаемость практически до нуля. Но не расстраиваемся: Xcode умеет открывать и такие плисты, но если Вы хотите всё ж посмотреть на XML в другом редакторе, Вы можете легко переконвертировать бинарный плист в текстовый из консоли: plutil -convert xml1 MyFile.plist
. Кстати, plutil
умеет конвертировать плист ещё и в JSON, это может кому-либо пригодиться, но лично я этим ни разу не пользовался.
Очень часто с плистами разработчик работает посредством NSUserDefaults, пусть даже он об этом зачастую и не знает. Этот класс разработан для работы с глобальными настройками программы, хранимыми в ~/Library/Preferences/com.yourcompany.yourapp.plist (который, кстати, обычно бинарный, то есть, bplist), и переключить его на работу с другим файлом нельзя. Но ведь мы хотим создавать и читать свои собственные плисты, не так ли? Для этого мы будем использовать простой класс NSPropertyListSerialization
, заботливо предоставленный нам разработчиками Cocoa.
Итак, что же умеет этот класс? Для начала, он умеет преобразовывать NSDictionary и NSArray в NSData, содержащий наш plist. И, разумеется, он умеет делать обратные преобразования: из NSData в NSDictionary или NSArray.
Рассмотрим простой пример: создадим словарик с кучей данных (в том числе вложенных) и посмотрим на практике, во что это дело сохранится.
- (IBAction)savePlist:(id)sender
{
NSMutableDictionary *root = [NSMutableDictionary dictionary];
[root setObject:@YES forKey:@"autosave"];
[root setObject:@"hello" forKey:@"greet-text"];
[root setObject:@"4F4@@" forKey:@"identifier"];
NSMutableArray *elements = [NSMutableArray array];
[elements addObject:@"one"];
[elements addObject:@"two"];
[elements addObject:@"thee"];
[root setObject:elements forKey:@"elements"];
NSMutableArray *subs = [NSMutableArray array];
for (NSInteger i = 0; i < 10; i++)
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:[NSString stringWithFormat:@"John %ld", i] forKey:@"name"];
[dict setObject:[NSString stringWithFormat:@"Moscow %ld", i] forKey:@"city"];
[dict setObject:[NSNumber numberWithInteger:i] forKey:@"id"];
[subs addObject:dict];
}
[root setObject:subs forKey:@"subs"];
NSLog(@"saving data:\n%@", root);
NSError *error = nil;
NSData *representation = [NSPropertyListSerialization dataWithPropertyList:root format:NSPropertyListXMLFormat_v1_0 options:0 error:&error];
if (!error)
{
BOOL ok = [representation writeToFile:self.plistFileName atomically:YES];
if (ok)
{
NSLog(@"ok!");
}
else
{
NSLog(@"error writing to file: %@", self.plistFileName);
}
}
else
{
NSLog(@"error: %@", error);
}
}
В результате выполнения этого кода, который слишком простой, что бы его ещё и комментировать, будет плист примерно такого вида:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>autosave</key>
<true/>
<key>elements</key>
<array>
<string>one</string>
<string>two</string>
<string>thee</string>
</array>
<key>greet-text</key>
<string>hello</string>
<key>identifier</key>
<string>4F4@@</string>
<key>subs</key>
<array>
<dict>
<key>city</key>
<string>Moscow 0</string>
<key>id</key>
<integer>0</integer>
<key>name</key>
<string>John 0</string>
</dict>
<dict>
<key>city</key>
<string>Moscow 1</string>
<key>id</key>
<integer>1</integer>
<key>name</key>
<string>John 1</string>
</dict>
<!-- тут ещё много -->
</array>
</dict>
</plist>
Что, просто? Конечно просто! И даже XML достаточно удобочитаемый. А в консоль ещё и свалится текстовое описание нашего словарика:
{
autosave = 1;
elements = (
one,
two,
thee
);
"greet-text" = hello;
identifier = "4F4@@";
subs = (
{
city = "Moscow 0";
id = 0;
name = "John 0";
},
{
city = "Moscow 1";
id = 1;
name = "John 1";
}
);
}
Неплохо.
Теперь будем загружать сохранённый на этом этапе плист:
- (IBAction)loadPlist:(id)sender
{
NSData *plistData = [NSData dataWithContentsOfFile:self.plistFileName];
if (!plistData)
{
NSLog(@"error reading from file: %@", self.plistFileName);
return;
}
NSPropertyListFormat format;
NSError *error = nil;
id plist = [NSPropertyListSerialization propertyListWithData:plistData options:NSPropertyListMutableContainersAndLeaves format:&format error:&error];
if (!error)
{
NSMutableDictionary *root = plist;
NSLog(@"loaded data:\n%@", root);
}
else
{
NSLog(@"error: %@", error);
}
}
И что же мы должны получить? Ну конечно же! Мы в консоли должны увидеть тот же симпатичный JSON-чик, что и при сохранении! Правда, нет гарантий, что он будет именно таким же: порядок следования элементов в NSDictionary не определён. Но все данные должны быть на месте.
Кстати говоря, мы загрузили наши данные в виде "mutable" данных, на что указывает флаг NSPropertyListMutableContainersAndLeaves
. Если бы мы указали NSPropertyListImmutable
, то получили бы не NSMutableDictionary, а обычный NSDictionary, так что тут есть небольшой простор для фантазии и оптимизации.
Что ж, в этом уроке мы немного разобрались с форматом PLIST и научились работать с файлами такого типа с помощью Cocoa. Полный пример можно найти, как всегда, на гитхабе.
Удачного кодинга!
UPD: Как заметил mejedi, бинарный формат плиста иногда может записываться в файл медленней plain-XML формата.
XML пишется «в лоб», а при сохранении в бинарный формат происходит поиск и устранение дублирующих элементов (формат по сути представляет собой поток сущностей с взаимными ссылками, например если у нас два раза строка «hello world» встречается, хранить две копии не обязательно).
Сейчас посмотрел код, чтобы освежить память — на 10.6 все так, как я описал, а на 10.8 устранение дубликатов больше не делается, по-идее должно стать быстрее (релевантная функция называется __CFBinaryPlistWrite).