💪
AndroidCollect
  • 写在前面
  • 计算机基础
    • 计算机组成原理
    • 算法
      • 查找
        • 二分查找
      • 排序
        • 简单排序
        • 高级排序
        • 特殊排序
      • 海量数据
      • 思想
        • 贪心
        • 分治
        • 动态规划
        • 回溯
      • 哈希算法
    • 数据结构
      • 队列
        • 知识点
        • 相关题目
          • 用两个栈实现队列
          • 实现循环队列
          • 用链表实现队列
          • 用数组实现队列
      • 栈
        • 相关算法题目
          • 用链表实现栈
          • 用数组实现栈
      • 链表
        • 知识点梳理
        • 相关算法题目
          • 删除倒数第n个结点
          • 合并两个有序链表
          • 检测单链表是否有环
          • 获取中间结点
          • 反转链表
      • 跳表
      • 哈希表
      • 树
        • 二叉树
        • 二叉查找树
        • AVL 树
        • Trie 树
        • 红黑树
      • 堆
        • 存储
        • 堆的应用
      • 图
    • 网络
      • 应用层协议
        • DNS
        • HTTP
        • HTTPS
      • 传输层协议
        • TCP
        • UDP
      • 输入网址后发生了什么
    • 操作系统
      • 内存
    • 数据库
  • 软件工程
    • 编程思想
    • 设计模式
      • 状态模式
      • 装饰器模式
      • 代理模式
      • 责任链模式
      • 建造者模式
      • 单例模式
      • 观察者模式
  • Java
    • 基础
    • 异常
    • 并发编程
      • ThreadLocal
      • 线程池
      • 理解 volatile
      • AbstractQueuedSynchronizer
    • 集合
      • LinkedHashMap 源码
      • HashMap 源码
    • 注解
    • 反射
      • JDK 动态代理
    • JVM
      • 自动内存管理机制
      • Class 文件格式
      • 类加载机制
      • Java 内存模型(JMM)
      • 字节码指令
      • HotSpot 虚拟机实现细节
    • 源码与原理
    • 各版本主要特性
  • Android
    • 基础组件
      • Context
      • Activity
        • 生命周期
        • 启动模式与任务栈
        • 启动流程
      • Service
      • ContentProvider
      • BroadcastReceiver
      • Fragment
      • View
        • 常用控件问题总结
          • RecyclerView
          • ViewPager2
        • CoordinatorLayout
        • SurfaceView
        • 事件分发
        • 绘制流程
        • 自定义 View
        • Window
    • 数据存储
      • 存储结构
      • Sqlite
      • 序列化
      • SharedPreferences
    • 资源
      • 图片加载
    • 动画
      • 属性动画
    • 线程和进程
      • Binder 机制
      • 跨进程通信
        • AIDL
    • 内部原理
      • 消息循环机制
      • Binder
      • Window
      • SparseArray
      • ArrayMap
      • RecyclerView
      • App 启动流程
    • 性能优化
      • 内存
        • 内存使用优化
        • 内存泄漏
      • 启动优化
      • 缩减包大小
      • 布局优化
      • ANR
    • 打包构建
      • dex 文件
      • APK 打包流程
      • APK 签名流程
    • 架构
      • 运行时
      • Android 系统架构
      • 应用项目架构
    • 开源框架源码或原理
      • RxJava
        • 使用笔记
        • 源码解析
      • Retrofit
      • ButterKnife
      • BlockCanary
      • LeakCanary
      • OkHttp
      • 图片加载
        • Glide
        • Picasso
    • 碎片化处理
      • 屏幕适配
    • 黑科技
      • 热修复
    • Jetpack
      • Lifecycle
      • Room
      • WorkManager
    • 新动态
      • AndroidX
      • 各系统版本特性
  • 开发工具
    • 正则表达式
    • ADB
    • Git
  • Kotlin
  • Flutter
  • 关于作者
  • 致谢
