Android SharedPreferences

SharedPreferences 是 Android SDK 供给的轻量的数据存储类,用于存储 Key-Value 数据。

运用指南

写入数据
context.getSharedPreferences("name", 0).edit().putString("key", "value").apply()

或是:

context.getSharedPreferences("name", 0).edit().putString("key", "value").commit()
  • apply() 会立即更改内存中的 SharedPreferences 目标,但会将更新异步写入磁盘。
  • commit() 将数据同步写入磁盘。但是,因为 commit() 是同步的,您应避免从主线程调用它,因为它或许会暂停您的界面出现。

对于这两个办法存在以下一些特性:

  • commit 办法会同步的将修正内容同步到磁盘中。并供给了写入成果的回来值。
  • apply 办法会将更改先提交到内存中,然后以异步的形式来写入到磁盘中。而且不会提示写入成果。
  • 请注意,当两个 Editor 同时修正时,最终一次调用 apply 的会成功。
  • 假如先履行了 apply ,在 apply 没有写入磁盘前,调用 commit ,commit 办法会堵塞,直到一切异步都履行完结。
读取数据
context.getSharedPreferences("name", 0).getString("key", defaultValue)
数据文件

SharedPreferences 会将键值对数据写入磁盘中的 XML 文件,文件目录为:

/data/data/应用包名/shared_prefs/文件名.xml

文件名称来自 Context#getSharedPreferences(name, mode) 的第一个参数 name 字符串,其 XML 结构是:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="TestKey">this is sp manager</string>
</map>

原理剖析

Context#getSharedPreferences

运用 SharedPreferences 的办法是经过 Context#getSharedPreferences(name, mode) 来获取一个 SharedPreferences 目标,经过这个目标去读取或是更改数据。经过这个办法会创立一个名称为参数 name 的 XML 文件,name 不同就会创立不同的文件,所以这儿建议大局运用的 SharedPreferences 运用一个大局的 name 。

读数据时,只需求传入两个字符串,name 和 key,下图是一个整体的查找流程:

SharedPreferences 的底层原理

Context#getSharedPreferences(name, mode) 的源码如下:

    public SharedPreferences getSharedPreferences(String name, int mode) {
        // KITKAT 前的版别存在 app 传入了一个 null ,将其指向为 null.xml 文件
        if (mPackageInfo.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);
    }

读数据时须确保同步,对 mSharedPrefsPaths 特点进行创立,是一个 ArrayMap ,然后从 mSharedPrefsPaths 中查找到 name 对应的 File ,假如没有 File 会去创立 File 目标,以 name.xml 命名。并将其保存到 mSharedPrefsPaths 中。

		// 这儿证明了 XML 文件的命名是根据 name 字符串确认的
		public File getSharedPreferencesPath(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }
    private File makeFilename(File base, String name) {
        if (name.indexOf(File.separatorChar) < 0) {
            final File res = new File(base, name);
            BlockGuard.getVmPolicy().onPathAccess(res.getPath());
            return res;
        }
        throw new IllegalArgumentException(
                "File " + name + " contains a path separator");
    }

这儿的 mSharedPrefsPaths 是保存在 ContextImpl 中的成员特点,这个目标在 ActivityThread 创立 Application 时,会创立 ContextImpl 作为 Context ,并创立 Application ,所以对于一个应用程序,mSharedPrefsPaths 是单例存在的。

最终调用 getSharedPreferences(File file, int mode) , 其源码如下:

    @Override
    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) {
                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;
            }
        }
      	// 多进程的情况下, 该形式已抛弃
        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;
    }

首要先加锁确保读数据时的同步,经过 getSharedPreferencesCacheLocked() 读取 sSharedPrefsCache ,sSharedPrefsCache 缓存的是包名和 SharedPreferencesImpl 的映射联系,这儿从 sSharedPrefsCache 读取到 SharedPreferencesImpl 目标后回来的是保存文件和 SharedPreferencesImpl 的映射联系的 ArrayMap。

    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 的映射联系集合后,读取文件对应的 SharedPreferencesImpl 目标,假如没有目标则去创立 SharedPreferencesImpl :

            sp = cache.get(file);
            // SharedPreferencesImpl 不存在,去创立
            if (sp == null) {
                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;
            }

创立 SharedPreferencesImpl 目标,最重要的是履行 startLoadFromDisk 办法:

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
    }
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

在这儿发动了一个新线程履行 loadFromDisk() ,后者将会从 XML 文件中读取数据并转化成 Map<String, Object> 类型的目标 :

    private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) return;
            // 清理备份文件
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try { // 读文件,保存到 map
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } 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;
            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();
            }
        }
    }

