搜索
您的当前位置:首页正文

Android 之不要滥用 SharedPreferences

来源:二三娱乐
闪存
  • SharedPreferences 系列
  • ContentProvider 系列(待更)

《Android 存储选项之 ContentProvider 启动过程源码分析》
《Android 存储选项之 ContentProvider 深入分析》

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

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

  • 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。

小结一下
  • 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();
      }
    
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();
}

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

其实不难分析出 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 数据提交过程: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 尽量不要放在一起。(如果整个文件本身就较小则可以忽略)

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

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

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

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

以上便是个人在学习 SharedPreferences 过程中一些心得和体会,文中如有不妥或更好的分析方法,欢迎您的指教!

如果你喜欢我的文章,就请留个赞吧!

Top