Pull to refresh

Работа с файлами .plist в Cocoa/CocoaTouch

Reading time 6 min
Views 27K
Всем доброго хабрадня!

Сегодня я бы хотел рассказать о некоторых аспектах сохранения настроек и прочих данных программы в 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).
Tags:
Hubs:
+14
Comments 23
Comments Comments 23

Articles