primers

  • What is SharedPreference

    Interface for accessing and modifying preference data returned by `Context#getSharedPreferences`. 
    For any particular set of preferences, there is a single instance of this class that all clients share. 
    Modifications to the preferences must go through an `Editor` object to ensure the preference values remain in a consistent state and control when they are committed to storage. 
    Objects that are returned from the various get methods must be treated as immutable by the application.
    Copy the code
  • What are the considerations in using SharedPreference

    • The difference between commit and apply

      • Return value: Apply returns no value, commit returns a Boolean value indicating whether the change was successful
      • Operation efficiency: Apply refers to commit modified data to memory and asynchronously to hard disks; COMMIT refers to commit data to hard disks synchronously. Therefore, when multiple concurrent commits are performed, they wait for the ongoing commit to be saved to disk before execution, thus reducing efficiency. However, apply only commits to memory, and subsequent calls to Apply overwrite the previous memory data, making it more efficient to some extent
      • Suggestion: If you do not care about the submission result, use Apply. To ensure a successful commit and subsequent operations, use commit
    • Multiprocess representation

      In multiple processes, it is not recommended to use SharedPreference to exchange data because different versions are unstable. Instead, ContentProvider is recommended. In some articles, it has been mentioned that it is possible to add a flag bit (MODE_MULTI_PROCESS) in a multi-process scenario using SharePrefenerce. This flag bit was supported by default prior to 2.3 (API 9), but after 2.3, situations requiring multi-process access will need to be declared as a display. This flag bit is now deprecated because it is unstable on some versions.

This article is a collection of several excellent articles and official documents, not listed separately, (#^.^#).

All men are mortal

Google I/O conference in 2019, the official launch SharedPreference wrapper classes EncryptedSharedPreference guarantee the safety of SharedPreference (in Jetpack Security component).

The internal implementation of Android 8.0 is also different before and after the source code, which shows that the authorities have been trying to remedy its shortcomings.

1. Design and implementation

SharedPreference is a lightweight storage class, which is used to store various configuration information of App. In essence, data is stored in XML in key-value mode and the directory is data/data/package_name/shared_prefs.

Json was new at the time, and while it was slowly becoming popular as a lightweight data storage interchange format, SP was more special to Android, especially for its friendly readability.

Therefore, its essence is to provide a set of interfaces to modify the key-value of locally stored XML files, and the modification results are also stored in local files.

2. Read operation

If the file is read each time and the specified key is found in the key-value pair that is parsed, the performance is obviously poor and the I/O operation can be optimized.

Read operation is optimized, so the designer of when SharedPreference object through the Context for the first time. The getSharedPreference initialization (), to a read of the XML file, and the key – value within the file to read out to cache in the map, In this way, subsequent operations can be performed directly in memory.

One obvious problem is swapping memory for efficiency, and this memory caching mechanism can lead to high memory usage when there is a large amount of data in XML. So, Share Preference is a good place to store lightweight, frequently accessed data!

3. Write operations

For write operations, the designers also provide a series of interfaces to optimize performance.

For example, when we modify a value, we usually use preferences? .edit()? .putString(key, value)? The apply () or preferences? .edit()? .putBoolean(key, value)? The.commit() implementation, what is edit()? What is apply()/commit()? Why not just go through Preferences? PutString (key, value)?

This is because, in complex businesses, there are times when multiple values need to be updated for a single user operation. Instead of multiple file updates, combine these updates into a single read and write operation. Therefore, the designer abstracts out an Editor class, which is also in the official definition. All updates to SP need to be performed through Editor, and edit values are actually written to the file only when you call commit/apply. The difference between Commit and apply is described above. The use of apply is to asynchronously perform file data synchronization, especially if frequent commits are performed on the main thread.

Editor+ Apply optimizes write operations: 1. 2. Asynchronous synchronization. I/O is suitable for child threads.

So the problem is that the child thread updates the file, it is necessary to consider thread safety; Can apply asynchronous operations avoid ANR? Unfortunately, the answer is no.

4. Data update & selection of files

As the number in XML increases, a file read or write operation becomes higher.

How is data updated in XML? Full or incremental? => Full. Edit updates the map data to the memory map. Commit /apply updates the map data to the XML. Therefore, you are advised to store data in files based on service requirements.

So the question is, why full update? Does commit/apply guarantee successful data synchronization? Incremental updates require more memory operations and comparisons, and also increase the problem of multi-threaded synchronization by being as lightweight as the original design, so taking full updates is not an incremental trade-off. Unfortunately, even with the internal optimization of SP, there is still no guarantee of 100% synchronization success, and the real world is harsh.

Thread safety

SharedPreference is thread-safe. How does it work? Lock. The designer added three locks.

1. Lock and read operations

// GuardedBy significantly improves code readability
final class SharedPreferencesImpl implements SharedPreferences {
    @GuardedBy("mLock")
    private Map<String.Object> mMap; ./** Latest memory state that was committed to disk */
    @GuardedBy("mWritingToDiskLock") private long mDiskStateGeneration; . public finalclass EditorImpl implements Editor {
        private final Object mEditorLock = new Object(a); @GuardedBy("mEditorLock")
        private final Map<String.Object> mModified = newHashMap<>(); . }... }Copy the code

For the read operation, we know that it achieves thread-safety through a lock by fetching a value from mMap memory and returning it:

    @Override
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            returnv ! =null? v : defValue; }}Copy the code