由 GitBook 提供支持
在本页
  • getSharedPreferences
  • 创建 SharedPreferences
  • 读取值
  • 写入值
  • 创建 Editor
  • putXxx
  • EditorImpl#apply()
  • EditorImpl#commitToMemory()
  • SharedPreferencesImpl#enqueDiskWrite
  • SharedPreferencesImpl#writeToFile()
  • EditorImpl#commit()
  • QueuedWork
  • 静态属性
  • QueuedWork#queue()
  • getHandler()
  • QueuedWorkHandler
  • processPendingWork
  • waitToFinish
  • 建议
  • 相关问题
  • commit 和 apply 区别?
  • 相关链接

这有帮助吗?

  1. Android
  2. 数据存储

SharedPreferences

源码解析

上一页序列化下一页资源

最后更新于5年前

这有帮助吗?

公众号已发表文章:

getSharedPreferences

通过 Context 的 getSharedPreferences 方法获取 Sp 实例,其具体的实现在 ContextImpl 类中,方法代码如下:

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    // 处理 name 为 null 的情况
    // 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) {
            //创建缓存的 map
            mSharedPrefsPaths = new ArrayMap<>();
        }
        //先从缓存中获取 File 
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            //缓存中没有,先生成file
            file = getSharedPreferencesPath(name);
            //加入缓存
            mSharedPrefsPaths.put(name, file);
        }
    }
    //通过文件生成 SharedPreferences
    return getSharedPreferences(file, mode);
}

逻辑就是根据名称找到或者生成对应的文件,生成文件时调用了 getSharedPreferencesPath,该方法代码如下:

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

调用了makeFileName方法,第一个参数是文件目录,通过getPreferencesDir方法获取;第二个是文件名,通过这里可以看出存储sp使用的xml文件。

getPreferencesDir 代码如下,其实就是获取 data 下的 "shared_prefs"目录:

private File getPreferencesDir() {
    synchronized (mSync) {
        if (mPreferencesDir == null) {
            //应用的data 目录下 shared_prefs 文件夹
            mPreferencesDir = new File(getDataDir(), "shared_prefs");
        }
        return ensurePrivateDirExists(mPreferencesDir);
    }
}

ensurePrivateDirExists 方法主要目的是在目录不存在时进行创建,源码如下,细节就不再分析了。

private static File ensurePrivateDirExists(File file) {
    return ensurePrivateDirExists(file, 0771, -1, null);
}


private static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) {
    if (!file.exists()) {
        final String path = file.getAbsolutePath();
        try {
            Os.mkdir(path, mode);
            Os.chmod(path, mode);
            if (gid != -1) {
                Os.chown(path, -1, gid);
            }
        } catch (ErrnoException e) {
            if (e.errno == OsConstants.EEXIST) {
                // We must have raced with someone; that's okay
            } else {
                Log.w(TAG, "Failed to ensure " + file + ": " + e.getMessage());
            }
        }

        if (xattr != null) {
            try {
                final StructStat stat = Os.stat(file.getAbsolutePath());
                final byte[] value = new byte[8];
                Memory.pokeLong(value, 0, stat.st_ino, ByteOrder.nativeOrder());
                Os.setxattr(file.getParentFile().getAbsolutePath(), xattr, value, 0);
            } catch (ErrnoException e) {
                Log.w(TAG, "Failed to update " + xattr + ": " + e.getMessage());
            }
        }
    }
    return file;
}

makeFilename 方法实现如下:

private File makeFilename(File base, String name) {
    //文件名包含分隔符时抛出异常
    if (name.indexOf(File.separatorChar) < 0) {
        final File res = new File(base, name);
           return res;
    }
    throw new IllegalArgumentException(
            "File " + name + " contains a path separator");
}

可以看出主要逻辑就是生成 File 对象。

以上是生成 file 对象的过程。

创建 SharedPreferences

有了文件后,就可以创建 Sp 实例了,这个过程通过 getSharedPreferences(file, 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");
                }
            }
            //创建 SharedPreferences 实例
            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;
}

