Android 之不要滥用 SharedPreferences(上)

闪存
Android 存储优化系列专题
  • SharedPreferences 系列

Android 之不要滥用 SharedPreferences(上)
Android 之不要滥用 SharedPreferences(下)

  • ContentProvider 系列(待完善)

Android 存储选项之 ContentProvider 启动存在的暗坑
《Android 存储选项之 ContentProvider 深入分析》

  • 对象序列化系列

Android 对象序列化之你不知道的 Serializable
Android 对象序列化之 Parcelable 深入分析
Android 对象序列化之追求完美的 Serial

  • 数据序列化系列(待更)

《Android 数据序列化之 JSON》
《Android 数据序列化之 Protocol Buffer 使用》
《Android 数据序列化之 Protocol Buffer 源码分析》

  • SQLite 存储系列

Android 存储选项之 SQLiteDatabase 创建过程源码分析
Android 存储选项之 SQLiteDatabase 源码分析
数据库连接池 SQLiteConnectionPool 源码分析
SQLiteDatabase 启用事务源码分析
SQLite 数据库 WAL 模式工作原理简介
SQLite 数据库锁机制与事务简介
SQLite 数据库优化那些事儿


前言

本文不是与大家一起探讨关于 SharedPreferences 的基本使用,而是结合源码的角度分析对 SharedPreferences 使用不当可能引发的“严重后果”以及该如何正确的使用 SharedPreferences。

SharedPreferences 是 Android 平台为应用开发者提供的一个轻量级的存储辅助类,用来保存应用的一些常用配置,它提供了 putString()、putString(Set<String>)、putInt()、putLong()、putFloat()、putBoolean() 六种数据类型。数据最终是以 XML 形式进行存储。在应用中通常做一些简单数据的持久化存储。SharedPreferences 作为一个轻量级存储,所以就限制了它的使用场景,如果对它使用不当可能会引发“严重后果”。

从源码角度出发(基于 API Level 28)
1、 SharedPreferences 文件保存位置
SharedPreferences config = context.getSharedPreferences("config", Context.MODE_PRIVATE);
String value = config.getString("key", "default");

通过 Context 的 getSharedPreferences() 方法得到 SharedPreferences 对象,这里实际调用的是 ContextImpl.getSharedPreferences() 方法。

@Override
public SharedPreferences getSharedPreferences(String name, int mode){
    //mBase实际类型是 ContextImpl
    return mBase.getSharedPrefenences(name, mode);
}

mBase 的实际类型是 ContextImpl(不熟悉的朋友,可以去看下 Activity 的创建过程,在 ActivityThread 中)。

ContextImpl 中 getSharedPreferences(String name, int mode) 调用过程如下:

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {  
    if (mPackageInfo.getApplicationInfo().targetSdkVersion <
            Build.VERSION_CODES.KITKAT) {
        if (name == null) {
            //如果 targetSdkVersion 小于 19 版本,name 传递 null,
            //则直接将文件名设置为null,既文件名为:null.xml
            name = "null";
        }
    }

    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            //mSharedPrefsPaths维护文件名name和文件File 的映射关系
            //这个在较早版本中不存在
            mSharedPrefsPaths = new ArrayMap<>();
        }
        //通过文件名name获取对应的文件File
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            //SharedPreferences文件目录创建过程
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    //根据File创建SharedPreferences
    return getSharedPreferences(file, mode);
}

代码中标注了详细的注释,这里主要维护了 SharedPreferences 文件名 name 和文件 File 的映射关系,既根据文件名 name 得到文件 File,每个 Activity 都会包含一个 ContextImpl 对象,mSharedPrefsPaths 是它的成员变量,既仅在当前对象有效。这里重点跟踪下 SharedPreferences 文件的保存目录,SharedPreferences 文件路径创建过程:

/**
 * 根据文件名创建File对象
 */
@Override
public File getSharedPreferencesPath(String name){
  return makeFilename(getPreferencesDir(), name+".xml");
}

getPreferencesDir 方法如下:

@Override
private File getPreferencesDir(){
  synchronized(mSync){
      if(mPreferencesDir == null){
           //创建SharedPreferences文件保存目录
          //getDataDir返回:/data/data/packageName/
          mPreferencesDir = new File(getDataDir(), "shared_prefs");
      } 
      //确保应用私有文件目录已经存在
      return ensurePrivateDirExists(mPreferencesDir);
  }
}