2. Write operations

What about write operations? As mentioned above, mModified is maintained in the Editor by storing changes and updating them. This data is only updated to mMap when applied, so a lock is required to maintain the mModified security -mEditorLock. Why doesn’t it share a lock with mMap? If they share a lock, get/ PUT will wait for each other regardless of whether apply is applied or not, thus losing the purpose of asynchronous updates. To keep get/ PUT non-conflicting, data stored in mModified before Apply will not be applied to mMap.

    public final class EditorImpl implements Editor {
        private final Object mEditorLock = new Object(a); @GuardedBy("mEditorLock")
        private final Map<String.Object> mModified = new HashMap<>();

        @Override
        public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this; }}Copy the code

When Editor apply is performed, two locks are required to keep mMap and mModified thread safe, and another lock is required to update mMap data to a local file.

	@Override
        public void apply(){... Sync to memory final MemoryCommitResult MCR = commitToMemory(); . Synchronized to a file SharedPreferencesImpl. This. EnqueueDiskWrite (MCR, postWriteRunnable); . Inform notifyListeners (MCR); }// Returns true if any changes were made
        private MemoryCommitResult commitToMemory(){... SharedPreferencesImpl. This. MLock secure mMap synchronized (SharedPreferencesImpl. This. MLock) {... Synchronized (mEditorLock) {...... synchronousfor (Map.Entry<String.Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        // "this" is the magic value for a removal mutation. In addition, setting a value to "null" for a given key is specified to be equivalent to calling remove on that key.
                        if (v == this || v == null) {
                            if(! mapToWriteToDisk.containsKey(k)) {continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if(existingValue ! =null && existingValue.equals(v)) {
                                    continue; } } mapToWriteToDisk.put(k, v); }... } mModified.clear(); . }}return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
                    listeners, mapToWriteToDisk);
        }
        
        private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
        	final boolean isFromSyncCommit = (postWriteRunnable == null);
        	final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    // mWritingToDiskLock ensures file securitysynchronized (mWritingToDiskLock) { writeToFile(mcr, isFromSyncCommit); }... }}; . QueuedWork.queue(writeToDiskRunnable, ! isFromSyncCommit); } privatestatic class MemoryCommitResult {
          final long memoryStateGeneration;
          final boolean keysCleared;
          @Nullable final List<String> keysModified; // For notify Modified Listener
          @Nullable final Set<OnSharedPreferenceChangeListener> listeners;
          final Map<String.Object> mapToWriteToDisk;
          final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

          @GuardedBy("mWritingToDiskLock")
          volatile boolean writeToDiskResult = false;
          boolean wasWritten = false;
        }
