UserDefaults Limitations and Alternatives
I recently led a discussion at Learn Swift Winnipeg on Swift 4's Codable
protocols with a section on how to store Codable
types in UserDefaults
.
During the discussion one member asked "How far can we go with UserDefaults?" while another said "You shouldn't be storing ... in UserDefaults".
I use UserDefaults
all the time but these comments made me realize I don't know how it actually works. I don't know what it's limitations are, why I should or shouldn't use it in a particular way, and how it compares to using FileManager
to store data in flat files. So I decided to find out!
What is UserDefaults?
Apple's documentation says:
An interface to the user’s defaults database, where you store key-value pairs persistently across launches of your app.
Hmm, interesting use of the term database. I don't think of UserDefaults
as a database per se. So I looked it up in the dictionary:
Database: A structured set of data held in a computer, especially one that is accessible in various ways.
Ok, that makes sense. The docs continue with:
... allows an app to customize its behavior to match a user’s preferences ... commonly used to determine an app’s default state at startup or the way it acts by default.
Pay attention to the phrasing "allows an app to" and "commonly used to". They are suggesting a few things we can do with UserDefaults
but stop short of saying what we can't do with UserDefaults
. In fact I could not find anywhere in this document explicitly "permitted" or "prohibited" uses of UserDefaults
. We'll need to dig more:
... UserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value.
The cache referenced here is an in-memory cache meaning anything stored in UserDefaults
is loaded into memory on app launch and remains in memory while the app is running. This affects device performance to a greater or lesser degree depending on how much data is stored.
When you set a default value, it’s changed synchronously within your process, and asynchronously to persistent storage.
This makes UserDefaults
super fast because we're only ever interacting with the in-memory cache, while the much slower persist-to-disk operations happen asynchronously.
... stored locally on a single device, and persisted for backup and restore.
Red alert! "backup and restore" means storing large amounts of data in UserDefaults
will slow down backups and restores, take up more storage space in iTunes or iCloud, and use more network bandwidth for users backing up to iCloud. Devices restored from backup will include anything previously stored. This may make sense for language selection but probably doesn't for a content cache which can be re-downloaded from a server.
Where is UserDefaults stored?
The standard UserDefaults
instance is stored as a property list in the app container's Preferences folder:
/AppData/Library/Preferences/<bundle-identifier>.plist
That's it. It's just a file on disk. Want to see for yourself?
- Open an app project which saves data to
UserDefaults
in Xcode. - Run the app on a physical device ensuring you trigger the save-to-UserDefaults behaviour.
- Select Window -> Devices and Simulators from Xcode's file menu.
- Select app from the Installed Apps section.
- Select the gear icon at bottom of list.
- Select "Download Container" and save somewhere easily accessible like Downloads.
- In Finder, navigate to and "right click" the saved
.xcappdata
file to open the context menu. - Select "Show Package Contents".
- Navigate to
/AppData/Library/Preferences/
Congratulations! You should be looking at the plist file, something like this.
How much data can be stored in UserDefaults?
I couldn't find any documentation on this but it looks like there is no hard limit other than the device's available storage. However in practice the most I could store on an iPhone 5s was around 100 MB before the app was shut down due to excessive memory pressure. This is because of that in-memory cache mentioned above.
Soroush Khanlou's Approach to Persistence
In Fatal Error episode 26. Persistence Soroush describes his basic approach to persisting data in iOS apps:
Only use the bare minimum for what you need.
He describes the idea of starting with UserDefaults
when you need to store around 20 items, then moving to flat files when you need to store several thousand items, and finally moving to a more advanced persistence option like Realm or CoreData when you need to store more.
I really like Soroush's opinion on this but I'd also like metrics to base my decisions on. I'd like to know how far we can push UserDefaults
and how that compares to working with flat files.
Pushing the limits of UserDefaults
I created a sample project available on GitHub called PersistencePerformanceFun. It measures the time and disk space required to encode/store and retrieve/decode a variable number of simple Swift struct
instances into UserDefaults
or into flat files using FileManager
.
Here it is... not much to look at is it?
Here's how it works:
- In Xcode launch the app on a physical device and display the Memory Report.
- In the app select a persistence method from the segmented control at the top.
- Enter the number of records to be stored into the text field.
- Tap Store to synchronously generate, encode, and store that number of records via the selected persistence method.
- Tap Retrieve to synchronously retrieve and decode however many records are stored via the selected persistence method.
- Select a different persistence method and try again.
Keep an eye on the Memory Report in Xcode. With UserDefaults
selected and a large number of records, maybe 100_000, you can clearly see the in-memory cache in action.
We start with approximately 10 MB of memory being used after launch. Tapping Store causes a rapid climb to 32.4 MB while 100_000 records are created and stored in UserDefaults
. Once completed the memory usage can only drop approximately halfway back to 19.7 MB because a copy of all those records is still in UserDefaults
's in-memory cache.
Let's compare that with flat files via FileManager
:
Again we start with approximately 10 MB of memory used after launch. Tapping Store causes a rapid climb to 30.5 MB. However once complete the memory usage drops all the way back to 10 MB because there is no in-memory cache hanging on to anything.
So that's memory usage. Now let's focus on the storage/retrieval time:
While we can clearly see the speed benefit of UserDefaults
's in-memory cache in the Store
and Retrieve
steps, it's a pretty trivial amount of time considering the total time of the operation. The difference will also be considerably smaller when storing less data, like maybe 20 records.
Backup and Restore
The Apple documentation states that UserDefaults
is included in "backup and restore". Does the same apply to our flat files? Can we choose what gets backed up and restored?
In order to answer these questions we need to understand how the filesystem in iOS works. Here are the guidelines provided by Apple:
- iOS Data Storage Guidelines (high level)
- FileSystemProgrammingGuide (low level)
The documentation can be a little hard to read so here is a summary:
Documents Directory:
Only documents and other data that is user-generated, or that cannot otherwise be recreated by your application, should be stored in the <Application_Home>/Documents/ directory and will be automatically backed up by iCloud.
Caches Directory:
Data that can be downloaded again or regenerated should be stored in the <Application_Home>/Library/Caches/ directory. Examples of files you should put in the Caches directory include database cache files and downloadable content, such as that used by magazine, newspaper, and map applications. Generally speaking, the application does not require cache data to operate properly, but it can use cache data to improve performance.Note that the system may delete the Caches directory to free up disk space, although never while an app is running, so your app must be able to re-create or download these files as needed.
Application Support Directory:
Put app-created support files in the <Application_Home>/Library/Application support/ directory. In general, this directory includes files that the app uses to run but that should remain hidden from the user. This directory can also include data files, configuration files, templates and modified versions of resources loaded from the app bundle. Files in Application Support/ are backed up by default.
Temporary Directory:
Data that is used only temporarily should be stored in the <Application_Home>/tmp/ directory. Although these files are not backed up to iCloud, remember to delete those files when you are done with them so that they do not continue to consume space on the user’s device.
Additional Notes:
Remember that files in Documents/ and Application Support/ are backed up by default. You can exclude files from the backup by calling
-[NSURL setResourceValue:forKey:error:]
using theNSURLIsExcludedFromBackupKey
key. Any file that can be re-created or downloaded must be excluded from the backup. This is particularly important for large media files. If your application downloads video or audio files, make sure they are not included in the backup.
In summary:
UserDefaults
will always be included in backups.- Our flat files will be included in backups if we put them in a backed up directory...
- ... unless we explicity mark them as excluded from backup.
Conclusion
History is full of occasions where everyone "just knew" a particular thing that seemed obvious but turned out to be completely wrong anyhow. The world is flat, heavier-than-air flying machines are impossible, etc. This however is not one of those occasions.
Understanding the memory, speed, storage, and backup implications of UserDefaults
I've come to the same conclusion that most of us "just knew" from the beginning:
UserDefaults
is best used for persisting very small user preference type data which needs to be restored from backup.- Anything else should be stored in the appropriate directory depending on how long the data is needed and whether or not it can be regenerated or re-downloaded.
Oh well, I'm glad to have gone through the process and learned exactly why this common knowledge is still valid. 😀