Android 轻量级缓存 SharedPreference 源码解析

本文是独立解析源码的第二篇,SharedPreference 是一个 Android 开发自带的适合保存轻量级数据的 K-V 存储库,它使用了 XML 的方式来存储数据,比如我就经常用它保存一些如用户登录信息等轻量级数据。那么今天就让我们来分析一下它的源码,研究一下其内部实现。

获取SharedPreferences

我们在使用 SharedPreferences 时首先是需要获取到这个 SharedPreferences 的,因此我们首先从 SharedPreferences 的获取入手,来分析其源码。

根据名称获取 SP

不论是在 Activity 中调用 getPreferences() 方法还是调用 Context 的 getSharedPreferences 方法,最终都是调用到了 ContextImpl 的 getSharedPreferences(String name, int mode) 方法。我们先看看它的代码:

@Override
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 (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);
}

可以看到,它首先对 Android 4.4 以下的设备做了特殊处理,之后将对 mSharedPrefsPaths 的操作加了锁。mSharedPrefsPaths 的声明如下:

private ArrayMap mSharedPrefsPaths;

可以看到它是一个以 name 为 key,name 对应的 File 为 value 的 HashMap。首先调用了 getSharedPreferencesPath 方法构建出了 name 对应的 File,将其放入 map 后再调用了 getSharedPreferences(File file, int mode) 方法。

获取 SP 名称对应的 File 对象

我们先看看是如何构建出 name 对应的 File 的。

@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

可以看到,调用了 makeFilename 方法来创建一个名为 name.xml 的 File。makeFilename 中仅仅是做了一些判断,之后 new 出了这个 File 对象并返回。

可以看到,SharedPreference 确实是使用 xml 来保存其中的 K-V 数据的,而具体存储的路径我们这里就不再关心了,有兴趣的可以点进去看看。

根据创建的 File 对象获取 SP

我们接着看到获取到 File 并放入 Map 后调用的 getSharedPreferences(file, mode) 方法:

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap cache = getSharedPreferencesCacheLocked();  // 1
        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); // 2
            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;
}

首先可以看到注释 1 处,这里调用了 getSharedPreferencesCacheLocked 来获取到了一个 ArrayMap,之后再从这个 Map 中尝试获取到对应的 SharedPreferencesImpl 实现类(简称 SPI)。

之后看到注释 2 处,当获取不到对应 SPI 时,再创建一个对应的 SPI,并将其加入这个 ArrayMap 中。

这里很明显是一个缓存机制的实现,以加快之后获取 SP 的速度,同时可以发现,SP 其实只是一个接口,而 SPI 才是其具体的实现类。

缓存机制

那么我们先来看看其缓存机制,进入 getSharedPreferencesCacheLocked 方法:

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

显然,这里有个全局的 ArrayMap:sSharedPrefsCache。

它是一个 ArrayMap> 类型的 Map,而从代码中可以看出,它是根据 PackageName 来保存不同的 SP 缓存 Map 的,通过这样的方式,就保证了不同 PackageName 中相同 name 的 SP 从缓存中0拿到的数据是不同的。

SharedPreferencesImpl

那么终于到了我们 SPI 的创建了,在 cache 中找不到对应的 SPI 时,就会 new 出一个 SPI,看看它的构造函数:

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file); // 1
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();    // 2
}

可以看到注释 1 处它调用了 makeBackupFile 来进行备份文件的创建。

之后在注释 2 处则调用了 startLoadFromDisk 来开始从 Disk 载入信息。

首先我们看看 makeBackupFile 方法:

static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");
}

很简单,返回了一个同目录下的后缀名为 .bak 的同名文件对象。

从 Disk 加载数据

接着,我们看看 startLoadFromDisk 方法:

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

可以看到,首先在加锁的情况下对 mLoaded 进行了修改,之后则开了个名为「SharedPreferencesImpl-load」的线程来加载数据。

我们看到 loadFromDisk 方法:

private void loadFromDisk() {
    synchronized (mLock) {  // 1
        if (mLoaded) {
            return;
        }
        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 map = null;
    StructStat stat = null;
    try { // 2
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16*1024);
                map = XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        /* ignore */
    }
    synchronized (mLock) {  // 3
        mLoaded = true;
        if (map != null) {
            mMap = map;
            mStatTimestamp = stat.st_mtim;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<>();
        }
        mLock.notifyAll();
    }
}

代码比较长,我们慢慢分析

首先在注释 1 处,如果已经加载过,则不再进行加载,之后又开始判断是否存在备份文件,若存在则直接将备份文件直接修改为数据文件 ${name}.xml。

之后在注释 2 处,通过 XmlUtils 将 xml 中的数据读取为一个 Map。由于本文主要是对 SP 的大致流程的解读,因此关于 XML 文件的具体读取部分,有兴趣的读者可以自己进入源码研究。

之后在注释 3 处,进行了一些收尾处理,将 mLoaded 置为 true,并对 mMap 进行了判空处理,以保证在 xml 没有数据的情况下其仍不为 null,最后释放了这个读取的锁,表示读取成功。

编辑 SharedPreferences

我们都知道,真正对 SP 的操作其实都是在 Editor 中的,它其实是一个接口,具体的实现类为 EditorImpl。让我们先看看 Editor 的获取:

获取 Editor

看到 SharedPreferencesImpl 的 edit 方法:

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 (mLock) {
        awaitLoadedLocked();
    }
    return new EditorImpl();
}

可以看到,这里首先先调用了 awaitLoadedLocked() 方法来等待读取的完成,当读取完成后才会真正创建并返回 EditorImpl 对象。

等待读取机制

