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<String, File> 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<File, SharedPreferencesImpl> 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<File, SharedPreferencesImpl>,之后再从这个 Map 中尝试获取到对应的 SharedPreferencesImpl 实现类(简称 SPI)。

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

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

缓存机制

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

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;
}

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

它是一个 ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> 类型的 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<String, Object>: 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<Str
    synchronized (mLock) {
        mModified.put(key,
                (values == null) ? null : new HashSet<St
        return this;
    }
}
public Editor putInt(String key, int value) {
    synchronized (mLock) {
        mModified.put(key, value);
        return this;
    }
}
public Editor putLong(String key, long value) {
    synchronized (mLock) {
        mModified.put(key, value);
        return this;
    }
}
public Editor putFloat(String key, float value) {
    synchronized (mLock) {
        mModified.put(key, value);
        return this;
    }
}
public Editor putBoolean(String key, boolean value) {
    synchronized (mLock) {
        mModified.put(key, value);
        return this;
    }
}
public Editor remove(String key) {
    synchronized (mLock) {
        mModified.put(key, this);
        return this;
    }
}
public Editor clear() {
    synchronized (mLock) {
        mClear = true;
        return this;
    }
}

提交 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<String, Object> 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 来进行同步写入。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容