02. Summary of Local Data Persistence Solutions

It will take about 5 minutes to finish reading this article.

Essentially, no matter which caching method is used, it will eventually be stored on the disk in the form of a file, but the upper layer performs some kind of “encapsulation” or “abstraction”, so it is still classified. Currently, iOS local persistent Cache has the following forms:

1. UserDefaults
2. File Cache
3. Keychain
4. Core Data
5. Database (SQLite)

Note: What is discussed here is only the persistent cache. If we simply discuss the cache, strictly speaking, memory caches such as NSCache, NSURLCache, etc. should also be taken into consideration. Below we will introduce these caching mechanisms from the aspects of principles, sample code, precautions, etc.

1. UserDefaults

UserDefaults is a common lightweight caching mechanism in iOS. It can be used to store application configuration information, user preferences, temporary cache data, etc.

Principle description:

UserDefaults uses plist files for storage, which saves cached data in the file in the form of key-value. This file is stored in the Library/Preferences directory in the application sandbox and is automatically managed by the system.

Sample Code:

UserDefaults is a singleton object and we can access it through its shared instance. Here is a sample code that uses UserDefaults to store and read:

1
2
3
4
5
6
7
// Storing data
UserDefaults.standard.set("hello", forKey: "greeting")

// read data
let greeting = UserDefaults.standard.string(forKey: "greeting")
print(greeting)

Notice:

UserDefaults can only store basic data types and some object types in the Foundation framework, such as NSString, NSNumber, NSArray, NSDictionary, etc., and does not support storing custom object types.

Since the stored data of UserDefaults is saved through plist files, for data that needs to be written frequently, it is best to use a more efficient storage method, such as Core Data, SQLite, etc.

When using UserDefaults to store sensitive data, encryption is required to ensure data security.

Summarize:

UserDefaults is a common lightweight caching mechanism in iOS. It uses plist files for storage and supports the storage of basic data types and some object types in the Foundation framework. It is easy to use and simple, but you need to pay attention to the limitations of stored data types and data security. question.

2. File Cache

The file caching mechanism is also a common caching method.

Principle Description:

It is a method of permanently saving an application’s data in the local file system so that the data can be read quickly the next time the application is launched. This caching mechanism works by saving data to the application’s sandbox directory and reading that data the next time the application starts.

Sample Code:

Using the file persistence caching mechanism requires the use of the file system API in iOS, such as NSFileManager and NSFileHandle classes. These classes provide basic operations on the file system, such as creating, deleting, moving, and reading and writing files.

The following is a sample code using the file persistence caching mechanism:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// Get the application's sandbox directory
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!

//Set the data to be saved
let data = "Hello, world!".data(using: .utf8)

//Save data to file
let fileURL = documentsDirectory.appendingPathComponent("cache.txt")
do {
try data?.write(to: fileURL)
} catch {
print("Error writing to file: \(error)")
}

//Read data from file
do {
let cachedData = try Data(contentsOf: fileURL)
let cachedString = String(data: cachedData, encoding: .utf8)
print("Cached string: \(cachedString)")
} catch {
print("Error reading from file: \(error)")
}

Notice:

When using the file persistence cache mechanism, you need to pay attention to the following points:

  1. You should try to avoid saving large amounts of data to the local file system, as this takes up storage space on the device.

  2. For sensitive data, encryption algorithms should be used to protect the security of the data.

  3. Cache files should be cleared regularly to avoid excessive cache files causing insufficient storage space on the device.

  4. Care should be taken to handle errors when reading and writing files to avoid program crashes or data loss.

In addition, there is a special but commonly used method of file caching, which is KeyedArchiver.

KeyedArchiver

Used to serialize objects into binary data and write them to files for caching. KeyedArchiver provides a convenient way for developers to serialize custom objects into binary data and save it to the file system for later reading from the file.

Here’s how to use KeyedArchiver:

  1. Implement NSCoding protocol

