SharedPreferences 源码阅读

1,089 阅读4分钟

1、前言

SharedPreferences是 Android 中比较常用的存储方法,它可以用来存储一些比较小的键值对集合。内存采用HashMap来存储,文件采用xml格式来存储;

2、源码

2.1 SharedPreferences对象获取

	context.getSharedPreferences(sharedName, Context.MODE_PRIVATE)

通过Context实例来获取,Context实例是ComtextImpl对象;流程和下面两个集合有关联

private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
private ArrayMap<String, File> mSharedPrefsPaths;

流程大致如下:

  1. 若sp名字对应的文件在mSharedPrefsPaths,不存在,则创建,并存储;文件目录:应用目录下/shared_prefs/name.xml
  2. 若sp文件对应的SharedPreferencesImpl在sSharedPrefsCache中不存在,则创建,并缓存
  3. 返回新创建实例或者缓存中实例

2.2 SharedPreferences存储

其存储又分为内存存储和本地文件存储;相关变量如下:

private final File mFile;
private final File mBackupFile;
private Map<String, Object> mMap;

2.2.1 xml文件解析

在SharedPreferencesImpl进行实例化时通过调用startLoadFromDisk方法,从文件中通过XmlPullParser进行解析,解析值对存储在mMap中;

synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
   .................................
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                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);
            }
        }
    }
   ...........................
  1. 读取采用了锁机制,保证数据安全
  2. 采用文件备份,进行回退操作;这也可能存在丢失
  3. 通过XmlUtils.readMapXml工具方法进行读写;XmlUtils是不可见工具类

2.2.2 内存读写

读方法是通过SharedPreferencesImpl实例的getXXX系列方法;写方法是通过EditorImpl实例putXXX系列方法;

读数据如下面代码:首先要等待已经从文件解析完毕,然后从集合mMap冲获取结果

    synchronized (mLock) {
        awaitLoadedLocked();
        Long v = (Long)mMap.get(key);
        return v != null ? v : defValue;
    }

写数据时:mModified为EditorImpl中HashMap实例,用来存储修改值对

    synchronized (mEditorLock) {
       mModified.put(key, value);
       return this;
   }
   

2.2.2 提交到内存

有两种方法均可提交到内存,那就是apply、commit方法;这两个方法现在推荐使用applay方法

commit方法

public boolean commit() {
        long startTime = 0;

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

        MemoryCommitResult mcr = commitToMemory();

        SharedPreferencesImpl.this.enqueueDiskWrite(
            mcr, null);
        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;
    }

apply方法

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

        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
        notifyListeners(mcr);
    }

区别就在mcr.writtenToDiskLatch.await()这句执行的环境,这个使用CountDownLatch锁机制,也就apply必须等待写任务执行完毕,才可以继续,而apply方法异步线程等待

commitToMemory方法:这个方法是根据EditorImpl中mModified内容来修改SharedPreferencesImpl中mMap集合内容;达到当前进程不死,就可以读到写的内容;但进程重新启动,可不一定;因为写入文件不一定操作成功

enqueueDiskWrite方法:其中调用writeToFile方法进行写入

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    .....................................
    if (fileExists) {
       ................................................
        if (!needsWrite) {
            mcr.setDiskWriteResult(false, true);
            return;
        }

        ............................
    try {
        FileOutputStream str = createFileOutputStream(mFile);
       .............................
        if (str == null) {
            mcr.setDiskWriteResult(false, false);
            return;
        }
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        ........................................
        mcr.setDiskWriteResult(true, true);
        ..........................
        return;
    }
    ..............................
    mcr.setDiskWriteResult(false, false);
}

主要方法内容大致如上:

  1. 使用工具XmlUtils.writeMapXml方法写回
  2. mcr.setDiskWriteResult中进行CountDownLatch锁释放,使apply和commit方法中同步或者异步代码继续执行

3、小结

  1. xml文件存储,多了很多无用信息
  2. 使用锁机制保证同步,同样也增加读写的成本
  3. 文件内容会载入内存,并一直存在;这可能造成内存溢出,也存在读写慢;因此,不可存入大量数据、每个sp中不要存过多的值对

其实sp的存储思想不难,其难点就是谁来使用都稳定;但这个稳定在中小型项目或者有些场景很多是不需要考虑的;我们可以从以下几个方面进行优化:

  1. 改变sp存储格式、减少文件大小
  2. 较少sp文件读写次数
  3. 分为需要线程安全、非线程安全场景;较少锁竞争造成的资源开销

我根据sp原理写了一个简单、高效的存储框架:这个框架未进行线程、进程处理,个人认为在中小项目中基本够用了

技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给与关注和点赞;如果文章存在错误,也请多多指教!