方法会先存缓存中获取,如果缓存没有,那么会执行创建,并把新创建的sp实例放入缓存。

创建前先进行了一些权限检查,checkMode 用于检查 sp 的模式,MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 在7.0及以后会抛出异常。

private void checkMode(int mode) {
    if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
        if ((mode & MODE_WORLD_READABLE) != 0) {
            throw new SecurityException("MODE_WORLD_READABLE no longer supported");
        }
        if ((mode & MODE_WORLD_WRITEABLE) != 0) {
            throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
        }
    }
}

创建 sp 时直接实例化了SharedPreferencesImpl,它是 SharePreferences 的实现类,构造方法的源码如下:

SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        //从文件中读取
        startLoadFromDisk();
}

在构造方法中,调用了 startLoadFromDisk 方法,从名字可以看出来,这个方法的任务主要是从文件中读取内容,其源码如下:

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    //开启一个线程读取文件
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

在该方法中,先将读取状态标识置为false,然后开启了一个线程执行文件读取任务,文件读取任务由 loadFromDisk 方法定义,代码为:

private void loadFromDisk() {
    synchronized (mLock) {
        //已经读取成功,直接返回
        if (mLoaded) {
            return;
        }
        //如果有备份文件,优先从备份文件恢复
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    Map<String, Object> map = null;
    Throwable thrown = null;
    try {
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                //创建文件输入流
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                //将读取的文件解析成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) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        //读取完成后设置读取状态为true
        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 赋值
                    mMap = map;
                } 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();
        }
    }
}

主要逻辑是读取XML文件并将其解析为 Map 对象,然后将读取标识置为 true。sp创建后,就可以进行读取和写入了。

读取值

以 getString 方法为例,在 SharedPreferencesImpl中代码:

public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

在读取前,先调用了 awaitLoadedLocked 方法,在还没有完成文件读取时进行等待:

private final Object mLock = new Object();

private void awaitLoadedLocked() {
    while (!mLoaded) {
        try {
            //没有读取成功,先等待
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

而前面 loadFromDisk 方法在读取成功后会调用 mLock.notifyAll 方法,从而通知读取操作可以继续。

值的读取就是从生成的 HashMap 中根据 key 获取对应的值。

写入值

相比读取,写入过程就复杂很多了。在写入时,需要先调用 edit 方法,获取一个 Editor 对象。

创建 Editor

edit 方法源码如下:

@Override
public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }
    return new EditorImpl();
}

当 Sp 可用时,创建了一个 EditorImpl 对象并返回。EditorImpl 是Editor的实现类,同时是SharedPreferencesImpl的内部类。

EditorImpl 只有三个属性:

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

执行修改主要是一些重载的 put 方法,remove 方法用于移除一个键值对,clear 方法用于清空修改。

putXxx

EditorImpl 中的对应的修改方法代码如下(put 方法以 putString 为例):

@Override
public Editor putString(String key, @Nullable String value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

@Override
public Editor remove(String key) {
    synchronized (mEditorLock) {
        mModified.put(key, this);
        return this;
    }
}

@Override
public Editor clear() {
    synchronized (mEditorLock) {
        mClear = true;
        return this;
    }
}

put 和 remove 操作的是 EditorImpl 的 mModified,而 clear 方法只是将清除标记置为 true。

另外这些方法都返回 Editor 对象,方便链式调用。

通过对 EditorImpl 操作后,所有要新增或发生修改的键值对都被记录在了 mModified 这个 map 中,而要使这些更改生效,就需要调用 apply 或者 commit 进行提交。

先来看下 apply 方法。

EditorImpl#apply()

apply 方法源码如下,略去了一些 log 信息:

@Override
public void apply() {
    //先提交到内存
    final MemoryCommitResult mcr = commitToMemory();
    
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    //等待,确保写入被执行
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                
                }
           }
    };

    QueuedWork.addFinisher(awaitCommit);

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

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // 写入内存后就可以通知监听者了
    notifyListeners(mcr);
}