Copy the code

Finally, three locks are used to achieve thread-safe write operations.

3. ANR

Apply intended to avoid the ANR of main thread I/O through asynchrony, but did it really achieve this goal? The answer can be found in this article: Dissecting the ANR issues caused by SharedPreference Apply – bytedance technical team

In the apply () method, first create a waiting for locks, depending on the source code version, will eventually to the task of update file QueuedWork. SingleThreadExecutor () a single thread or HandlerThread to execute, when the file after the update releases the lock.

But when activity.onStop () and the Service handle related methods like onStop, queuedWork.waittoFinish () waits for all pending locks to be released, so if SharedPreferences have not finished updating, It is possible to get stuck in the main thread and eventually time out resulting in ANR.

When does SharedPreferences never complete a task? For example, apply() too often and inappropriately, resulting in too many tasks.

Process safety

1. How to ensure process security?

Note: This class does not support use across multiple processes. :

/**
 * Interface for accessing and modifying preference data returned by {@link Context#getSharedPreferences}.  
 * For any particular set of preferences, there is a single instance of this class that all clients share.
 * Modifications to the preferences must go through an {@link Editor} object  to ensure the preference values remain in a consistent state and control  when they are committed to storage.  Objects that are returned from the various <code>get</code> methods must be treated as immutable by the application.
 *
 * <p>Note: This class provides strong consistency guarantees. It is using expensive operations which might slow down an app. Frequently changing properties or properties where loss can be tolerated should use other mechanisms. For more details read the comments on {@link Editor#commit()} and {@link Editor#apply()}.
 *
 * <p><em>Note: This class does not support use across multiple processes.</em>
 */
public interface SharedPreferences {
Copy the code

So SharedPreference itself is process insecure, so if we have such a business requirement, how can we guarantee it?

For example, add a file lock to ensure that only one process is accessing the file at a time. For example, use ContentProvider to customize access to SharedPreference within it.

Once upon a time, the preference passed in Mode could be read and written to multiple processes, but this has been abandoned. We can see the official attempt and abandonment of multiple processes:

    /** * File creation mode: the default mode, where the created file can only be accessed by the calling application (or all applications sharing the same user ID). * /
    public static final int MODE_PRIVATE = 0x0000;

    /**
     * File creation mode: allow all other applications to have read access to the created file.
     * <p>
     * Starting from {@link android.os.Build.VERSION_CODES#N}, attempting to use this mode throws a {@link SecurityException}.
     *
     * @deprecated Creating world-readable files is very dangerous, and likely to cause security holes in applications. It is strongly discouraged; instead, applications should use more formal mechanism for interactions such as {@link ContentProvider}, {@link BroadcastReceiver}, and {@link android.app.Service}. There are no guarantees that this access mode will remain on a file, such as when it goes through a backup and restore.
     * @see android.support.v4.content.FileProvider
     * @see Intent#FLAG_GRANT_WRITE_URI_PERMISSION
     */
    @Deprecated
    public static final int MODE_WORLD_READABLE = 0x0001;

    /**
     * File creation mode: allow all other applications to have write access to the created file.
     * <p>
     * Starting from {@link android.os.Build.VERSION_CODES#N}, attempting to use this mode will throw a {@link SecurityException}.
     */
    @Deprecated
    public static final int MODE_WORLD_WRITEABLE = 0x0002;

    /**
     * File creation mode: for use with {@link #openFileOutput}, if the file already exists then write data to the end of the existing file instead of erasing it.
     * @see #openFileOutput
     */
    public static final int MODE_APPEND = 0x8000;

