SharedPreferences 的底层原理

936 阅读7分钟

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,下图是一个整体的查找流程:

image-20230310164650464.png

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. 不适用于多线程。