apply 逻辑是:

  1. 通过 commitToMemory 方法把修改提交至内存中

  2. 通过 enqueueDiskWrite 将磁盘写入任务提交至任务队列

  3. 通知监听者

awaitCommit 可能在文件写入时被执行,也可能会在 QueuedWork 执行完所有任务后再执行。如果通过文件写入过程执行,会从QueuedWork的finisher中把它移除。

EditorImpl#commitToMemory()

这个方法用于将在 Editor 上执行所有更改提交到内存的Sp中,并返回一个 MemoryCommitResult 对象提交信息,代码如下:

private MemoryCommitResult commitToMemory() {
    // 当前内存中sp的版本号
    long memoryStateGeneration;
    
    //所有发生修改的key,用于通知监听者
    List<String> keysModified = null;
    
    Set<OnSharedPreferenceChangeListener> listeners = null;
    
    //要写入磁盘的map
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        if (mDiskWritesInFlight > 0) {
            // 此时有写入任务正在执行,所以不能直接修改 mMap,而是克隆它
            mMap = new HashMap<String, Object>(mMap);
        }
        //mapToWriteToDisk 为所有键值对
        mapToWriteToDisk = mMap;
        
        //写入任务+1
        mDiskWritesInFlight++;

        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            //如果有监听者,则需要记录发生更改的 key
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (mEditorLock) {
            boolean changesMade = false;

            if (mClear) {
                //如果调用过 clear 先执行清空操作
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                mClear = false;
            }
            //遍历通过 Editor 做出修改的键值对
            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                //在 remove 方法中,移除一个key时,将它的值设为 EditorImpl.this
                //所以如果 v==this,就代表移除一个key
                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;
                        }
                    }
                    //将新增加或者放生变更的key和value放到即将写入的map中
                    mapToWriteToDisk.put(k, v);
                }

                changesMade = true;
                if (hasListeners) {
                    //记录真正发生变更的key
                    keysModified.add(k);
                }
            }

            mModified.clear();

            if (changesMade) {
                //将内存中的 Sp 版本号自增
                mCurrentMemoryStateGeneration++;
            }
            //记录当前内存中的版本
            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    //创建 MemoryCommitResult 对象
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
}

该方法的主要逻辑是,根据 Editor 的 mModified 中记录的发生修改的key,来更新内存中的 map 对应的 key。如果Sp有监听者,还需要记录发生修改的key,以便通知监听者。最后返回一个MemoryCommitResult 对象,它记录了当前内存中Sp版本号、发生变更的key、监听者和最要写入文件的map(即内存中Sp的所有键值对)。

需要注意的点:

  1. 对于 Editor#clear() 方法,如果执行过,会先将所有键值对清空,然后写入 mModified 中记录的键值对

  2. 内存中的Sp 有一个版本号标识,每次提交新的改动到内存后,该版本号会加1

该方法返回的是一个 MemoryCommitResult 对象,MemoryCommitResult 是 SharedPreferencesImpl 的静态内部类,它的定义如下:

// Return value from EditorImpl#commitToMemory()
private static class MemoryCommitResult {
    //标识当前内存的版本
    final long memoryStateGeneration;
    //发生修改的key集合
    @Nullable final List<String> keysModified;
    //sp 的监听者
    @Nullable final Set<OnSharedPreferenceChangeListener> listeners;
    //要写入文件的map
    final Map<String, Object> mapToWriteToDisk;
    //lock
    final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

    @GuardedBy("mWritingToDiskLock")
    volatile boolean writeToDiskResult = false;
    
    boolean wasWritten = false;

    private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
            @Nullable Set<OnSharedPreferenceChangeListener> listeners,
            Map<String, Object> mapToWriteToDisk) {
        this.memoryStateGeneration = memoryStateGeneration;
        this.keysModified = keysModified;
        this.listeners = listeners;
        this.mapToWriteToDisk = mapToWriteToDisk;
    }

    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        writeToDiskResult = result;
        //countDown
        writtenToDiskLatch.countDown();
    }
}