由于读取过程是一个异步的过程,很有可能导致读取还没结束,我们就开始编辑,因此这里用到了一个 awaitLoadedLocked 方法来阻塞线程,直到读取过程完成,下面我们可以先看看 awaitLoadedLocked 方法:

private void awaitLoadedLocked() {
    if (!mLoaded) {
        // Raise an explicit StrictMode onReadFromDisk for th
        // 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) {
        }
    }
}

可以看到,这里会阻塞直到 mLoaded 为 true,这样就保证了该方法后的方法都会在读取操作进行后执行。

EditorImpl

前面我们提到了 Edit 的真正实现类是 EditorImpl,它其实是 SPI 的一个内部类。它内部维护了一个Map: mModified,通过 mModified 来存放对 SP 进行的操作,此时还不会提交到 SPI 中的 mMap,我们做的操作都是在改变 mModified。

下面列出一些 EditorImpl 对外提供的修改接口,其实都是在对 mModified 这个 Map 进行修改,具体代码就不再讲解,比较简单:

public Editor putString(String key, @Nullable String val
    synchronized (mLock) {
        mModified.put(key, value);
        return this;
    }
}
public Editor putStringSet(String key, @Nullable Set

提交 SharedPreferences

提交本来可以放到编辑中的,但因为它才是重头戏,因此我们单独拎出来讲一下。

我们都知道 SP 的提交有两种方式——apply 和 commit。下面我们来分别分析:

apply

public void apply() {
    final long startTime = System.currentTimeMillis();
    final MemoryCommitResult mcr = commitToMemory();    // 1
    final Runnable awaitCommit = new Runnable() {
            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() {
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);    // 2
    // 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);
}

首先看到注释 1 处,调用了 commitToMemory 方法,它内部就是将原先读取进来的 mMap 与刚刚修改过的 mModified 进行合并,并存储于返回的 MemoryCommitResult mcr中

而在注释 2 处,调用了 enqueueDiskWrite 方法,传入了之前构造的 Runnable 对象,这里的目的是进行一个异步的写操作。

前面提到的两个方法,我们放到后面分析。

也就是说,apply 方法会将数据先提交到内存,再开启一个异步过程来将数据写入硬盘

commit

public boolean commit() {
    long startTime = 0;
    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }
    MemoryCommitResult mcr = commitToMemory();  // 1
    SharedPreferencesImpl.this.enqueueDiskWrite(    // 2
        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);
    return mcr.writeToDiskResult;
}

看到注释 1 处,可以发现,同样调用了 commitToMemory 方法进行了合并。

之后看到 2 处,同样调用了 enqueueDiskWrite 方法,不过传入的第二个不再是 Runnable 方法。这里提一下,如果 enqueueDiskWrite 方法传入的第二个参数为 null,则会在当前线程执行写入操作。

也就是说,commit 方法会将数据先提交到内存,但之后则是一个同步的过程写入硬盘

同步数据至内存

下面我们来看看两个方法中都调用了的 commitToMemory 的具体实现:

private MemoryCommitResult commitToMemory() {
        ...
        synchronized (mLock) {
            boolean changesMade = false;
            if (mClear) {
                if (!mMap.isEmpty()) {
                    changesMade = true;
                    mMap.clear();
                }
                mClear = false;
            }
            for (Map.Entry e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                if (v == this || v == null) {
                    if (!mMap.containsKey(k)) {
                        continue;
                    }
                    mMap.remove(k);
                } else {
                    if (mMap.containsKey(k)) {
                        Object existingValue = mMap.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mMap.put(k, v);
                }
                changesMade = true;
                if (hasListeners) {
                    keysModified.add(k);
                }
            }
            mModified.clear();
            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }
            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}

可以看到,具体的代码就如同我们之前所说的一样,将 mMap 的数据与 mModified 的数据进行了整合,之后将 mModified 重新清空。最后将合并的数据放入了 MemoryCommitResult 中。

写入数据至硬盘

我们同样看到 apply 和 commit 都调用了的方法 enqueueDiskWrite:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);   // 1
    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };
    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();  // 2 
            return;
        }
    }
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

可以看到 1 处,若第二个 Runnable 为 null 的话,则会将 isFromSyncCommit 置为 true,也就是写入会是一个同步的过程。之后在注释 2 处便进行了同步写入。否则会构造一个 Runnable 来提供给 QueueWork 进行异步写入。

QueueWork 类内部维护了一个 single 线程池,这样可以达到我们异步写入的目的。

而 writeToFile 方法中其实就是又调用了之前的 XmlUtils 来进行 XML 的写入。

总结

SharedPreferences 其实就是一个用使用 XML 进行保存的 K-V 存储库。

在获取 SP 时会进行数据的加载,将 name 对应的 xml 文件以 Map 的形式读入到内存。

而 SP 的编辑操作其实都是在 Editor 内实现,它内部维护了一个新的 Map,所有的编辑操作其实都是在操作这个 Map,只有提交时才会与之前读取的数据进行合并。

其提交分为两种,apply 和 commit,它们的特性如下

  • apply

    • 会将数据先提交到内存,再开启一个异步过程来将数据写入硬盘
    • 返回值时可能写入操作还没有结束
    • 写入失败时不会有任何提示
  • commit

    • 会将数据先提交到内存,但之后则是一个同步的过程写入硬盘

    • 写入操作结束后才会返回值

    • 写入失败会有提示

因此,当我们对写入的结果不那么关心的情况下,可以使用 apply 进行异步写入,而当我们对写入结果十分关心且提交后有后续操作的话最好使用 commit 来进行同步写入。

你可能感兴趣的:(Android 轻量级缓存 SharedPreference 源码解析)