[Из песочницы] NSUserDefaults на практике

Данный текст представляет собой перевод статьи NSUserDefaults In Practice. Автором оригинала является Дэвид Смит (David Smith). Перевод выполнен с любезного разрешения автора.

Что такое NSUserDefaults?


Комментарий с которого начинается заголовочный файл «NSUserDefaults.h» вполне хорошо описывает класс. Этим комментарием я и воспользуюсь, чтобы начать:
NSUserDefaults являются:

1) иерархическим
2) постоянным (персистентным)
3) межпроцессным
4) и в некоторых случаях распределенным
хранилищем вида ключ-значение. NSUserDefaults оптимизированы для хранения пользовательских настроек.

1) Иерархическое:


NSUserDefaults содержат список мест хранения данных, в котором они эти данные ищут. Этот список называется «список поиска». В «списке поиска» содержатся некоторые произвольные строки, называемые «идентификаторами набора»(suite identifier) или «идентификаторами домена». Когда поступает запрос NSUserDefaults проверяют каждый элемент в своем списке поиска, пока не найдут тот, который содержит ключ из запроса, или пока не пройдут весь список. Список включает:

  • Управляемые («принудительные») настройки, устанавливаемые профилем конфигурации или администратором сети через MCX (Managed Client for OS X — Управляемый Клиент OS X)
  • Аргументы командной строки
  • Настройки для текущего домена в облаке
  • Настройки для текущего домена, текущего пользователя, на текущем хосте
  • Настройки для текущего домена, текущего пользователя, на любом хосте
  • Настройки добавленные вызовом -addSuiteNamed:
  • Настройки глобальные для всех приложений текущего пользователя, на текущем хосте
  • Настройки глобальные для всех приложений текущего пользователя, на любом хосте
  • Настройки для текущего домена, для всех пользователей, на текущем хосте
  • Настройки глобальные для всех приложений всех пользователей, на текущем хосте
  • Настройки зарегистрированные через -registerDefaults:

Замечание: настройки «текущий хост + текущий пользователь» не реализованы на iOS, watchOS, и tvOS, а настройки «для любого пользователя» в основном ничего не дают приложениям на этих операционных системах.

2) Постоянное (персистентное):


Настройки сохраняемые в NSUserDefaults персистентны между перезагрузками и перезапусками приложений, если иное не указано.

3) Межпроцессные:


Настройки могут быть доступны на чтение/запись из нескольких процессов одновременно (например, из приложения и его расширения).

4) В некоторых случаях распределенные:


На данный момент поддержка есть только в режиме Shared iPad для учащихся (программа Apple www.apple.com/education/it — прим. перев.).

Данные сохраненные в NSUserDefaults могут быть сделаны распределенными («ubiqitous» — прим. перев.), т.е. синхронизирующимися между устройствами через облако. Распределенные «пользовательские настройки» автоматически передаются на все устройства залогиненные в один iCloud аккаунт. При чтении настроек (через вызов методов вида -*ForKey:) распределенные настройки проверяются перед локальными. Все операции с распределенными настройками асинхронные. Таким образом, если загрузка из iCloud не завершена, то зарегистрированные настройки могут быть возвращены, вместо распределенных. Распределенные настройки устанавливаются в конфигурационном файле настроек (Defaults Configuration File) приложения.

Хранилище ключ-значение:


NSUserDefaults сохраняют объекты списков свойств (plist-файлов): NSString, NSData, NSNumber, NSDate, NSArray и NSDictionary — идентифицируемые по ключам типа NSString. Это подобно работе NSMutableDictionary.

Оптимизированы для хранения пользовательских настроек:


NSUserDefaults предназначены для хранения относительно небольших объемов данных часто запрашиваемых и редко модифицируемых. Другие способы использования могут привести к медленной работе или большему потреблению памяти, чем более подходящие решения.

В CoreFoundation функции CFPreferences содержащие в названии «App» работают с теми же списками поиска, что и NSUserDefaults. Наблюдение NSUserDefaults с помощью механизма KVO (Key-Value Observing) возможно для любого сохраненного в них ключа. Когда наблюдение ведется за изменениями от других процессов или устройств, использование NSKeyValueObservingOptionPrior не влияет на поведение KVO.

NSUserDefaults основы: 99%


В обычных обстоятельствах NSUserDefaults предельно просты.

Чтение настроек из NSUserDefaults:


