Android SharedPreferences 原理简单分析

先上代码,在平时使用时,getSharePreferences的方法源码如下:


public SharedPreferences getSharedPreferences(String name, int mode) {

                            SharedPreferencesImpl sp;

                            synchronized (ContextImpl.class) {

                                if (sSharedPrefs == null) {

                                    sSharedPrefs = new ArrayMap>();

                                }

                                final String packageName = getPackageName();

                                ArrayMap packagePrefs = sSharedPrefs.get(packageName);

                                if (packagePrefs == null) {

                                    packagePrefs = new ArrayMap();

                                    sSharedPrefs.put(packageName, packagePrefs);

                                }

                                // 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 (mPackageInfo.getApplicationInfo().targetSdkVersion <

                                        Build.VERSION_CODES.KITKAT) {

                                    if (name == null) {

                                        name = "null";

                                    }

                                }

                                sp = packagePrefs.get(name);

                                if (sp == null) {

                                    File prefsFile = getSharedPrefsFile(name);

                                    sp = new SharedPreferencesImpl(prefsFile, mode);

                                    packagePrefs.put(name, sp);

                                    return sp;

                                }

                            }

                            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;

                        }

可见SDK是先取了缓存(sSharedPrefs静态变量),如果缓存未命中,才会开始构建新的对象,也就是说多次调用getSharedPreferences方法,几乎没有代价。而且实例的构造是被包裹在synchronized关键字下,因此该过程是多线程且安全的。

构造SharedPreferences

第一次构建SharedPreferences对象


// SharedPreferencesImpl.java

SharedPreferencesImpl(File file, int mode) {

    mFile = file;

    mBackupFile = makeBackupFile(file);

    mMode = mode;

    mLoaded = false;

    mMap = null;

    startLoadFromDisk();

}

1.其中mFile代表了磁盘上的配置文件

2.mBackupFile代表了“灾备”文件,是一个数据写入失败时,用以恢复的文件,该文件路径为mFile+“.bat”

3.mMap用于在内存中存储我们的配置数据,也就是getXxx数据的来源。

下面重点关注下 startLoadFromDisk()方法


private void startLoadFromDisk() {

        synchronized (this) {

            mLoaded = false;

        }

        new Thread("SharedPreferencesImpl-load") {

            public void run() {

                loadFromDisk();

            }

        }.start();

    }

这里他开启了一个从Disk读取的线程,即:


// SharedPreferencesImpl.java

private void loadFromDisk() {

    synchronized (SharedPreferencesImpl.this) {

        if (mLoaded) {

            return;

        }

        if (mBackupFile.exists()) {

            mFile.delete();

            mBackupFile.renameTo(mFile);

        }

    }

    ..略去无关代码 ..

    str = new BufferedInputStream(

            new FileInputStream(mFile), 16*1024);

    map = XmlUtils.readMapXml(str);

    synchronized (SharedPreferencesImpl.this) {

        mLoaded = true;

        if (map != null) {

            mMap = map;

            mStatTimestamp = stat.st_mtime;

            mStatSize = stat.st_size;

        } else {

            mMap = new HashMap<>();;

        }

        notifyAll();

    }

}

loadFormDisk()在整个过程中非常关键,这其中它做了以下几件事:

1.如果有灾备文件,则直接使用灾备文件回滚。

2.把配置从磁盘文件中读取到内存被保存到mMap字段中, 即 mMap=map;

3.标记读取完成,这个字段在后面的awaitLoadLocked会用到,记录读取文件的时间,后面MODE_MULTI_PROCESS 中会用到

4.发一个notifyAll通知已经读取完毕,激活其他所有等待加载中的线程。

在这里借用一张图来演示其整个过程:

image

getX原理分析


   public float getFloat(String key, float defValue) {

        synchronized (this) {

            awaitLoadedLocked();

            Float v = (Float)mMap.get(key);

            return v != null ? v : defValue;

        }

    }

1.synchronized保证了线程安全

2.get操作一定是从mMap中读取,即从内存中读取,无过多性能损耗。

3awaitLoadedLocked()保证了读取操作一定是在loadFormDisk()执行完成之后,同步等待。因此在第一次调用get操作时,可能会发生阻塞,特别注意:这也是为什么sp会被定义为轻量级存储系统的重要原因

putX原理分析

相对get操作,put操作会复杂一些。

创建editor


// SharedPreferencesImpl.java

    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.

        synchronized (this) {

            awaitLoadedLocked();

        }

        return new EditorImpl();

    }

这里的EditorImpl()无构造函数,仅仅去初始化两个成员变量


// SharedPreferencesImpl.java

public final class EditorImpl implements Editor {

    private final Map mModified = Maps.newHashMap();

    private boolean mClear = false;

    ...略去方法定义 ..

    public Editor putString(String key, @Nullable String value) { ... }

    public boolean commit() { ... }

    ..

}

1.mModified是我们每次putXxx后所改变的配置

2.mClear标识要清空配置项

putString


// SharedPreferencesImpl.java

public void apply() {

    final MemoryCommitResult mcr = commitToMemory();

    .. 略无关 ..

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

}

