Use of Shared Preferences and Source Code Analysis

Shared Preferences is a lightweight data storage method in Andorid. Usually used to store some simple data types, such as int, String, Boolean. Shared Preferences stores data when it comes in the form of ArrayMap key-value pairs. Eventually, ArrayMap's data is written to an XML file through an IO stream. The location of the XML file on the phone is: / data/data/shared_prefs./

Use of Shared Preferences

SharedPreferences sf = getPreferences("demo",Context.MODE_PRIVATE);
SharedPreferences.Editor mEditor = sf.edit();
mEditor.putString("name","leidong");
mEditor.putBoolean("man",true);
mEditor.putInt("age",35);
mEditor.commit();//mEditor.apply();

String name = sf.getString("name","");
boolean man = sf.getBoolean("man",true);
int age = sf.getInt("age",0);

Mode:

//Private mode. The XML file overwrites every time
public static final int MODE_PRIVATE = 0x0000;
//Files are open to read and write. Insecurity. Officials have abandoned this usage.
public static final int MODE_WORLD_READABLE = 0x0001;
public static final int MODE_WORLD_WRITEABLE = 0x0002;
//Add mode. Check whether the file exists, add content to the file if it exists, and create a new file if it does not exist.
public static final int MODE_APPEND = 0x8000
//Cross-process model, officially abandoned
public static final int MODE_MULTI_PROCESS = 0x0004;

II. Principles of Shared Preferences

1. Acquisition of SharedPreferences objects

Activity & ContextWrapper

//If the name of xml is not specified, the default value is the className of the current activity
public SharedPreferences getPreferences(int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
}
public SharedPreferences getSharedPreferences(String name, int mode) {
    return mBase.getSharedPreferences(name, mode);
}

ContextImpl.java

public SharedPreferences getSharedPreferences(String name, int mode) {
    // At least one application in the world actually passes in a null
    // name.  This happened to work because when we generated the file name
    // we would stringify it to "null.xml".  Nice.
    if (mLoadedApk.getApplicationInfo().targetSdkVersion <
            Build.VERSION_CODES.KITKAT) {
        if (name == null) {
            name = "null";
        }
    }

    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);
}
//Generate an XML file and create its File object
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}
//Returns the file object of / data/data/shared_prefs / directory
private File getPreferencesDir() {
    synchronized (mSync) {
        if (mPreferencesDir == null) {
            mPreferencesDir = new File(getDataDir(), "shared_prefs");
        }
        return ensurePrivateDirExists(mPreferencesDir);
    }
}

//ContextImpl uses the sSharedPrefsCache collection to save the newly created SharedPreferencesImpl object, retrieved each time
//Before the SharedPreferencesImpl object, query the sSharedPrefsCache collection, and if there is one in the collection, return it directly, such as
//If not, create a SharedPreferencesImpl object and add it to the sSharedPrefsCache collection.
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
           //Check mode, if MODE_MULTI_PROCESS and MODE_WORLD_WRITEABLE, throw directly
           // SecurityException does not allow access to SharedPreferencesImpl objects corresponding to this xml.
            checkMode(mode);
            if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                if (isCredentialProtectedStorage()
                        && !getSystemService(UserManager.class)
                                .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                    throw new IllegalStateException("SharedPreferences in credential encrypted "
                            + "storage are not available until after user is unlocked");
                }
            }
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
//In the case of MODE_MULTI_PROCESS mode, in the case of multiple processes reading and writing xml, in order to prevent extracting from the sShared PrefsCache collection
//The data in the outgoing sShared Preferences Impl object is not synchronized with the data in xml, so the data in XML is read from disk every time.
//Let's return the new sSharedPreferencesImpl object.
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

SharedPreferencesImpl.java

//Markup, whether xml has been loaded into memory
private boolean mLoaded = false; 
//Constructor, initialization, creation of backup file mBackupFile, reading xml files on disk
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);//Backup file
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    startLoadFromDisk();
}
//Start a new thread to load the xml file content of the disk
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}
//
private void loadFromDisk() {
    synchronized (mLock) {
        //If xml has been loaded, return it directly
        if (mLoaded) {
            return;
        }
        //If the backup exists, it means that the previous read and write operations have been interrupted. Use mBackup File as mFile and use the contents of the backup file directly.
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    // Debugging
    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }

    Map<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) XmlUtils.readMapXml(str);//Read the content from xml into memory and assign it to map
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        mLoaded = true;//Mark as loaded xml file
        mThrowable = thrown;

        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
            if (thrown == null) {
                if (map != null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
            // In case of a thrown exception, we retain the old map. That allows
            // any open editors to commit and store updates.
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();//Wake up other processes waiting for mLock locks
        }
    }
}
//Get the stored key-value pair data in Shared Preferences
public int getInt(String key, int defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();//If mLoaded is false, the thread will always be waiting
        Integer v = (Integer)mMap.get(key);//Get the values read into memory (mMap key-value pairs)
        return v != null ? v : defValue;
    }
}