其中 setDiskWriteResult 方法用来设置文件写入结果,后面会讲到。

SharedPreferencesImpl#enqueDiskWrite

将变更提交的内存后得到 MemoryCommitResult 对象后,调用了 enqueDiskWrite 进行文件写入,这个方法源码如下:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    //是否是 commit                          
    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) {
            // mDiskWritesInFlight 在提交至内存时会自增,
            // 如果是 1 说明此时没有其他的任务要写入
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //在当前线程中直接执行写入
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

该方法中第二个参数是一个 Runnable ,用于在文件写入后执行。如果这个参数为null,那么就说明此次调用来自 commit() 方法,否则此次调用就来自 apply ()方法,在上面的 apply 方法中,我们也看到它确实传了一个 Runnable。

上面方法中的 writeToDiskRunnable 定义了文件写入的过程:先调用 writeToFile 将内存的Sp写入文件,然后将 mDiskWritesInFlight 减一,最后在执行 postWriteRunnable。

不过 writeToDiskRunnable 的执行时机却暗藏玄机。如果此调用来自 commit() 方法,并且目前只有这次写入需要执行,那么就会在当前线程执行这次文件写入(如果我们的调用来自主线程,就会直接在主线程中执行文件写入,这就可能导致性能问题);其他情况都会通过 QueuedWork 把写入任务加入队列中,关于 QueuedWork 稍后再介绍,先来看看 writeToFile 方法如何执行文件写入的。

SharedPreferencesImpl#writeToFile()

writeToFile 是执行文件写入的方法,代码如下,我省略的一些日志输出代码:

@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    // 省略了一些代码

    boolean fileExists = mFile.exists();

    // Rename the current file so it may be used as a backup during the next read
    if (fileExists) {
        boolean needsWrite = false;

        // 只有文件版本小于内存版本时才写入
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
                synchronized (mLock) {
                    //对于 apply,没有必要每次都写入,而是只执行最新一次的提交对应的写入
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }

        if (!needsWrite) {
            //没必要写入文件,直接返回
            mcr.setDiskWriteResult(false, true);
            return;
        }

        boolean backupFileExists = mBackupFile.exists();

        if (!backupFileExists) {
            //备份文件不存在,将正式文件重命名为备份文件
            if (!mFile.renameTo(mBackupFile)) {
                //如果失败,停止写入
                mcr.setDiskWriteResult(false, false);
                return;
            }
        } else {
            //备份文件存在,删除正式文件
            mFile.delete();
        }
    }//end if(fileExists)..

    try {
        //创建文件输出流(会自动创建文件)
        FileOutputStream str = createFileOutputStream(mFile);

        if (str == null) {
            mcr.setDiskWriteResult(false, false);
            return;
        }
        //写入数据
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

        writeTime = System.currentTimeMillis();

        FileUtils.sync(str);

        fsyncTime = System.currentTimeMillis();

        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

        //写入成功,删除备份文件
        mBackupFile.delete();

        //更新文件版本号
        mDiskStateGeneration = mcr.memoryStateGeneration;

        //设置写入结果
        mcr.setDiskWriteResult(true, true);

        long fsyncDuration = fsyncTime - writeTime;
        mSyncTimes.add((int) fsyncDuration);
        mNumSync++;

        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }

    // 发生异常,清理未成功写入的文件
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false, false);
}

在写入前,会先比较当前磁盘版本号与内存版本号,只有磁盘版本号小于内存版本号时,才会执行文件写入。

另外,对于 apply ,会将每次 mcr 的版本号与当前内存的最终版本号进行对比,只有相等时才会执行文件写入。这样一来,如果短时间内有多次 apply 文件写入请求,只有最后一次写入会被真正执行。

在执行写入前,会先创建一个备份文件,当写入过程中发生意外,下次读取时可以从备份文件中恢复。在从文件加载Sp 的 loadFromDisk 方法中就有如下代码:

synchronized (mLock) {
    if (mLoaded) {
        return;
    }
    //如果备份文件存在,则优先从备份文件中恢复
    if (mBackupFile.exists()) {
        mFile.delete();
        mBackupFile.renameTo(mFile);
    }
}

正式的文件写入过程主要分以下几步:

  1. 创建文件输出流

  2. 将 map 写入到 xml 文件

  3. 删除备份文件

  4. 更新磁盘Sp对应版本号 mDiskStateGeneration

  5. 设置写入结果

关于第五步,针对文件写入情况,会调用 MemoryCommitResult#setDiskWriteResult 方法设置结果,这个方法源码:

//lock
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

void setDiskWriteResult(boolean wasWritten, boolean result) {
    this.wasWritten = wasWritten;
    writeToDiskResult = result;
    //countDown
    writtenToDiskLatch.countDown();
}

设置完结果后,调用了 writtenToDiskLatch.countDown() 以通知正在等待的线程。

以上就是 apply 的过程,主要分为两步:

  1. 提交修改至内存,生成一个 MemoryCommitResult 对象mcr,这个 mcr 记录着当前内存中sp的版本号、所有键值对、发生修改的键等信息

  2. 通过 QueuedWork 将文件写入任务入队,等待执行

在讲解 QueuedWork 之前,我们先对照着看一下 commit 方法。

EditorImpl#commit()

public boolean commit() {
    //1.提交至内存
    MemoryCommitResult mcr = commitToMemory();
    //2.将文件写入任务入队列
    SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null);
    try {
        //3.等待文件写入结果
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
                
    }
    //4. 通知监听者
    notifyListeners(mcr);
    //5. 返回文件写入结果
    return mcr.writeToDiskResult;
}

commit 方法也包括提交内存和将文件写入任务入队,但后面还增加了等待文件写入完成的过程,因为commit 方法的返回值就是文件写入的结果。

为了方便阅读,再把该方法源码贴一遍:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    //是否是 commit                          
    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) {
            // mDiskWritesInFlight 在提交至内存时会自增,
            // 如果是 1 说明此时没有其他的任务要写入
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //在当前线程中直接执行写入
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

对于 commit 方法提交,如果当前并没有其他文件写入任务要执行,那么会在当前线程执行文件写入。

接下来终于要揭晓 QueuedWork 的神秘面纱了。

在看代码细节之前,先来看看官方注释对 QueuedWork 有个初步了解:

Internal utility class to keep track of process-global work that's outstanding and hasn't been finished yet.

This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for other things in the future.

The queued asynchronous work is performed on a separate, dedicated thread.

通过以上注释,可以获取一下信息:

  1. 用于维护进程级别的任务

  2. 新任务通过queue方法入队

  3. 可以增加 finisher 任务,它们肯定会被执行,可以用来确保任务已经被执行完成

  4. 目前主要用于执行 Sp 的文件同步并提供等待完成机制

  5. 队列的任务会在单独线程中执行。

下面就来看看 QueuedWork 的代码实现,先来看看它的几个重要的静态属性:

静态属性

private static final long DELAY = 100;
//类的锁
private static final Object sLock = new Object();
//处理工作的锁,确保只有一个线程在处理任务
private static Object sProcessingWork = new Object();

private static Handler sHandler = null;
//存储所有finisher
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
//存储所有任务
private static final LinkedList<Runnable> sWork = new LinkedList<>();
//是否允许延迟
private static boolean sCanDelay = true;

  
public static void addFinisher(Runnable finisher) {
    synchronized (sLock) {
        sFinishers.add(finisher);
    }
}    

QueuedWork#queue()

前面在 SharedPreferencesImpl#enqueDiskWrite(),调用了queue 方法将文件写入工作入队,该方法源码如下:

public static void queue(Runnable work, boolean shouldDelay) {
    //1.获取 handler
    Handler handler = getHandler();

    synchronized (sLock) {
        //2. 将work添加到任务列表
        sWork.add(work);
        //3. 发送消息触发执行
        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}

主要逻辑就是讲任务加入队列中,然后通过handler发送消息来出发任务的执行。这个方法的第二个参数标识是否需要延后一段时间(DELAY 的值是100),在SharedPreferencesImpl#enqueDiskWrite()中是这样调用的:

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

也就是说,对于 commit ,shouldDelay 为 false;对于 apply ,shouldDelay 为true。shouldDelay 决定了在通过 handler 发送消息时是否启用延时。

那为什么要 apply 进行延时呢?

在 SharedPreferencesImpl 的 writeToFile 方法中有如下判断:

if (mDiskStateGeneration < mcr.memoryStateGeneration) {
    if (isFromSyncCommit) {
        needsWrite = true;
    } else {
        synchronized (mLock) {
            //对于 apply,没有必要每次都写入,而是只执行最新一次的提交对应的写入
            if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                needsWrite = true;
            }
        }
    }
}

DELAY 的值是常量100,如果100ms 内有多次 apply 提交,这个延时可以确保当100ms后,任务队列中的文件写入任务被统一处理时,只有最新的apply提交对应的文件写入任务会真正被执行。因为只有它对应的mcr的版本号是和内存 一致的,其他的mcr版本都低于内存版本。这样可以有效去除冗余的文件写入,提升性能。

getHandler()

上面方法中 handler 的获取调用了 getHandler() 方法,其源码如下:

private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {
            //创建新的线程
            HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                    Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();
            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        return sHandler;
    }
}

