📜 ⬆️ ⬇️

NSUserDefaults in practice

This text is a translation of the article NSUserDefaults In Practice . The original author is David Smith. The translation was made with the kind permission of the author.

What is NSUserDefaults?


The comment that starts the header file “NSUserDefaults.h” describes the class quite well. I will use this comment to start:
NSUserDefaults are:

1) hierarchical
2) permanent (persistent)
3) interprocess
4) and in some cases distributed
repository type key value. NSUserDefaults are optimized for storing user preferences.

1) Hierarchical:


NSUserDefaults contain the list of data storage locations where they are looking for this data. This list is called a “search list”. The “search list” contains some arbitrary strings, called “suite identifiers” or “domain identifiers”. When a request is received NSUserDefaults will check each item in its search list until they find the one that contains the key from the request, or until they have passed the entire list. The list includes:
')

Note: “current host + current user” settings are not implemented on iOS, watchOS, and tvOS, and “for any user” settings basically do not give anything to applications on these operating systems.

2) Permanent (persistent):


Settings stored in NSUserDefaults are persistent between reloads and application restarts, unless otherwise specified.

3) Inter-Process:


Settings can be available for reading / writing from several processes simultaneously (for example, from an application and its extension).

4) In some cases distributed:


At the moment, there is support only in Shared iPad for students (Apple program www.apple.com/education/it - approx. Transl.).

The data stored in NSUserDefaults can be made distributed ("ubiqitous" - approx. Transl.), I.e. synchronizing between devices via the cloud. Distributed “user settings” are automatically transferred to all devices logged into one iCloud account. When reading settings (via calling methods of the form - * ForKey:), distributed settings are checked before local ones. All operations with distributed settings are asynchronous. Thus, if the download from iCloud is not complete, then the registered settings can be returned, instead of distributed. Distributed settings are set in the application's Confaults Configuration File.

Key-value storage:


NSUserDefaults store property list objects (plist files): NSString, NSData, NSNumber, NSDate, NSArray, and NSDictionary — identified by keys of the NSString type. This is similar to the work of NSMutableDictionary.

Optimized to store user settings:


NSUserDefaults are designed to store relatively small amounts of data that are often requested and rarely modified. Other uses may result in slower work or more memory consumption than more suitable solutions.

In CoreFoundation, the CFPreferences functions containing “App” in their names work with the same search lists as NSUserDefaults. Observing NSUserDefaults using the KVO (Key-Value Observing) mechanism is possible for any key stored in them. When monitoring changes from other processes or devices, using the NSKeyValueObservingOptionPrior does not affect KVO behavior.

Basics NSUserDefaults: 99%


Under normal circumstances, NSUserDefaults are extremely simple.

Reading settings from NSUserDefaults:


If there is a setting that controls some of the code, you simply call the appropriate getter method ( -objectForKey: or one of the wrapping methods for a particular type).
If you find that you need to do something else to get the setting, you should take a step back and weigh it all over again:

  1. Caching values ​​from NSUserDefaults is usually not necessary, since reading is already extremely fast.
  2. Calling -synchronize before reading the value is not necessary in any situations.
  3. Actions in response to a change in value are almost never needed, since the purpose of any settings is to control what the program does, and not to force it to act.
  4. Writing code to handle the “value not set” case is also not generally necessary, since you can register a default value (see Registering default values below).

Saving user settings in NSUserDefaults


Similarly, when a user changes a setting, you simply call -setObject: forKey: (or one of its type-specific wrappers).

If it turns out that you need something else, then again, it probably is not necessary. Almost never call -synchronize after setting a value (see below. Splitting settings between programs ). And users are usually not able to change settings so quickly that “bundling” of any kind would be beneficial for performance. Real disk writing is asynchronous, and NSUserDefaults merge changes into a single write operation automatically.

Default logging


It may seem tempting to write code like:

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

But in the long term, it has a hidden flaw: if you ever want to change the initial value, you will not have a way to distinguish the value set by the user (which he would like to keep) and the initial value set by you (which you would like to change). Also, doing so is kind of slow. The solution is to use the -registerDefaults method :

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

What has many advantages:


The -registerDefaults: method can be called as many times as needed. And all key-value pairs from all the dictionaries transferred to it will be registered. This gives you the opportunity to keep the registration settings next to the code that works with them.

Splitting settings between programs


One tricky moment, which nevertheless often occurs, is the need to share settings between several running processes, for example, between an application and its extension or between two or more applications (on macOS)