从这里可以看出 SharedPreferences 文件的存储位置是在应用程序包名下 shared_prefs 目录内。

这里需要注意的是文件名 name 不能是路径形式如:“/config”,如下将会抛出异常:

@override
private File makeFilename(File base, String name){
  if(name.indexOf(File.separatorChar) < 0){
     //SharedPreferences文件名中如果包含“/”字符将会抛出异常
     return new File(base, name);
  }
  throw new IllegalArgumentException("File " + name + " contains a path     separator" );
}

跟踪到这里,SharedPreferences 的文件保存路径我们就算是找到了。这一步中主要通过文件名 name 创建对应文件 File 对象。并且会将其缓存在 ContextImpl 的 Map(mSharedPerfsPaths)容器中。 接着我们看 SharedPreferences 的创建过程。


2、SharedPreferences 创建过程
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        //得到用于缓存SharedPreferences的Map容器
        //该Map容器在ContextImpl单例方式声明
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            //Android N之后不在支持MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE
            checkMode(mode);
            //Android 7.0之后的文件级加密相关
            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首次创建,实际类型是SharedPreferencesImpl
            //SharedPreferences只是一个接口,定义了操作的基本API。
            //真正实现是在SharedPreferencesImpl中
            sp = new SharedPreferencesImpl(file, mode);
            //保存在Map容器中,该Map容器为单例
            cache.put(file, sp);
            return sp;
        }
    }
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        //MODE_MULTI_PROCESS的加载策略
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

在该方法中首先看下 getSharedPreferencesCacheLocked 方法如下:

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        //sSharedPrefsCache是static ArrayMap容器
        //早期是HashMap,ArrayMap相比HashMap在内存占用上略有一定优势
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    //根据应用包名,获取ArrayMap对象
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        //这里的存储是根据包名,保存所有SharedPreferencesImpl集合
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

sSharedPrefsCache 声明为 static ArrayMap 对象,根据当前应用包名 packageName 得到保存 SharedPreferences 的集合并返回。

回到上面的方法中,根据 File 从返回保存 SharedPreferences 的集合中获取,如果是第一次创建,直接创建 SharedPreferencesImpl 对象,并将其缓存在 Map(sSharedPrefsCache) 容器中。跟踪到这里我们可以确定 SharedPreferences 的实际返回类型是 SharedPreferencesImpl。

本文基于 API Level 28 分析的 ContextImpl 中关于 SharedPreferences 处理机制,这相较于较早版本的管理策略有所不同,具体你可以参考之前基于 API Level 16 源码分析的 SharedPreferences


小结一下
  • SharedPreferences 只是一个接口,定义了标准操作 API,而真正实现的是 SharedPreferencesImpl,我们后续的一系列对 SharedPreferences 的操作实际都是通过 SharedPreferencesImpl 完成的。

  • 系统会将每个 SharedPreferences 文件对应的操作对象(实际为 SharedPreferencesImpl)进行缓存,后续相关 Context.getSharedPreferences("name", mode) 都是从该缓存中直接获取。

  • SharedPreferences 为我们提供了 Context.MODE_MULTI_PROCESS 的加载模式,不知道在上面 getSharedPreferences(File file, int mode) 方法中,你有没有注意到:

    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
          getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
          //MODE_MULTI_PROCESS的加载策略
          sp.startReloadIfChangedUnexpectedly();
      }
    

当应用指定的 targetSdkVersion 小于 API Level 11 时,则重新从文件中加载一遍数据到内存中,所以指望 SharedPreferences 能够跨进程通信的可以死心了。关于 SharedPreferences 跨进程使用分析你可以参考《Android 之不要滥用SharedPreferences(下)


3、SharedPreferences 的数据加载过程

终于说到 SharedPreferences 数据操作的相关内容了,这部分也是我们要重点讨论的内容,因为这里面或多或少存在一些暗坑,如果对它不足够了解,很容易引发相关性能问题。

上面有分析到 SharedPreferences 的实际操作类型是 SharedPreferencesImpl,它的构造方法如下:

SharedPreferencesImpl(File file, int mode) {
    //SharedPreferences保存文件,前面有分析到
    mFile = file;
    //SharedPreferences备份文件
    mBackupFile = makeBackupFile(file);
    //加载模式
    mMode = mode;
    //标志位,表示是否正在加载
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    //开启线程,加载对应文件数据到Map容器中
    startLoadFromDisk();
}

有关 SharedPreferences 的备份文件 mBackupFile 的作用,由于这部分内容也比较多,主要涉及到 SharedPreferences 的数据丢失,和多进程使用场景,如果想更深入了解该部分内容你可以参考这里

在 SharedPreferencesImpl 的构造方法中,我们需要重点跟踪方法的最后 startLoadFromDisk 方法如下:

private void startLoadFromDisk() {
    synchronized (mLock) {
        //加载状态标志位,每当需要加载时,先将其置为false
        //加载完成之后再置为true
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        
        public void run() {
            //开启独立线程进行数据加载
            loadFromDisk();
        }
    }.start();
}

mLoaded 起到加载状态标志的作用,该标志状态非常重要(主要是多线程访问等待),如果此时在 UI 线程操作 SharedPreferences 数据,可能导致 UI 线程等待。后面会详细分析到该部分。

SharedPreferences 文件内容加载使用了异步线程,真正开始加载 loadFromDisk 方法如下:

//代码中省略了部分
private void loadFromDisk() {
    synchronized (mLock) {
        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<String, Object> map = null;
    //主要在MODE_MULTI_PROCESS起到作用
    StructStat stat = null; 
    //确定加载过程中是否发生过异常
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                //通过BufferedInputStream从文件中读取内容
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                //SharedPreferences的文件操作都封装在XmlUtils中
                //返回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) {
        //表示SharedPreferences文件中数据已经加载到内存Map中
        mLoaded = true;
        mThrowable = thrown;
        try {
            //表示加载过程未发生异常
            if (thrown == null) {
                if (map != null) {
                    //如果成功直接赋值给其成员
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } 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();
        }
    }
}

代码篇幅虽然较长,但是不难理解,源码可以看出通过 BufferedInputStream 加载对应 SharedPreferences 文件内容,系统封装了 XmlUtils 进行 XML 文件数据读写,并且将数据封装在 Map 容器并返回,如果整个过程未发生任何异常,则直接将其赋值给 SharedPreferencesImpl 的成员 mMap,声明如下:

private Map<String, Object> mMap;

跟踪到这里 SharedPreferences 的首次加载机制就已经明确了,每个 SharedPreferences 存储都会对应一个 name.xml 文件,在使用时,系统通过异步线程一次性将该文件内容加载到内存中,保存在 Map 容器中。实际后续我们对 SharedPreferences 的一些列 getXxx() 操作都是直接操作的该 Map 容器。后面我们将验证到该部分内容。

小结一下

SharedPreferencesImpl 在初始化时,会开启异步线程加载对应 name 的 XML 文件内容到 Map 容器中,如果文件内容较大,这一过程耗时还是不能忽视的,主要体现在如果此时我们操作 SharedPreferences 会导致线程等待问题,这里主要根据前面分析到的加载状态标志 mLoaded 变量有关,接下来我们就对其进行分析。


4、一系列 getXxx() 操作

通过前面的分析,你肯定也能想到:SharedPreferences 的数据都保存在 Map 容器中,此时就是根据 Key 到该 Map 容器中查找对应的数据即可,以 getString() 为例:

@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        //这里就是根据前面分析到的mLoaded加载状态标志
        //判断当前SharedPreferences文件内容是否加载完成
        //否则调用方线程进入等待wait
        awaitLoadedLocked();
        //这里直接就是从Map容器中获取
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

可以看到直接根据 key 到 Map 中查找对应的数据并返回。
这里我们还需要重点跟踪 mLoaded 标志起到的作用,awaitLoadedLocked 方法如下:

private void awaitLoadedLocked() {
    if (!mLoaded) {
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        //加载状态标志位,如果未加载完成,该变量为false,会将调用线程wait住
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

还记得前面分析 SharedPreferences 数据加载过程 mLoaded 标志位,在开始加载文件数据之前先将该标志位置为 false,从文件加载完成之后,重新将其置为 true,表示此次文件内容加载完成。如果加载过程较为耗时,此时我们在 UI 线程中对 SharedPreferences 做相关数据操作,该线程就会进入 wait 状态。这就导致出现主线程等待低优先级线程锁的问题,比如一个 100KB 的 SP 文件读取等待时间大约需要 50 ~ 100ms。此时非常容易造成卡顿,如果再严重甚至会引发 ANR。这里涉及到一个优化点,最后会给大家总结出。

mLock 锁的唤醒操作,在 loadFromDisk 方法最后,唤醒所有等待线程(如果存在)

       try {              
            // ... 省略
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();
        }
小结一下
  • mLoaded 标志起到 SharedPreferences 文件内容是否加载完成(加载到 Map 容器中),如果未加载完成,此时对其做相关数据操作就会导致 awaitLoadedLocked 方法的等待。

  • 通过 SharedPreferences 存储的数据都会在内存中保留一份(Map 变量中),后续的一系列 getXxx() 操作直接在该容器中获取数据。


5、一系列 putXxx() 操作

前面分析到对 SharedPreferences 的一系列 getXxx() 操作,大家此时是否会认为 putXxx() 操作也是直接对该 Map 容器操作呢?显然不是的,修改数据操作相比 getXxx() 操作要麻烦很多,继续结合源码进行分析:

SharedPreferences config = context.getSharedPreferences("config", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = config.edit().putString("key", "value");
//提交
editor.apply();

put 操作要首先经过 edit 方法返回 Editor 对象:

@Override
public Editor edit() {
    synchronized (mLock) {
        //这里与一系列getXxx()作用一致
        //同样受到mLoaded标志状态的作用
        awaitLoadedLocked();
    }
    //实际返回的是EditorImpl
    return new EditorImpl();
}

SharedPreferences 的 edit 方法实际返回的是 EditorImpl 对象:

public final class EditorImpl implements Editor {
    private final Object mEditorLock = new Object();

    /**
     * 保存修改数据的容器
     * 一系列添加、修改、删除数据都保存在该临时容器Map中
     */
    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>();

    //标志当前是否是清除操作
    @GuardedBy("mEditorLock")
    private boolean mClear = false;

    //添加String类型数据
    @Override
    public Editor putString(String key, @Nullable String value) {
        synchronized (mEditorLock) {
            //添加到一个临时Map容器
            mModified.put(key, value);
            return this;
        }
    }

   //其它数据类型添加省略

}

Editor 只是一个接口,与 SharedPreferences 功能类似,定义基础操作 API,我们一系列的 putXxx()、remove()、clear()、apply()、commit() 实际都是在 EditorImpl 中完成。

从源码中我们可以看出,操作数据都保存在 EditorImpl 中的 mModified 容器中,最后我们必须通过 commit 或 apply 进行提交,这里也是我们重点要分析的。

这里也需要注意每次通过 SharedPreferences.edit() 都会创建一个新的 EditorImpl 对象,应该尽量批量操作统一提交。最后会一起总结出。

任务提交 commIt 或 apply 方法调用几乎一致,都会经过 commitToMemory 方法后调用 enqueueDiskWrite 方法。不同之处在于 enqueueDiskWrite 方法,如果当前是 commit 提交,则将数据写入文件任务在当前线程执行;否则 apply 提交则将写入文件任务在工作线程中完成,看下详细过程:

    @Override
    public boolean commit() {
        long startTime = 0;

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

        //将mModified容器中数据提交到SharedPreferencesImpl成员Map容器中
        //后者数据要写入文件时使用
        MemoryCommitResult mcr = commitToMemory();
        //将MemoryCommitResult作为参数
        //根据策略 commit/apply决定任务在工作线程还是在当前线程
        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;
    }

我们先来跟踪下 commitToMemory 方法过程:

private MemoryCommitResult commitToMemory() {
        long memoryStateGeneration;
        //保存发生变化的key
        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);
            }
            //将成员mMap赋值给局部变量,后续for循环中
            mapToWriteToDisk = mMap;
            mDiskWritesInFlight++;

            //我们可以监听SharedPreferences数据提交完成
            boolean hasListeners = mListeners.size() > 0;
            if (hasListeners) {
                keysModified = new ArrayList<String>();
                //这里收集回调通知
                listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
            }

            synchronized (mEditorLock) {
                //该标志主要作用是确保当前是否真正发生变化,避免无谓的I/O操作。
                boolean changesMade = false;

                if (mClear) {
                    //如果是clear操作,可以看出直接清空数据
                    if (!mapToWriteToDisk.isEmpty()) {
                        changesMade = true;
                        mapToWriteToDisk.clear();
                    }
                    mClear = false;
                }
                //这里开始遍历一系列修改后的数据容器mModified
                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)) {
                            //value等于null,然后mMap由不包含该key
                            //可以直接跳过
                            continue;
                        }
                        //如果value==null,可以直接将其移除
                        mapToWriteToDisk.remove(k);
                    } else {
                        if (mapToWriteToDisk.containsKey(k)) {
                            //如果mMap容器中包含该key,则直接修正为最新提交数据value
                            Object existingValue = mapToWriteToDisk.get(k);
                            if (existingValue != null && existingValue.equals(v)) {
                                //如果value相等则跳过本次
                                //主要是考虑changesMode标志位,确认当前数据是否真正发生变化
                                continue;
                            }
                        }
                        //否则直接添加新的key:value
                        mapToWriteToDisk.put(k, v);
                    }
                    //这里在for循环中,如果发生数据变化,该changeMade将会置为true
                    //表示当前数据发生变化
                    changesMade = true;
                    if (hasListeners) {
                        keysModified.add(k);
                    }
                }
                //清空临时修改数据容器
                mModified.clear();

                if (changesMade) {
                    mCurrentMemoryStateGeneration++;
                }

                memoryStateGeneration = mCurrentMemoryStateGeneration;
            }
        }
        return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                mapToWriteToDisk);
    }