First, the NSCoding protocol needs to be implemented in the custom object so that KeyedArchiver can serialize the object into binary data. The protocol includes two methods, encode and init(coder:), which are used to serialize objects into binary data and deserialize binary data into objects respectively.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

class Person: NSObject, NSCoding {
var name: String
var age: Int

init(name: String, age: Int) {
self.name = name
self.age = age
}

func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(age, forKey: "age")
}

required init?(coder: NSCoder) {
name = coder.decodeObject(forKey: "name") as? String ?? ""
age = coder.decodeInteger(forKey: "age")
}
}

  1. Cache objects

Next, use KeyedArchiver to serialize the custom object into binary data and write it to a file for caching.

1
2
3
4
5
6
7
8
9
10
let person = Person(name: "Tom", age: 20)

// Get file path
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let filePath = path + "/person.archive"

// Serialize the object into binary data and write it to the file
let data = NSKeyedArchiver.archivedData(withRootObject: person)
try? data.write(to: URL(fileURLWithPath: filePath))

  1. Read object from cache
1
2
3
4
5
6
//Read binary data from file and deserialize into object
let data = try? Data(contentsOf: URL(fileURLWithPath: filePath))
let cachedPerson = NSKeyedUnarchiver.unarchiveObject(with: data!) as? Person

print("Name: \(cachedPerson?.name ?? "")")
print("Age: \(cachedPerson?.age ?? 0)")

You need to pay attention to the following points when using KeyedArchiver:

(1) Implement the encode and init(coder:) methods in the NSCoding protocol for custom objects so that KeyedArchiver can serialize objects into binary data.

(2) KeyedArchiver will serialize objects into binary data, so you need to pay attention to memory consumption issues.

(3) For some sensitive data, data security needs to be considered. Encryption algorithms can be used to protect data security.

(4) Pay attention to errors when handling file reading and writing to avoid program crashes or data loss.

3. Keychain

Keychain is a persistent caching mechanism for iOS and part of the security framework in iOS, providing an API for storing and retrieving data in a secure manner.

Principle Description:

Data in Keychain is stored in a protected system area, which is completely isolated and data cannot be shared between different applications. Each application has its own Keychain, which can only access its own data and not other applications’ data. This isolation ensures the security of Keychain storage.

It guarantees secure storage and retrieval of sensitive data between application launch and device restart. It stores data such as passwords, encryption keys, and other credentials in an encrypted manner so that it cannot be accessed by other applications and systems.

For every application, KeyChain has two access areas, private area and public area. The private area is a Sandbox. Any data stored by this program is not visible to other programs, and other applications cannot access the data in this area. Using the Keychain API, data can be stored through key-value pairs and identified using service names and account names. This ensures that the data is associated with the application and can only be used by applications with the corresponding service name and account name. access.

If you want to place the stored content in the public area so that multiple applications can jointly access some data, you can first declare the name of the public area. The official document calls this name “keychain access group”.

Usage Example:

The following is how to use Keychain and sample code (Swift):

  1. Import the Security framework

Before using Keychain, you need to import the Security framework.

1
import Security
  1. Storing data

Use the SecItemAdd method to store data into the Keychain.

1
2
3
4
5
6
7
8
9
10
11
12
13
let password = "123456"
let data = password.data(using: .utf8)!
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "MyPassword",
kSecValueData as String: data]

let status = SecItemAdd(query as CFDictionary, nil)

if status == errSecSuccess {
print("Password saved to Keychain.")
} else {
print("Failed to save password to Keychain.")
}

In the above code, we convert the string password into binary data and store it into Keychain. kSecClass represents the stored data type, kSecAttrAccount is the name used to identify the stored data, and kSecValueData is the stored binary data.

  1. Read data

Use the SecItemCopyMatching method to read data from the Keychain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "MyPassword",
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne]

var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)

if status == errSecSuccess {
if let retrievedData = dataTypeRef as? Data,
let password = String(data: retrievedData, encoding: .utf8) {
print("Retrieved password from Keychain: \(password)")
} else {
print("Failed to retrieve password from Keychain.")
}
} else {
print("Failed to retrieve password from Keychain.")
}