可以看到,初始化时,新建了一个 HanlderThread ,并基于它创建了一个 QueuedWorkHandler 赋值给 sHandler。

QueuedWorkHandler

QueuedWorkHandler 是 QueuedWork 的静态内部类,它的定义很简单,在收到MSG_RUN消息后,调用 processPendingWork() 处理所有待执行的任务。

private static class QueuedWorkHandler extends Handler {
    static final int MSG_RUN = 1;

    QueuedWorkHandler(Looper looper) {
        super(looper);
    }

    public void handleMessage(Message msg) {
        if (msg.what == MSG_RUN) {
            processPendingWork();
        }
    }
}

processPendingWork

private static void processPendingWork() {
    synchronized (sProcessingWork) {
        LinkedList<Runnable> work;

        synchronized (sLock) {
            work = (LinkedList<Runnable>) sWork.clone();
            sWork.clear();

            // 移除所有MSG_RUN消息
            getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
        }

        if (work.size() > 0) {
            //按顺序执行任务
            for (Runnable w : work) {
                w.run();
            }
        }
    }
}

processPendingWork 的逻辑也很简单,就是将任务列表中的任务按序执行。正常情况下,这个过程就是在 getHandler 中创建的 HandlerThread 中进行。

那不正常情况呢?

waitToFinish

/**
 * Trigger queued work to be processed immediately. The queued work is processed on a separate
 * thread asynchronous. While doing that run and process all finishers on this thread. The
 * finishers can be implemented in a way to check weather the queued work is finished.
 *
 * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
 * after Service command handling, etc. (so async work is never lost)
 */
public static void waitToFinish() {

    Handler handler = getHandler();

    synchronized (sLock) {
        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
            // Delayed work will be processed at processPendingWork() below
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);
        }

        // We should not delay any work as this might delay the finishers
        sCanDelay = false;
    }

    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
    try {
        processPendingWork();
    } finally {
        StrictMode.setThreadPolicy(oldPolicy);
    }

    try {
        while (true) {
            Runnable finisher;

            synchronized (sLock) {
                finisher = sFinishers.poll();
            }

            if (finisher == null) {
                break;
            }

            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }
    //...

}

waitToFinish 会在当前线程立刻执行所有待执行的任务,任务执行完后会一并执行所有 finisher 来通知任务执行完成。

而通过方法的注释可以看出,这个方法主要在 Activity的onPause 方法中、BroadcastReceiver 的 onReceive 方法后、以及Service 的 onCommand 后调用(具体见下面代码),以确保所有任务都被执行没有丢失,但这回导致在主线程执行文件写入,是有可能造成性能问题的。