Если есть настройка управляющая некоторой частью кода, вы просто вызываете подходящий геттер-метод (-objectForKey: или один из оберточных методов для конкретного типа).
Если обнаружилось, что вам требуется сделать что-то ещё для получения настройки, то следует сделать шаг назад и взвесить всё ещё раз:

  1. Кэширование значений из NSUserDefaults обычно не нужно, поскольку чтение и так предельно быстрое
  2. В вызове -synchronize перед чтением значения нет необходимости ни в каких ситуациях
  3. Действия в ответ на изменение значения почти никогда не нужны, поскольку предназначение любых настроек — контролировать что программа делает, а не заставлять действовать
  4. Написание кода для обработки случая «значение не установлено» также в общем случае не нужно, так как можно зарегистрировать значение по умолчанию (см. ниже Регистрация значения по умолчанию).

Сохранение пользовательских настроек в NSUserDefaults


Аналогично, когда пользователь изменяет некоторую настройку, вы просто вызываете -setObject:forKey: (или одну из его типо-специфичных оберток).

Если обнаружилось, что вам требуется что-то ещё, то, опять же, вероятно, это не нужно. Почти никогда не стоит вызывать -synchronize после установки значения (см. ниже Разделение настроек между программами). И пользователи обычно не способны изменять настройки так быстро, чтобы «пакетирование» любого вида было бы полезно для производительности. Реальная запись на диск асинхронна, и NSUserDefaults производят слияние изменений в одну операцию записи автоматически.

Регистрация значения по умолчанию


Может показаться соблазнительным написать код наподобие:

- (void) applicationDidFinishLaunching:(NSApplication *)app {
	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
	if (![defaults objectForKey:@"Something"]) {
		[defaults setObject:initialValue forKey:@"Something"];
	}
}

Но в долгосрочной перспективе он имеет скрытый изъян: если когда-нибудь возникнет желание изменить начальное значение, у вас не будет способа отличить значение установленное пользователем (которое он хотел бы сохранить) и начальное значение установленное вами (которое хотелось бы изменить). Кроме того, делать так отчасти медленно. Решение — использовать метод -registerDefaults:

[[NSUserDefaults standardUserDefaults] registerDefaults:@{
	@"Something" : initialValue
}];

Что имеет множество преимуществ:

  • Здесь ничего не сохраняется на диск, поэтому никогда не будет путаницы со значением, которое установил пользователь
  • Зарегистрированное значение автоматически переопределяется любым установленным пользователем значением. Т.о. не нужно оборачивать if-ом установку значения, чтобы проверить нужно ли её делать
  • Нет записи на диск: нет замедления в процессе запуска приложения, нет износа диска.

Метод
-registerDefaults: можно вызывать сколько угодно раз. И все пары ключ-значение из всех переданных ему словарей будут зарегистрированы. Это дает возможность держать регистрацию настроек рядом с кодом, который с ними работает.

Разделение настроек между программами


Один сложный момент, который тем не менее часто встречается — это необходимость разделять настройки между несколькими запущенными процессами, например, между приложением и его расширением или между двумя и более приложениями (на macOS)

В старые (добрые/недобрые) времена, ещё до того как приложения стали помещать в «песочницы», всё было очень просто: используй [[NSUserDefaults alloc] initWithSuiteName:] с одинаковым именем в обоих процессах, и эти процессы станут разделять одни настройки. Терминологическое замечание: «домен» и «имя набора» используются как взаимозаменяемые. Оба термина обозначают просто произвольную строку, идентифицирующую хранилище настроек.

В мире «песочниц», в мире современных macOS и всех iOS NSUserDefaults изначально ограничены работой в «песочнице» вашего приложения. Если использовать -initWithSuiteName:, то получаешь всего лишь новое хранилище настроек, всё так же неразделяемое. Чтобы сделать его разделяемым нужны две вещи:

  1. Создать разделяемый контейнер, для размещения настроек
  2. Использовать идентификатор этого контейнера как имя набора, которое передается в NSUserDefaults при создании набора (методом -initWithSuiteName: — прим. перев.). Я не стану сейчас углубляться в детали, но здесь можно найти актуальную документацию. Как только вы добавляете приложение или расширение приложения в группу, то набор с именем равным идентификатору группы автоматически становится разделяемым.

Если один из процессов устанавливает разделяемую настройку, а затем уведомляет другой процесс, чтобы тот прочитал её, то вы, возможно, в одной из тех очень редких ситуаций, когда вызов метода -synchronize может быть полезен. Это блокирующий метод. Он гарантирует, что после возврата из него чтение настройки любым другим процессом вернет новое, а не старое значение. Для приложений, исполняющихся на iOS 9.3 и последующих версий или на macOS Sierra и последующих версий, -synchronize не нужен (или не рекомендован) даже в описанной ситуации. Поскольку KVO-наблюдение за настройками теперь работает между процессами, то читающий процесс может просто наблюдать за изменением значения напрямую.