读取到数据后保存到了 SharedPreferencesImpl 的成员特点 mMap(Map<String, Object>)中。

Context#getSharedPreferences 最终回来的是一个 SharedPreferencesImpl 目标,这个目标的 mMap 中保存了从 XML 中读取到的数据。

注意因为SharedPreference内容都会在内存里存一份,所以不要运用SharedPreference保存较大的内容,避免不必要的内存浪费。

Activity#getPreferences

另一种运用办法是在 Activity 中供给了 getPreferences() 办法,这个办法本质上调用的仍是 Context#getSharedPreferences(name, mode) , 只不过封装了 name 参数的内容:

public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
}

回来的是不含包名的当前类名字符串:

public String getLocalClassName() {
    final String pkg = getPackageName();
    final String cls = mComponent.getClassName();
    int packageLen = pkg.length();
    if (!cls.startsWith(pkg) || cls.length() <= packageLen
            || cls.charAt(packageLen) != '.') {
        return cls;
    }
    return cls.substring(packageLen+1);
}

SharedPreferences 读操作

public interface SharedPreferences {
    @Nullable
    String getString(String key, @Nullable String defValue);
    //...
}

SharedPreferences 读取数据的 get 系列办法在 SharedPreferences 接口中直接界说,其完成在 SharedPreferencesImpl 类中:

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

这儿的本质是读取 mMap 特点中的数据,并将其类型强制转化。但在真正读取数据前,会处理同步:

    @GuardedBy("mLock")
    private void awaitLoadedLocked() {
        if (!mLoaded) {
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

这儿是一个自旋等候机制,假如 mLoaded 不为 true 则会进入线程等候状况。 mLoaded 在 loadFromDisk() 办法中读 XML 文件数据时设置成了 true 。

SharedPreferences 写操作

SharedPreferences 接口中对于写操作都界说在了 Editor 接口中:

public interface Editor {
    Editor putString(String key, @Nullable String value);
    Editor putStringSet(String key, @Nullable Set<String> values);
    Editor putInt(String key, int value);
    Editor putLong(String key, long value);
    Editor putFloat(String key, float value);
    Editor putBoolean(String key, boolean value);
    Editor remove(String key);
    Editor clear();
    boolean commit();
    void apply();
}

这样的意图是经过封装成链式调用,更加方便地批量操作数据。

Editor 的完成在 SharedPreferencesImpl 中:

public final class EditorImpl implements Editor {
    private final Object mEditorLock = new Object();
    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>();
    @GuardedBy("mEditorLock")
    private boolean mClear = false;
    // ...
}

比较值得注意的是,它内部有一个 HashMap 类型的 mModified 特点,临时保存数据,以 putString 办法为例:

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

putString 办法会把键值对数据保存的 mModified 中,最终在调用 apply()commit() 时,才将 mModified 中的数据写入到 XML 文件中。

所以,在数据没有写入到磁盘前(在 apply()commit() 没有开端履行写入时),调用 SharedPreferences 的 get 系列办法读数据是读不到的;但 apply()commit() 一旦开端履行写入操作,就会抢占 mLock 锁,这个时候调用 SharedPreferences 的 get 系列办法会被堵塞,等候写入操作完结后释放锁。

commitToMemory

不管是 apply() 仍是 commit() ,在履行写入磁盘前,都需求履行提交到内存的操作, 经过 commitToMemory() 办法进行:

    private MemoryCommitResult commitToMemory() {
        long memoryStateGeneration;
        boolean keysCleared = false;
        List<String> keysModified = null;
        Set<OnSharedPreferenceChangeListener> listeners = null;
        Map<String, Object> mapToWriteToDisk;
        synchronized (SharedPreferencesImpl.this.mLock) {
            if (mDiskWritesInFlight > 0) {
                mMap = new HashMap<String, Object>(mMap);
            }
            mapToWriteToDisk = mMap;
            mDiskWritesInFlight++;
            boolean hasListeners = mListeners.size() > 0;
            if (hasListeners) {
                keysModified = new ArrayList<String>();
                listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
            }
            synchronized (mEditorLock) {
                boolean changesMade = false;
              	// 调用了 Editor 的 clear 办法,清理数据
                if (mClear) {
                    if (!mapToWriteToDisk.isEmpty()) {
                        changesMade = true;
                        mapToWriteToDisk.clear();
                    }
                    keysCleared = true;
                    mClear = false;
                }
                for (Map.Entry<String, Object> e : mModified.entrySet()) {
                    String k = e.getKey();
                    Object v = e.getValue();
                    // 数据不存在或调用了 remove(v == this)
                    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);
                    }
                    changesMade = true;
                    if (hasListeners) {
                        keysModified.add(k);
                    }
                }
                mModified.clear();
                if (changesMade) {
                    mCurrentMemoryStateGeneration++;
                }
                memoryStateGeneration = mCurrentMemoryStateGeneration;
            }
        }
        return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
                listeners, mapToWriteToDisk);
    }