//Service 启动
private void handleServiceArgs(ServiceArgsData data) {
        Service s = mServices.get(data.token);
        if (s != null) {
            try {
                if (data.args != null) {
                    data.args.setExtrasClassLoader(s.getClassLoader());
                    data.args.prepareToEnterProcess();
                }
                int res;
                if (!data.taskRemoved) {
                    //调用Service的 onStartCommand
                    res = s.onStartCommand(data.args, data.flags, data.startId);
                } else {
                    s.onTaskRemoved(data.args);
                    res = Service.START_TASK_REMOVED_COMPLETE;
                }

                QueuedWork.waitToFinish();
                
                //.....
            }
        }
    }
//service stop
private void handleStopService(IBinder token) {
        Service s = mServices.remove(token);
        if (s != null) {
            try {
                //调用Service的onDestroy方法
                s.onDestroy();
                s.detachAndCleanUp();
                //.....
                
                QueuedWork.waitToFinish();

                //....
            } catch (Exception e) {
                if (!mInstrumentation.onException(s, e)) {
                    throw new RuntimeException(
                            "Unable to stop service " + s
                            + ": " + e.toString(), e);
                }
                Slog.i(TAG, "handleStopService: exception for " + token, e);
            }
        } 
    }
//在 Android 11 之前,在 onPause调用后执行 QueuedWork.waitToFinish();
@Override
    public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
            int configChanges, PendingTransactionActions pendingActions, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        if (r != null) {
            if (userLeaving) {
                performUserLeavingActivity(r);
            }

            r.activity.mConfigChangeFlags |= configChanges;
            performPauseActivity(r, finished, reason, pendingActions);

            // Make sure any pending writes are now committed.
            if (r.isPreHoneycomb()) {
                QueuedWork.waitToFinish();
            }
            mSomeActivitiesChanged = true;
        }
    }
//在Android 11之后,在onStop 后调用
public void handleStopActivity(IBinder token, boolean show, int configChanges,
            PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
        final ActivityClientRecord r = mActivities.get(token);
        r.activity.mConfigChangeFlags |= configChanges;

        final StopInfo stopInfo = new StopInfo();
        performStopActivityInner(r, stopInfo, show, true /* saveState */, finalStateRequest,
                reason);

        //...

        // Make sure any pending writes are now committed.
        if (!r.isPreHoneycomb()) {
            QueuedWork.waitToFinish();
        }
        
        //...
}

建议

  • 尽量使用apply

  • 多次 put ,一次apply

  • 拆分 sp 文件的大小,避免一个 app 只使用一个 Sp,这样文件将会变得很大,写入时间会变长。如果恰好卡在 waitToFinish 这样的时间点,有可能造成 ANR。

  • 不要连续多次edit(), 应该获取一次获取edit(),然后多次执行putxxx(), 减少内存波动

  • 不要使用MODE_MULTI_PROCESS

相关问题

commit 和 apply 区别?

commit 会等待文件写入完成,并且有可能会在主线程写入文件,并且没有针对短时间内频繁更新做优化,有可能导致每次操作都在主线程写入。

apply 如果短时间内(100ms)有多次提交,只有最后一次会执行文件写入(因为会对比每次提交的版本号是否与当前内存版本号一致),并且是在单独的线程里执行写入,不会影响性能。

相关链接

另外,调用 方法时,第二个参数传的是 null,在分析该方法时我们也看到,该方法正是以这个参数是否为 null 来区分是commit 还是 apply 的。

New work will be {@link queued}. It is possible to add 'finisher'-runnables that are {@link guaranteed to be run}. This is used to make sure the work has been finished.

SharedPreferences 源码解析(上)
SharedPreferences 源码解析(下)
QueuedWork
#queue
#waitToFinish
gityuan:全面剖析SharedPreferences
SharedPreferences灵魂拷问之原理
SharedPreferencesImpl#enqueDiskWrite