В результате приложения выполняющиеся на этих операционных системах в общем случае никогда не должны вызывать -synchronize.

Моя основная рекомендация: разделять как можно меньше настроек — просто потому, что код проще понимать и поддерживать, когда значения не изменяются извне.

Транзакции в NSUserDefaults не реализованы, поэтому нет способа гарантировать, что результаты нескольких изменений станут доступны для чтения все разом. Другое приложение может увидеть результаты первых изменений до того как последующие изменения будут завершены.

Разделение настроек между устройствами


Распределенные (т.е. сохраняемые в iCloud) настройки сейчас поддерживаются только в режиме Shared iPad для обучения. Поэтому они вне сферы данного общего обсуждения. В настоящее время для распределенного хранения данных вне учебного режима следует использовать не NSUserDefaults, а NSUbiquitousKeyValueStore. Несколько нетривиальных моментов распределенных настроек упомянуты в разделе Ловушки и предостережения.

Ловушки и предостережения: StackOverflow направляет в этот раздел


Несмотря на поставленную во главу угла простоту, остается масса способов создать себе проблемы.

NSUserDefaults значительно улучшились за годы своего существования. Список ниже актуален для iOS 10 и macOS Sierra, но должен быть более длинным для старых систем, и, скорее всего, станет короче в будущих.

  • Коллекции возвращаемые NSUserDefaults всегда неизменяемые, даже если вы сохраняли изменяемую коллекцию
  • Сохранение изменяемой коллекции в NSUserDefaults и затем изменение её не приведет к сохранению измененного значения
  • Настройки — это не plist-файлы, хранение в plist-файлах это лишь частый случай. Прямое вмешательство в plist-файлы может иметь непредсказуемые последствия. Вместо этого используйте NSUserDefaults, CFPreferences, утилиту «defaults» командной строки. Можно использовать «defaults import» and «defaults export» для конвертации «целиком» между стандартным plist-файлом и настройками
  • Plist-файлы — это не настройки. Используйте «plutil» или NSPropertyList API для работы с произвольными plist-файлами, но не с настройками или NSUserDefaults
  • В NSUserDefaults могут храниться только типы, которые могут храниться plist-ах. Если вы хотите сохранить произвольный объект, то нужно применить NSKeyedArchiver или что-то похожее для того, чтобы сначала получить NSData. Зачастую это означает, что вы пытаетесь сохранить нечто, не являющееся пользовательскими настройками
  • До iOS 9.3 / macOS Sierra, KVO работает только со standardUserDefaults
  • До iOS 9.3 / macOS Sierra, KVO не уведомляет об изменениях произведенных другими приложениями
  • NSUserDefaultsDidChangeNotification не уведомляет об изменениях произведенных другими приложениями
  • Метод -registerDefaults: отрабатывает на каждом экземпляре NSUserDefaults, а не только на том, на котором вы его вызвали
  • Метод +resetStandardUserDefaults не делает ничего особенно полезного
  • «VolatileDomain»-методы тоже ничего особенно полезного не делают
  • Используя метод -setPersistentDomain:forName: сложно избежать проблем, если более одного потока устанавливают настройки. Причина в ситуациях наподобие такой:

    1. Поток 1 вызывает -persistentDomainForName:, получая снапшот текущих настроек
    2. Поток 2 вызывает -setObject:forKey: для ключа «A»
    3. Поток 1 создает копию полученного снапшота и меняет значение для ключа «B», затем вызывает -setPersistentDomain:forName:

    Изменение значения для ключа «A» теряется, поскольку Поток 1 устанавливает весь словарь разом, и в этом словаре нет нового значения для ключа «A».
  • Если вы устанавливаете настройку, и немедленно вызываете exit() или abort() (но не -terminate или подобное), то значение, которое вы установили может быть потеряно. Можно использовать CFPreferencesAppSynchronize(), чтобы сделать выход безопасным.
  • Если вы используете настройки в связке с KVO между процессами или устройствами, то убедитесь, что не устанавливаете настройки в ответ на их изменение. В противном случае может возникнуть «цикл», в котором два или более процессов/устройств постоянно отвечают на изменения производимые друг другом, расходуя батарею и трафик (в случае распределенных настроек)
  • В отличие от обычных настроек, распределенные сохраняются в облаке, и нет гарантии, что они будут доступны. Поэтому чтение распределенных настроек может вернуть зарегистрированное значение вместо настоящего из облака, если загрузка ещё не завершена (что в свою очередь может потребовать неограниченно большого времени). Ваше приложение должно быть к этому готово.