在这个办法中,又创立了一个临时的 Map 目标 mapToWriteToDisk ,mapToWriteToDisk 读取了一切的 mMap 并写入了 mModified 中的更新,最终将处理更新后的 mapToWriteToDisk 保存到了 MemoryCommitResult 目标中。

MemoryCommitResult 目标后续会在调用 enqueueDiskWrite(MemoryCommitResult, Runnable) 办法时,将数据写入到磁盘文件中:

    private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
      	// 是否同步 
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--; // 更新计数
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run(); 
                    }
                }
            };
      	// 同步履行,需求的额外操作
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1; 
            }
            if (wasEmpty) {
                writeToDiskRunnable.run(); // 直接同步履行 runnable ,并回来
                return;
            }
        }
      	// 异步情况经过 QueuedWork 履行
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

这儿的 writeToFile(mcr, isFromSyncCommit) 履即将数据写入文件的操作。

commit

commit 是同步操作,enqueueDiskWrite(mcr, null) 的第二个参数传了 null ,而且,会发动一个 CountDownLatch 进行等候,履行完结后经过 MemoryCommitResult 的 writeToDiskResult 回来写入成果。

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

apply

apply 是异步操作,经过 QueuedWork 履行异步使命,并处理同步。该办法没有回来值。

    @Override
    public void apply() {
        final long startTime = System.currentTimeMillis();
        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 something
                    }
                }
            };
        QueuedWork.addFinisher(awaitCommit);
        Runnable postWriteRunnable = new Runnable() {
                @Override
                public void run() {
                    awaitCommit.run();
                    QueuedWork.removeFinisher(awaitCommit);
                }
            };
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
	// 能够在它抵达磁盘之前通知侦听器,因为侦听器应该一直回来相同的 SharedPreferences 实例,该实例的更改反映在内存中。
        notifyListeners(mcr);
    }

多进程

尽管 Context.MODE_MULTI_PROCESS 已弃用,但上面的 Context#getSharedPreferences 办法仍有想过逻辑。MODE_MULTI_PROCESS 确保多进程数据正确的办法是每次获取都会尝试去重新 reload 文件,这样在多进程形式下是不可靠的:

  1. 运用 MODE_MULTI_PROCESS 时,不能保存 SharedPreferences 变量,有必要每次都从context.getSharedPreferences 获取。假如保存变量会无法触发reload,有或许两个进程数据不同步。
  2. 加载磁盘数据是耗时的,而且其他操作会等候该锁。这意味着许多时候获取 SharedPreferences 数据都不得不从文件再读一遍,大大降低了内存缓存的效果。文件读写耗时也影响了功能。
  3. 修正数据时得用 commit ,确保修正时写入了文件,这样其他进程才干经过文件大小或修正时刻感知到。

所以在多进程形式下,并不合适运用 SharePreferences,应该选择更好的进程间通讯计划。

一种多线程的 SharePreferences 完成思路是:经过 ContentProvider,当 SharePreferences 调用进程与 ContentProvider 为同一进程时,走 SharePreferences 的完成,当进程不同时,再经过 ContentProvider 完成。

总结

SharedPreferences 需求要点重视的知识包括:

  1. apply 和 commit 办法的区别。
  2. Editor 运用链式调用,便于批量操作。
  3. SharedPreferences 运用了同步锁和线程创立,功能上必定有损耗。
  4. 多个 Map 直接保存了不同的映射联系:
    • mSharedPrefsPaths 保存 name 和 file 的映射联系。
    • sSharedPrefsCache 保存 file 和 SharedPreferencesImpl 的映射联系。
    • SharedPreferencesImpl 的 mMap 保存 key 和 value 的联系。
  5. 多个 Map 保存数据:
    • mMap 保存从 XML 中加载到内存的数据。
    • mModified 保存修正的数据。
    • mapToWriteToDisk 兼并了 mMap 和 mModified ,准备写入到磁盘。
  6. 不适用于多线程。