【Android】深入解析 SharedPreferences 源码

SharedPreference是一个 Android 开发自带的适合保存轻量级数据的key-value
存储库,它使用了xml的方式来存储数据,比如可以使用它保存一些如用户 登录信息等轻量级数据。

官方文档:
https://developer.android.google.cn/training/data-storage/shared-preferences

注:本文以 API 30 的 SharedPreferences 源码进行解析。


一、获取 SharedPreferences

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

根据名称获取 SharedPreferences

不论是在Activity中调用getPreferences方法还是调用ContextgetSharedPreferences方法,最终都是调用到了ContextImplgetSharedPreferences(String name, int mode)方法。

注:ContextImpl 是 Context 的实现类。

源码位置:
/sdk/sources/android-30/android/app/ContextImpl.java

我们先看看它的代码:

public SharedPreferences getSharedPreferences(String name, int mode) {
    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) {
            // 调用 getSharedPreferencesPath 方法构建出 name 对应的 File
            file = getSharedPreferencesPath(name);
            // 将 file 放入 map
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);
}

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

mSharedPrefsPaths的声明如下:

private ArrayMap<String, File> mSharedPrefsPaths;

可以看到它是一个以namekeyname对应的Filevalue
HashMap

获取 SharedPreferences 名称对应的 File 对象

先看看是如何构建出name对应的File的:

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

可以看到,调用makeFilename方法来创建一个名为name.xmlFile

makeFilename中仅仅是做了一些判断,之后new出了这个File对象并返回。

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

可以看到,SharedPreference确实是使用xml来保存其中的key-value数据的。

根据创建的 File 对象获取 SharedPreference

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

public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        // 调用 getSharedPreferencesCacheLocked 来获取到一个 ArrayMap
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        // 从这个 Map 中尝试获取到对应的 SharedPreferencesImpl 实现类
        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");
                }
            }

            // 当获取不到对应 SharedPreferencesImpl 时,再创建一个对应的 SharedPreferencesImpl,并将其加入这个 ArrayMap 中
            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) {
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

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

缓存机制

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

显然,这里有个全局的sSharedPrefsCache,它是一个ArrayMap类型的Map。根据PackageName来保存不同的SharedPreference缓存Map的,通过这样的方式,就保证了不同PackageName中相同nameSharedPreference从缓存中拿到的数据是不同的。

SharedPreferencesImpl 的创建

cache中找不到对应的SharedPreferencesImpl时,就会new出一个 SharedPreferencesImpl,看看它的构造函数:

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    // 调用 makeBackupFile 来进行备份文件的创建
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    // 调用 startLoadFromDisk 来开始从 Disk 载入信息
    startLoadFromDisk();
}

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) {
        // 如果已经加载过,则不再进行加载
        if (mLoaded) {
            return;
        }

        // 判断是否存在备份文件,若存在则将备份文件直接修改为数据文件 name.xml
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }

    Map<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                // 通过 XmlUtils 将 xml 中的数据读取为一个 Map
                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) {

    } catch (Throwable t) {
        thrown = t;
    }

    // 进行一些收尾处理
    synchronized (mLock) {
        // 将 mLoaded 置为 true
        mLoaded = true;
        mThrowable = thrown;

        try {
            if (thrown == null) {
                // 对 mMap 进行判空处理,以保证在 xml 没有数据的情况下其仍不为 null
                if (map != null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            // 释放这个读取的锁,表示读取成功
            mLock.notifyAll();
        }
    }
}

二、编辑 SharedPreferences

真正对SharedPreferences的操作其实都是在Editor中的,它其实是一个接口, 具体的实现类为EditorImpl

获取 Editor

看到SharedPreferencesImpledit方法:

public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }

    return new EditorImpl();
}

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

等待读取机制

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

awaitLoadedLocked方法:

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

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

EditorImpl

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

下面列出一些EditorImpl对外提供的修改接口,其实都是在对mModified这个Map进行修改:

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

public Editor putStringSet(String key, @Nullable Set<String> values) {
    ...
}

public Editor putInt(String key, int value) {
    ...
}

public Editor putLong(String key, long value) {
    ...
}

public Editor putFloat(String key, float value) {
    ...
}

public Editor putBoolean(String key, boolean value) {
    ...
}

public Editor remove(String key) {
    ...
}

public Editor clear() {
    ...
}

提交 SharedPreferences

SharedPreferences的提交有两种方式:applycommit

apply

public void apply() {
    final long startTime = System.currentTimeMillis();

    // 调用 commitToMemory 方法,它内部就是将原先读取进来的 mMap 与刚刚修改过的 mModified 进行合并,并存储于返回的 MemoryCommitResult mcr 中
    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.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    // 调用 enqueueDiskWrite 方法,传入了之前构造的 Runnable 对象,这里的目的是进行一个异步的写操作。
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    notifyListeners(mcr);
}

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

commit

public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }

    // 和 apply 方法一样,调用 commitToMemory 方法进行了合并
    MemoryCommitResult mcr = commitToMemory();

    // 和 apply 方法一样,调用 enqueueDiskWrite 方法,不过传入的第二个不再是 Runnable 方法,
    // 通过源码注释 "if null, then we're allowed to do this disk write on the main thread",如果 enqueueDiskWrite 方法传入的第二个参数为 null,则会在当前线程执行写入操作
    SharedPreferencesImpl.this.enqueueDiskWrite(
        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;
}

也就是说,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;

            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();

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

可以看到,将mMap的数据与mModified的数据进行了整合,之后将mModified重新清空。最后将合并的数据放入了MemoryCommitResult中。

写入数据至硬盘

enqueueDiskWrite方法:

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    // 若第二个 Runnable 为 null 的话,则会将 isFromSyncCommit 置为 true,也就是写入会是一个同步的过程
    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();
            return;
        }
    }

    // 构造一个 Runnable 来提供给 QueueWork 进行异步写入
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

QueueWork类内部维护了一个single线程池,这样可以达到我们异步写入的目的。而writeToFile方法中其实就是又调用了之前的XmlUtils来进行xml的写入。


总结

SharedPreferences其实就是一个用使用xml进行保存的key-value存储库。在获取SharedPreferences时会进行数据的加载,将name对应的xml文件以Map的形式读入到内存。

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

SharedPreferences的提交分为两种,applycommit,它们的特性如下:

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

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


© 2020 Tyhoo Wu, All rights reserved.

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