Продвинутые NSUserDefaults: вероятно вам это не нужно


Мешок с подарками-сюрпризами полный реже используемыми возможностями NSUserDefaults. Может содержать пчёл.

  • Методы добавления/удаления набора принимают другой экземпляр NSUserDefaults и эффективно вставляют его в экземпляр, на котором вызываются (документация по -addSuiteNamed: говорит: «вставляет указанное доменное имя в лист поиска получателя» — прим. перев.). Да, — это означает, что можно автоматически обращаться к настройкам, которые вы разделяете с другим процессом. Нет, — вам, вероятно, не стоит так делать потому, что невидимое обращение к межпроцессно-изменяемому состоянию это страшно
  • Методы вида «PersistentDomain» (постоянный домен) были единственным способом работы с настройками другого приложения до появления -initWithSuiteName:. Главное применение оставшееся в современном мире — это вызов -removePersistentDomainForName: для удаления всех настроек в домене
  • Метод -objectIsForcedForKey: позволяет проверить была ли данная настройка переопределена конфигурационным профилем (iOS) или MCX (macOS)
  • Метод -setURL:forKey: делает то, что написано на коробке. Он уникален тем, что это единственный метод NSUserDefaults, позволяющий сохранять не-plist тип. Если хочется сохранить NSURLs, следует использовать -setURL:forKey: вместо -setObject:forKey:
  • Разнообразные типизированные геттеры выполняют неявные конвертации читаемых значений. Например, «1.0» будет прочитано как «1», если вы используете -integerForKey: или как «YES», если вы используете -boolForKey:
  • Аргументы командной строки вашей программы, имеющие форму "-key value" (-ключ значение) переопределяют настройки, что может пригодиться для тестирования. Кстати, можно настроить эти аргументы в схеме проекта в XCode
  • Чтобы записать глобальные настройки, которые читают все приложения можно использовать команду «defaults write -g»

NSUserDefaults компромиссы производительности: ускоряемся


В общем, производительность NSUserDefaults достаточно хороша, чтобы вам не стоило о ней волноваться. Однако, есть несколько моментов, о которых стоит знать, если проблема возникает (пожалуйста, используйте профилировщик, наподобие утилиты «Инструменты», для проверки!)

При первом чтении любой настройки весь набор загружается в память. Это может занять значительное время на медленных системах. Следствия из этого:

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

Даже если в домене нет настроек, всё равно возникают накладные расходы на обнаружение этого факта. Например, если у вас есть настройка «Включить отладочные логи», то обычно быстрее и экономичнее по памяти хранить её в стандартных настройках, чем в отдельном наборе «Логирование».

Чтение уже загруженных настроек происходит чрезвычайно быстро: порядка половины микросекунды на 2012 MacBook Pro. Определенные вещи могут инвалидировать кэш и потребовать повторной загрузки: если набор разделяем с другим процессом, то установка настройки в любом из процессов инвалидирует кэш в обоих. В более типичном случае не разделенных настроек чтение настройки после установки создаст небольшие накладные расходы, но не полную перестройку кэша. Выводы отсюда:

  1. Когда возможно, избегать разделения
  2. Когда возможно, минимизировать установки
  3. Всегда свободно читать.

Многократная установка одного ключа, даже в разные значения, может быть значительно быстрее, чем установка многих различных ключей. Это позволяет NSUserDefaults быть быстрыми в случаях наподобие сохранения изменяемого в реальном времени размера окна.

Большая часть работы, вызываемой установкой настройки, является асинхронной. Но чтение может быть блокирующим, пока выполняется асинхронная запись. Вызов -synchronize тоже блокирующий. Перемежающиеся операции установки и чтения большого разделенного набора — худший случай для производительности настроек.

Установка значения в коллекции внутри настроек вызовет установку всей коллекции (по всей видимости, акцент на «и не изменившейся части коллекции тоже» — прим. перев.). Поддержка «частичной записи» работает только для ключей верхнего уровня.

Установка значения (в конечном счете она асинхронна и случается несколько позднее в другом процессе) запишет весь plist на диск, неважно сколь малым было изменение. Избегайте хранения больших объемов данных, особенно при частых изменениях.

Ужасный ужас программы «крестики-нолики», приводящий к печальному концу NSUserDefaults
03/20/2017 17:17:27
0

комментарии (0)