这里是吧我们的配置项放到mModified属性里保存。等到apply或commit的时候回写到内存和磁盘。

关于apply和commit我们需要分别来看,即可它们之间的区别。


// SharedPreferencesImpl.java

public void apply() {

    final MemoryCommitResult mcr = commitToMemory();

    ... 略无关 ...

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

}

apply的核心在于两点:

1.commitToMemory完成了内存的同步回写。
2.enqueueDiskWrite完成了磁盘异步回写具体如下:

// SharedPreferencesImpl.java
private MemoryCommitResult commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();
    synchronized (SharedPreferencesImpl.this) {

        ... 略去无关 ...

        mcr.mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        synchronized (this) {
            for (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) {
                    mMap.remove(k);
                } else {
                    mMap.put(k, v);
                }
            }

            mModified.clear();
        }
    }
    return mcr;
}

两个关键点

1.把Editor.mModified中的配置项回写到SharedPreferences.mMap中,完成了内存的同步回写。
2.把shareadPreference.mMap保存在mcr.mapTOWriteToDisk中,而后者就是即将要回写到磁盘中的数据源。

// SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr);
                }

                ...
            }
        };

    ...

    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

使用了singgleThreadExecutor单一线程池去依次执行写入磁盘的runnable序列。

在这之后是真正执行把数据写入磁盘的方法:

private void writeToFile(MemoryCommitResult mcr) {
    // Rename the current file so it may be used as a backup during the next read
    if (mFile.exists()) {
        if (!mBackupFile.exists()) {
            if (!mFile.renameTo(mBackupFile)) {
                return;
            }
        } else {
            mFile.delete();
        }
    }

    // Attempt to write the file, delete the backup and return true as atomically as
    // possible.  If any exception occurs, delete the new file; next time we will restore
    // from the backup.
    try {
        FileOutputStream str = createFileOutputStream(mFile);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        try {
            final StructStat stat = Os.stat(mFile.getPath());
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
        }
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        return;
    }

    // Clean up an unsuccessfully written file
    mFile.delete();
}

整段代码下来,主要分为三个过程:

1.先把已存的老配置文件复制重命名,即加上.bak作为灾备文件,然后删除老的配置文件删除。相当于做的备份处理
2.想mFile一次性写入所有的配置项,即mcr.mMapToWriteToDisk(即上文提到被mMap赋值后即将写入数据)一次性写入到磁盘之中,如果写入成功则删除.bak的灾备文件,同时记录这次同步的时间。
3.如果上述过程中2失败,则会删除这个半成品配置文件。

apply总结:

1.通过commitToMemory将配置项同步会写到内存SharePreferences .mMap中,此时,任何的getXxx都可以获得最新的数据了。
2.通过enqueDiskWrite调用writeFile将所有配置项一次性异步会写到磁盘,这是一个单线程的线程池

具体流程可总结为下图:

SharedPreferencesApply.png

Commit

commit比较简单,直接看代码和时序图即可,大致和apply相同、

public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}
SharedPreferencesCommit.png

可以看得出,commit会等待异步任务的返回,说明会阻塞当前调用线程,也因此说commit是同步写入,apply是异步写入。

在了解完整个SharedPreferences的代码原理之后,我们不难发现它的一些特点和基本概念,所以下面我们可以试着总结出一些平时在使用SharedPreferences时一些需要注意的事项,和所需要规避的事情:

勿存储过大value

永远记住,SharedPreferences是一个轻量级的存储系统,不要存过多且复杂的数据,这会带来以下的问题
第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。
这些key和value会永远存在于内存之中,占用大量内存。

勿存储复杂数据

SharedPreferences通过xml存储解析,JSON或者HTML格式存放在sp里面的时候,需要转义,这样会带来很多&这种特殊符号,sp在解析碰到这个特殊符号的时候会进行特殊的处理,引发额外的字符串拼接以及函数调用开销。如果数据量大且复杂,严重时可能导频繁GC。

不要乱edit和apply,尽量批量修改一次提交

edit会创建editor对象,每进行一次apply就会创建线程,进行内存和磁盘的同步,千万写类似下面的代码
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
sp.edit().putString("test1", "sss").apply();
sp.edit().putString("test2", "sss").apply();
sp.edit().putString("test3", "sss").apply();
sp.edit().putString("test4", "sss").apply();

建议apply,少用commit

commit同步写内存,同步写磁盘。有是否成功的返回值
apply同步写内存,异步写磁盘。无返回值

registerOnSharedPreferenceChangeListener弱引用问题

见本文初

apply和commit对registerOnSharedPreferenceChangeListener的影响

对于 apply, listener 回调时内存已经完成同步, 但是异步磁盘任务不保证是否完成
对于 commit, listener 回调时内存和磁盘都已经同步完毕

不要有任何用SP进行多进程存储的幻想

这个话题不需要过多讨论,只记住一点,多进程别用SP,Android没有对SP在多进程上的表现做任何约束和保证。附上Google官方注释:

@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}.

你可能感兴趣的:(Android SharedPreferences 原理简单分析)