系统考虑到本次提交数据是否真正发生变化:changesMade 变量的作用,否则在提交时会直接 return 处理,该部分内容你可以参考下一篇《Android 之不要滥用 SharedPreferences(下)》。这算是一层优化,避免无谓的 I/O 操作。

其实不难分析出 commitToMemory 方法主要工作是:前面我们一系列的 putXxx() 或 remove() 操作都会添加到 mModified 临时容器中,mModified 保留着我们当前的改变,通过遍历该容器与 mMap(SharedPreferencesImpl 成员)容器做比较,比如相同 key 不同 value 此时将修改提交到 mMap 容器中,然后 mMap 中就保存了修正后,我们最后一次提交的数据。最后清空 mModified 容器。

重新回到前面 commit 方法,调用 enqueueDiskWrite 方法如下:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    //执行写入文件的Runnalbe任务
    //这里也主要区分commit或apply提交的区别
    //apply提交会将该任务丢入线程池,异步执行
    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                //这里执行写入文件操作
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                postWriteRunnable.run();
            }
        }
    };

    //当commit提交时,会在当前线程执行run方法
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //commit操作,直接在当前线程中执行
            writeToDiskRunnable.run();
            return;
        }
    }
    //如果是apply(),提交则将任务加入线程池排队执行
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

writeToDiskRunnable 是执行写入文件操作的任务(就是将最后一次 commitToMemory 之后的 mMap 数据写回到文件)。

如果是 commit 操作,会直接在当前线程中执行 writeToDiskRunnable.run();除了 commit 提交之外,还可以 apply 进行提交,此时 writeToDiskRunnable 任务将被添加到线程池,该线程池只有一个线程,故所有提交的任务都需要经过串行等待执行。(注意:QueuedWork 早期版本实现是只有一个线程的线程池,本文依据 API Level 28 分析,系统已经改成 HandlerThread ,熟悉它的朋友都知道,这仍然是串行执行)

无论是使用 commit 还是 apply 数据提交 ,即使我们只改动其中一个条目,都会把整个内容(mMap)全部写入到文件。而且即使我们多次写入同一个文件,SP 也没有将多次修改合并为一次,这也是 SharedPreferences 性能差的重要原因之一

分析到这里关于 SharedPreferences 数据提交过程:commit 发生在当前线程,apply 发生在工作线程,如果要保证 I/O 操作不阻塞 UI 线程我们可以优先考虑使用 apply 来提交修改,这样是否就绝对安全了呢?这里先告诉大家绝对不是的!!!


6、apply() 异步提交一定安全吗?