2. Shared Preferences. Editor's Storage of Data

private final Map<String, Object> mModified = new HashMap<>();

public Editor edit() {
    // TODO: remove the need to call awaitLoadedLocked() when
    // requesting an editor.  will require some work on the
    // Editor, but then we should be able to do:
    //
    //      context.getSharedPreferences(..).edit().putString(..).apply()
    //
    // ... all without blocking.
    //If the thread gets the mLock object lock, but mLoaded is false, that is, the loading xml process is not finished, the thread will wait all the time.
    synchronized (mLock) {
        awaitLoadedLocked();
    }

    return new EditorImpl();
}
//If mLoaded is false, the thread will always be waiting
private void awaitLoadedLocked() {
    if (!mLoaded) {
        // Raise an explicit StrictMode onReadFromDisk for this
        // thread, since the real read will be in a different
        // thread and otherwise ignored by StrictMode.
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

//The stored key-value pair data is stored only in the mModified collection of the internal class Editor
public Editor putInt(String key, int value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }
    //Save the data in the mModified collection of the internal class Editor to the mMap collection of the Shared Preferences Impl class.
    MemoryCommitResult mcr = commitToMemory();
    //Write the data in the mMap collection into the xml file
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    notifyListeners(mcr);//Notify registered listeners
    return mcr.writeToDiskResult;
}

public void apply() {
    final long startTime = System.currentTimeMillis();
    //Save the data in the mModified collection of the internal class Editor to the mMap collection of the Shared Preferences Impl class.
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    //Write the data in the mMap collection into the xml file
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);//Notify registered listeners
}
//Write the data in the mMap collection into the xml file.
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    //If it is in the form of apply(), then postWriteRunnable!= null, isFromSyncCommit is true.
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);//Writing to disk xml is encapsulated in New Threads
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    //If it is in the form of commit() and there is no write disk task (mDiskWritesInFlight == 1), it is called directly.
    //writeToDiskRunnable.run() performs writeToFile() write operations, and no new threads are available.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    //If it's in the form of apply(), all threads are added to QueuedWork, saved as queues, and started one by one.
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

In summary, Shared Preferences Impl defines the ArrayMap collection, which loads the contents of the XML file into the ArrayMap collection when the object is initialized. Shared Preferences Impl provides a get() interface to return data from its ArrayMap collection. Editor also defines the set of ArrayMap, provides an put() interface to the outside world, accepts the value passed by the caller and stores it in its ArrayMap. When commit() or apply(), it synchronizes the ArrayMap values in Editor and Shared Preferences Impl, and saves the data in ArrayMap into an XML file. To consider the use of multithreading, most operations in Shared Preferences Impl and Editor have corresponding object locks and synchronized blocks of synchronized code. Since Shared Preferences Impl is initialized to load XML once, for performance reasons, ContextImpl sets up a buffer (Array Map) to save Shared Preferences Impl objects for reuse of existing objects. Shared Preferences does not support cross-process usage. Although there is a MODE_MULTI_PROCESS cross-process mode, it does not work very well. Officials have abandoned this usage.

Matters needing attention:

1. Do not save SharedPreference variables when using MODE_MULTI_PROCESS
2.xml is not suitable for too much data. Threads may be blocked during commit() operations or loading xml.
3. Try to use apply() instead of commit()

Doubt:
1. ContextImpl's buffer uses Map key pair to save Shared Preferences Impl object. Its key value is packageName, which accesses Shared Preferences with the same name at the same time in multi-threaded situation. That must be using the same Shared Preferences Impl object, which will cause thread security problems. Therefore, the operation in Shared Preferences Impl and Editor has added synchronous code block syn. Chronized. The problem of multithreading is solved. However, why does google not recommend MODE_MULTI_PROCESS for multi-process? Is there a difference between multithreading and multiprocess considerations?
2. If an application wants to access Shared Preferences in another application, why create PackageContext ()?

Context configContext = null;
try {
    configContext = createPackageContext("com.example.administrator.android_example", Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
}catch (PackageManager.NameNotFoundException e) {
    e.printStackTrace();
}
SharedPreferences sf = configContext.getSharedPreferences("demo", Context.MODE_WORLD_READABLE);

Thread security refers to the conflict and asynchrony problems that occur when multiple threads operate on the same variable at the same time. Because each process has its own memory space, and the memory space between processes is isolated, each process has its own Context, so it is necessary to obtain the corresponding Context to obtain the corresponding Shared Preferences object, otherwise cross-process can not get the desired Shared Preferences data. synchronized can ensure thread security as much as possible, but it can not guarantee process security.

Reference resources:
https://stackoverflow.com/questions/27827678/use-sharedpreferences-on-multi-process-mode
https://stackoverflow.com/questions/4693387/sharedpreferences-and-thread-safety
https://www.jianshu.com/p/875d13458538

Keywords: xml Java Android Google

Added by zipdisk on Sun, 19 May 2019 04:10:33 +0300