In the above code, we use kSecReturnData to represent the returned stored binary data, and kSecMatchLimit to represent the amount of returned data. Here we only need to return one result, so we specify kSecMatchLimitOne.

Notice:

You need to pay attention to the following points when using Keychain:

(1) Access to the Keychain should be restricted and ensure that only authorized users can access the data in the Keychain.

(2) The data stored in Keychain are encrypted, so they cannot be accessed from the outside. If the data needs to be shared with other applications, you can use the group method to allow the program to be shared between apps, but the TeamID must be the same.

(3) The data in the Keychain is not stored in the Sandbox of the App. Even if the App is deleted, the data is still stored in the Keychain. If you reinstall the app, you can also get data from the keychain.

(4) Keychain is a secure storage mechanism, but it is not perfect. For example, a jailbroken device can access data stored in Keychain. Therefore, highly sensitive data should not be stored in Keychain. So sensitive information still needs to be used and stored with caution.

(5) When handling Keychain, attention should be paid to handling possible errors to avoid program crash or data loss.

Apple also provides a wealth of development documentation for keychain, including Keychain Services Programming Guide: the article includes keychain development using mac and ios.

4. Core Data

Core Data is a data management framework provided by Apple that provides a convenient way to manage and manipulate data in applications. The most important feature is persistent storage, which stores data in local files so that it can be used the next time the application starts.

Principle Description:

In Core Data, persistent storage is implemented using a SQLite database. SQLite is a lightweight embedded database that can be easily embedded into applications, providing an efficient and reliable way to store data.

Usage example:

Using Core Data for persistent storage requires the following steps:

(1) Define the data model: Create a data model file in Xcode to define the entities, attributes, relationships, etc. of the data.

(2) Create Core Data stack: Use the NSPersistentContainer class to create the Core Data stack, including managed object context, persistent storage coordinator and other components.

(3) Store and read data: Use the NSManagedObjectContext class to operate the managed object context, and store and read data by operating elements such as entities and attributes.

Here is a simple Swift code example that demonstrates how to use Core Data for data storage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Create Core Data stack
let container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores { _, error in
if let error = error {
print("Failed to load persistent stores: \(error)")
return
}
}

//Create a managed object context
let context = container.viewContext

//Create a Person entity object
let person = NSEntityDescription.insertNewObject(forEntityName: "Person", into: context) as! Person
person.name = "Tom"
person.age = 30

//Save data to persistent storage
do {
try context.save()
} catch {
print("Failed to save context: \(error)")
}

//Read data from persistent storage
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
do {
let persons = try context.fetch(fetchRequest)
for person in persons {
print("Name: \(person.name!), Age: \(person.age)")
}
} catch {
print("Failed to fetch persons: \(error)")
}

When using Core Data for persistent storage, you need to pay attention to the following points:

(1) When defining your data model, you should keep it as simple as possible. A complex data model can cause database operations to slow down and increase code complexity.

(2) Core Data provides a variety of persistent storage methods, such as SQLite, binary files, XML files, etc. Developers need to choose the appropriate storage method based on specific needs.

(3) Core Data is a thread-safe framework, but multi-threaded programming still requires attention to thread safety issues. It is recommended to create a separate managed object context in each thread.

In short, Core Data is a powerful persistent storage in iOS.

5. Database (SQLite)

In iOS, SQLite is a lightweight database engine that is widely used for persistent storage. SQLite uses files as data storage media to store and read data by encoding and decoding data. In iOS, SQLite is used to develop native applications to store large amounts of structured data.

Principle Description

SQLite is a disk-based relational database that supports SQL language operations. SQLite stores all data in a single file that can be easily copied, backed up, and transferred. SQLite is characterized by occupying very little memory and being fast, making it suitable for resource-constrained environments such as mobile devices.

Usage example:

Using SQLite persistent cache requires the following steps:

(1) Import the SQLite library: Import the SQLite library into the project, you can use CocoaPods or import it manually.

(2) Create database: Create database files through the API provided by the SQLite library. If the database file already exists, you can open it directly, otherwise you can use SQL statements to create the database file.

(3) Create a table: Use SQL statements to create a table and define field types and constraints for the table.

(4) Execute SQL statements: Execute SQL statements through the API provided by SQLite, and perform add, delete, modify and query operations on the table.

(5) Close the database: When the application exits or no longer uses the database, the database needs to be closed and resources released.

Sample code

Here is an example code for using SQLite for data storage, using Swift’s SQLite.swift library:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//Import SQLite.swift library
import SQLite

//Open database connection
let db = try! Connection("path/to/database.sqlite3")

//Define data table
let users = Table("users")
let id = Expression<Int64>("id")
let name = Expression<String>("name")
let email = Expression<String>("email")

//Create table
try!db.run(users.create { t in
t.column(id, primaryKey: true)
t.column(name)
t.column(email, unique: true)
})

//Insert data
let insert = users.insert(name <- "Alice", email <- "alice@example.com")
try!db.run(insert)

// Query data
for user in try! db.prepare(users) {
print("id: \(user[id]), name: \(user[name]), email: \(user[email])")
}

// update data
let alice = users.filter(name == "Alice")
try! db.run(alice.update(email <- "alice@example.org"))

// delete data
try! db.run(alice.delete())

//Close database connection
db.close()

Notice:

When using SQLite persistent cache, you need to pay attention to the following things:

(1) It is necessary to manually write SQL statements to perform data operations, which may be difficult for developers who are not familiar with the SQL language.

(2) Thread safety needs to be ensured when performing data operations. You can use the transaction mechanism provided by SQLite to perform data operations to avoid data damage or loss.

(3) SQLite database files can be easily copied, backed up and transferred, but data security issues need to be paid attention to.

6. Summary

After the above introduction, we can summarize it as follows:

UserDefaults

UserDefaults is a lightweight persistent storage method suitable for storing some simple configuration information or user preferences. Its advantage is that it is simple and easy to use, without having to consider issues such as data models and data migration. But its disadvantage is that it can only store some basic data types and cannot support complex data structures.

Best application scenario: Suitable for storing a small amount of simple configuration information or user preferences.

File Caching

File caching can store data in the local file system in the form of files, and is suitable for storing larger data, such as pictures, videos, etc. Its advantage is that it can flexibly control the cache strategy and cache size, which can effectively reduce the server load and improve the user experience. However, its disadvantage is that it requires manual management of cache, including cache path, cache file name, cache expiration time, etc. Improper management may lead to too many cache files and waste storage space.

Best application scenario: Suitable for storing large amounts of non-sensitive data, such as pictures, audio, video (SDWebImage), etc.

Keychain

Keychain is a secure storage method that can encrypt sensitive information (such as user passwords, keys, etc.) and store it in the system to ensure data security. Its advantage is that it can protect sensitive information from being maliciously obtained, and it also provides a convenient API to manage this sensitive information. But its disadvantage is that it can only store smaller data and is not suitable for storing large amounts of data.

Best application scenario: Suitable for storing sensitive information, such as user passwords, keys, etc.

Core Data

Core Data is an ORM (Object Relational Mapping) framework that can store data in a SQLite database. Its advantage is that it can easily manage data models, while providing powerful query, sorting, filtering and other functions, which is very suitable for storing complex data structures. However, its disadvantage is that the learning curve is steep, and you need to understand some concepts and APIs of Core Data. You also need to consider issues such as the migration of data models.

Best application scenario: Suitable for storing complex data structures, such as contacts, music playlists, calendar events, etc.

Database (SQLite)

Database is a universal storage method that can store any type of data. It also provides rich query, sorting, filtering and other functions. But generally, we prefer to use it in scenarios that require complex storage and query. Such as contacts, music playlists, etc.