前面说到 apply 使写入文件任务发生在工作线程中,这样防止 I/O 操作阻塞 UI 线程;但它同样可能会引发卡顿性能问题,我们需要跟踪另外一部分系统源码:

首先 Android 四大组件的创建以及生命周期管理调用,都是通过进程间通信完成的,到我们自己应用进程,通过调度完成过渡任务的是 ActivityThread,ActivityThread 是我们应用进程的入口类(main 方法所在),来看下 Activity 的 onPause 的回调过程:

@Override
public void handlePauseActivity(IBinder token, boolean show, int configChanges, PendingTransactionActions pendingactions, boolean finalStateRequest, String reason){
    //... 省略
    if(!r.isPreHoneycomb()){
      //这里检查,异步提交的SharedPreferences任务是否已经完成
      //否则一直等到执行完成
      QueuedWork.waitToFinish();
    }
    //... 省略
 }

你没有看错又要等待,等待什么呢?
我们通过 SharedPreferences 一系列的 apply 提交的任务,都会被加入到工作线程 QueueWork 中,该任务队列以串行方式执行(只有一个工作线程),如果我们 apply 提交非常多的任务,此时判断任务队列还未执行完成,就会一直等到全部执行完成,这就非常容易发生卡顿,如果超过 5s 还会引发 ANR。

由此可见 apply 提交也不是”绝对安全“的,试想当你 apply 提交大量任务,并且还都是大型 key 或 value 时!!!

总结

SharedPreferences 的实际操作者是 SharedPreferencesImpl,当首次创建 SharedPreferences 对象,会根据文件名将对应文件内容使用异步线程一次性加载到 Map 容器中,试想如果此时存储了一些大型 key 或 value 它们一直在内存中得不到释放。如果加载过程中,对其做相关数据操作,会导致线程等待 awaitLoadedLocked。系统会缓存每个使用过的 SharedPreferencesImpl 对象。每当我们 edit 都会创建一个新的 EditorImpl 对象,当修改或者添加数据时会将其添加到 EditorImpl 的 mModifiled 容器中,通过 commit 或 apply 提交后会比较 mModifiled 与 mMap 容器数据,修正(commitToMemory 方法作用) mMap 中最后一次数据提交后写入文件。


优化建议:

1、不要存放大的 key 或 value 在 SharedPreferences 中,否则会一直存储在内存中(Map 容器中)得不到释放,内存使用过高会频繁引发 GC,导致界面丢帧甚至 ANR

2、不相关的配置选项最好不要放在一起,单个文件越大加载时间越长。(参照 SharedPreferences 初始化时会开启异步线程读取对应文件,如果此时耗时较长,当对其进行相关数据操作时会导致线程等待)

3、读取频繁的 key 和 不频繁的 key 尽量不要放在一起。(如果整个文件本身就较小则可以忽略)

4、不要每次都 edit 操作,每次 edit 都会创建新的 EditorImpl 对象,最好批量处理统一提交。否则每次 edit().commit() 都会创建新的 EditorImpl 对象并进行一次 I/O 操作,严重影响性能。

5、commit 提交发生在 UI 线程,apply 提交发生在工作线程,对于数据的提交最好是批量操作统一提交。虽然 apply 任务发生在工作线程(不会因为 I/O 阻塞 UI 线程),但是如果添加过多任务也有可能带来其它”严重后果“(参照系统源码 ActivityThread - handlePauseActivity 方法实现)

6、尽量不要存放 JSON 或 HTML 类型数据,这种可以直接文件存储

7、最好能够提前初始化 SharedPreferences,避免 SharedPreferences 第一次创建时读取文件内容线程未结束而出现的等待情况,参照优化点第 2 条

8、不要指望它能够跨进程通信:Context.MODE_MULTI_PROCESS


以上便是对 SharedPreferences 的学习心得和指导建议,文中如果不妥或有更好的分析结果,欢迎你的指正。

有关 SharedPreferences 更深入分析请参考下篇《Android 之不要滥用SharedPreferences(下)》。

文章如果对你有帮助,请留个赞吧!如果喜欢我的分析还可以阅读系列的其他相关文章。


扩展阅读

...

UI 优化系列

网络优化系列

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

推荐阅读更多精彩内容