    / * * *@deprecated MODE_MULTI_PROCESS does not work reliably in some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes.  Applications should not attempt to use it.  Instead, they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.
     */
    @Deprecated
    public static final int MODE_MULTI_PROCESS = 0x0004;
Copy the code

2. Files are damaged and backed up

As mentioned above, onStop will wait for the end of SP operation, this is to ensure the success of data synchronization as much as possible, but unknown reasons exist after all, how to avoid file synchronization interruption, failure to cause data damage and loss? The answer is backup.

Bak: deletes the backup file after the write is complete. If a backup file is found after the process starts, the backup file is renamed as the source file. The original content is lost but the file is not damaged.

FAQ about Share Preference

  • What is SharedPreference
  • How does SharedPreference implement data read and write
  • Do read operations block the main thread? Of course it will. If you get on the main thread and the file has not been read for the first time, it will wait and block if there is a lock
  • Is it type safe? No, key-value pairs are not guaranteed to hold the same type of key store, and if they do, they can cause a ClassCastException, which is also an issue to consider in the upgrade
  • Why pass Editor
  • What is the difference between commit and apply
  • Is thread safe? How to ensure
  • Is the process secure? How to ensure
  • Exception handling: Read/write fails or is interrupted

DataStore

define

Jetpack DataStore is a data storage solution that allows you to store key-value pairs or typed objects with protocol buffers. DataStore uses Kotlin coroutines and Flow to store data asynchronously, consistently, and transactionally.

If you’re currently using SharedPreferences to store data, consider migrating to DataStore instead.

In short, Jetpack DataStore is a new data storage solution that can be used to store key-value pairs or structured objects, using Kotlin collaboration and streaming data manipulation, asynchronous and consistent.

implementation

DataStore provides two implementations: Preferences DataStore and Proto DataStore.

  • Preferences DataStore stores and accesses data using keys. This implementation does not require a predefined schema, and it does not provide type safety. Key is used to read and write data, which does not ensure type security and does not require predefined formats. Similar to SP, but based on Flow implementation, does not block the main thread.
  • Proto DataStore stores data as instances of a custom data type. This implementation requires you to define a schema using protocol buffers, but it provides type safety. To use data objects for read and write, predefined data formats are required to ensure type security.

Preferences DataStore

setup

// Preferences DataStore (SharedPreferences like APIs)
dependencies {
  implementation "Androidx. Datastore: datastore - preferences: 1.0.0 - alpha05"
}
// Alternatively - use the following artifact without an Android dependency.
dependencies {
  implementation "Androidx. Datastore: datastore - preferences - core: 1.0.0 - alpha05"
}
Copy the code

Read and write

Persist simple key-value pairs using DateStore and Preferences.

create

val dataStore: DataStore<Preferences> = context.createDataStore(name = "settings")
Copy the code

read

Since no predefined format is required, the key to store to the DataStore must be defined through preferencesKey(), and the datastore.data property is used to retrieve the stored data through Flow.

val EXAMPLE_COUNTER = preferencesKey<Int>("example_counter")
val exampleCounterFlow: Flow<Int> = dataStore.data
  .map { preferences ->
    // No type safety.preferences[EXAMPLE_COUNTER] ? :0
}
Copy the code

write

Like SP, DataStore provides an edit() method to update data in the DataStore, but in a very different way, its transform executes a piece of code as an atomic operation.

suspend fun incrementCounter(){ dataStore.edit { settings -> val currentCounterValue = settings[EXAMPLE_COUNTER] ? :0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1}}Copy the code

Proto DataStore

setup

// Typed DataStore (Typed API surface, such as Proto)
dependencies {
  implementation "Androidx. Datastore: datastore: 1.0.0 - alpha05"
}
// Alternatively - use the following artifact without an Android dependency.
dependencies {
  implementation "Androidx. Datastore: datastore - core: 1.0.0 - alpha05"
}
Copy the code

Read and write

Define the format

The Proto DataStore must specify a predefined format file in the app/ SRC /main/ Proto directory that specifies the data types to persist. See Protobuf Language Guide for more information.

syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message Settings {
  int example_counter = 1;
}
Copy the code
  • Note that the stored object classes are generated at compile time, so make sure the project is rebuilt.

create

There are two steps:

  • Define a class that inherits fromSerializer<T>And theTIs a type defined in Proto Files. This operation tells the DataStore how to read and write data, and you need to ensure that the type has an initial value, which is needed when creating a file.
  • useContext.createDataStore()createDataStore<T>For example, the filename variable tells DataStore which file to store data in, and the serializer variable tells DataStore which serialized class, as defined in the previous step.
object SettingsSerializer : Serializer<Settings> {
  override val defaultValue: Settings = Settings.getDefaultInstance()

  override fun readFrom(input: InputStream): Settings {
    try {
      return Settings.parseFrom(input)
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException("Cannot read proto.", exception)
    }
  }

  override fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
}

val settingsDataStore: DataStore<Settings> = context.createDataStore(fileName = "settings.pb", serializer = SettingsSerializer)
Copy the code

read

val exampleCounterFlow: Flow<Int> = settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }
Copy the code

write

Provide the updateData method.

suspend fun incrementCounter() {
  settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}
Copy the code

Use DataStore in synchronized code

Caution: Avoid blocking threads on DataStore data reads whenever possible. Blocking the UI thread can cause ANRs or UI jank, and blocking other threads can result in deadlock.!

The main benefit of DataStore is the asynchronous API, but it does not mean that it is feasible to change the surrounding code to asynchronous execution, such as using synchronous disk I/O libraries or dependent libraries that do not provide asynchronous apis.

Kotlin Ctrip provides a runBlocking generator that Bridges the gap between synchronous and asynchronous code and can be used to read data from DataStore synchronously.

// block the calling thread until the DataStore returns data
val exampleData = runBlocking { dataStore.data.first() }
Copy the code

Performing synchronous I/O operations on the main thread may cause ANR and UI Jank, which can be optimized by asynchronously preloading DataStore data:

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        dataStore.data.first()
        // You should also handle IOExceptions here.}}Copy the code

This way, the DataStore asynchronously reads the data and caches it to memory, making the next synchronous fetch faster and potentially avoiding simultaneous I/O operations when the read is complete.

Migrate SP data to DataStore

Introducing dependency libraries

Implementation "androidx datastore: datastore - preferences: 1.0.0 - alpha05"

SP is specified when creating datastores

In the place where the SharedPreference was obtained, the DataStore is created and the migration is completed.

	// original
	fun initPreference(context: Context, fileName: String, mode: Int) {
        preferences = context.getSharedPreferences(fileName, mode)
	}

	// now
	fun initPreference(context: Context, fileName: String) {
        try {
            if (isPreferenceMigrated(fileName)) {
                dataStore = context.createDataStore(fileName)
            } else {
                dataStore = context.createDataStore(
                    fileName,
                    migrations = listOf(SharedPreferencesMigration(context, fileName))
                )
                setPreferenceMigrated(fileName)
            }
        } catch (e: Exception) {
        }
    }
Copy the code

GetXXX rewrite

	// original
    fun getString(key: String.defaultValue: String) :String {
        returnpreferences? .getString(key, defaultValue) ? : defaultValue }// now
    fun getString(key: String.defaultValue: String) :String {
        val pKey = preferencesKey<String>(key)
        returnrunBlocking { dataStore? .data? .catch { it.printStackTrace() } ? .map { p -> p[pKey] ? : defaultValue }? .first() ? : defaultValue } }Copy the code

SetXXX rewrite

	// original
    fun putString(key: String, value: String){ preferences? .edit()? .putString(key, value)? .apply() }// now
    fun putString(key: String, value: String) {
        val pKey = preferencesKey<String>(key) runBlocking { dataStore? .edit { it[pKey] = value } } }Copy the code

The above are the features and benefits of DataStore. More details will be provided next time because there are many mechanisms involved.