In the old (good / bad) times, even before applications were put in sandboxes, everything was very simple: use [[NSUserDefaults alloc] initWithSuiteName:] with the same name in both processes, and these processes will share the same settings. Terminological note: "domain" and "set name" are used interchangeably. Both terms simply mean an arbitrary string that identifies the configuration store.

In the world of sandboxes, in the world of modern macOS and all iOS NSUserDefaults are initially limited to working in the sandbox of your application. If you use -initWithSuiteName:, then you get only a new storage of settings, still indivisible. To make it shared two things are needed:

  1. Create a shared container to hold the settings.
  2. Use the identifier of this container as the set name, which is passed to NSUserDefaults when creating the set (using the -initWithSuiteName: method - comment transl .). I will not go into details now, but here you can find relevant documentation. As soon as you add an application or application extension to a group, the set with the name equal to the group identifier automatically becomes shared.

If one of the processes establishes a shared setting and then notifies the other process to read it, then you may be in one of those very rare situations where a call to the -synchronize method may be useful. This is a blocking method. It guarantees that after returning from it, reading the settings by any other process will return a new one, and not the old value. For applications running on iOS 9.3 and later or on macOS Sierra and later, -synchronize is not needed (or recommended) even in the described situation. Since KVO monitoring of settings now works between processes, the reading process can simply watch the value change directly.

As a result, applications running on these operating systems generally should never invoke -synchronize .

My main recommendation is to share as little of the settings as possible - simply because the code is easier to understand and maintain when the values ​​do not change externally.

Transactions in NSUserDefaults are not implemented, so there is no way to ensure that the results of several changes are readable all at once. Another application may see the results of the first changes before the subsequent changes are completed.

Splitting settings between devices


Distributed (i.e., stored in iCloud) settings are now supported only in Shared iPad mode for training. Therefore, they are outside the scope of this general discussion. Currently, for distributed storage of data outside of training mode, you should not use NSUserDefaults, but NSUbiquitousKeyValueStore. A few nontrivial moments of distributed settings are mentioned in the section Traps and Warnings .

Pitfalls and cautions: StackOverflow sends to this section.


Despite the simplicity that has been paramount, there are plenty of ways to create problems for yourself.

NSUserDefaults have improved significantly over the years of their existence. The list below is relevant for iOS 10 and macOS Sierra, but should be longer for older systems, and is likely to be shorter in future ones.


Advanced NSUserDefaults: you probably don’t need it


A surprise gift bag full of the less-used features of NSUserDefaults. May contain bees.


NSUserDefaults Performance Compromises: Accelerating


In general, the performance of NSUserDefaults is good enough so you don’t have to worry about it. However, there are several things to know about if a problem occurs (please use a profiler, like the Tools tool, to check!)

When you first read any setting, the entire set is loaded into memory. This can take considerable time on slow systems. The implications of this are:

  1. Do not store huge amounts of data in the settings, because they will be loaded all at once
  2. Do not pile mountains of presets, as each set will require its own initial boot.

Even if there are no settings in the domain, overhead is still incurred in detecting this fact. For example, if you have the “Enable debug logs” setting, then it is usually faster and more economical to store it in the standard settings in memory than in a separate “Logging” set.

Reading already loaded settings is extremely fast: about half a microsecond on a 2012 MacBook Pro. Certain things can invalidate the cache and require reloading: if the set is shared with another process, then setting the settings in either process disables the cache in both. In a more typical case of unshared settings, reading the settings after installation will create a small overhead, but not a complete rebuilding of the cache. Conclusions from here:

  1. When possible, avoid separation.
  2. When possible, minimize installations
  3. Always free to read.

Repeatedly installing one key, even at different values, can be much faster than installing many different keys. This allows NSUserDefaults to be fast in cases like saving a resizable real-time window.

Most of the work involved in setting up the setup is asynchronous. But reading can be blocking while asynchronous writing is in progress. The -synchronize call is also blocking. Intermittent set-up and read operations of a large split set are the worst case for performance tuning.

Setting the value in the collection inside the settings will cause the installation of the entire collection (apparently, an emphasis on “the unchanged part of the collection too” - approx. Transl.). Partial write support works only for top-level keys.

Setting the value (ultimately, it is asynchronous and happens later in another process) writes the entire plist to disk, no matter how small the change was. Avoid storing large amounts of data, especially with frequent changes.

The awful horror of the tic-tac-toe program, leading to the sad end of NSUserDefaults

Source: https://habr.com/ru/post/324400/


All Articles