SharedPreference

SP是Android的一种轻量级的存储方式,api很简单,所以在日常开发中,也是很常用的.但是滥用SP带来的后果也会很严重,最近项目中遇到个问题,所以这篇博客主要就讲讲SP的一些点.

SP容易造成的后果

SP的设计决定,SP在创建的时候,会把整个文件加载进内存,如果SP文件较大,明显会造成的两个严重问题:

  • 第一次获取数据的时候,阻塞主线程,造成卡顿,掉帧.
  • 解析SP的时候,产生大量的临时对象,导致频繁的GC.
  • 解析出来的Key和Value会一直存在于内存中,占用大量内存.

堵塞主线程

至于为什么会阻塞主线程,可以看下源码:
SP的实现类SharedPreferenceImpl中的getString()

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

可以看到有个awaitLoadedLocked()方法,这个方法又是什么意思呢:

private void awaitLoadedLocked() {
    while (!mLoaded) {
        try {
            wait();
     } catch (InterruptedException    unused) {
      }
    }
}

这是一个锁,所以,调用getString(),就会造成主线程等待.

占用内存

上面说到,不被释放掉.这个问题需要到ContextImpl类中,在调用getSharedPreference的时候,会把所有sp放到一个静态变量里面存起来

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是static的,它保存了你所有使用的sp,所以加载的键值对都在内存中.

commit和apply

最近项目中使用sp存储了比较多类型的状态值,所以采用了sp的方式,采用了apply的方式提交.后来发现,某些读取的操作执行出来的结果不正确,一直以为是系统休眠导致,后台偶然注意到sp的提交方式.将apply改成commit就正常了.所以在想,为什么用apply会出问题,而commit就正常了呢?

首先,先查了一下两者的区别:

  • apply没有返回值而commit返回boolean表明修改是否提交成功
  • apply是将修改数据原子提交到内存,而后异步真正提交到硬件磁盘;而commit是同步的提交到硬件磁盘,因此,在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内存,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率.
  • apply方法不会提示任何失败的提示

commit的官方的注释:

/**
* Commit your preferences changes back from this Editor to the
  * {@link SharedPreferences} object it is editing.  This atomically
  * performs the requested modifications, replacing whatever is currently
  * in the SharedPreferences.
  *
  * <p>Note that when two editors are modifying preferences at the same
  * time, the last one to call commit wins.
  *
  * <p>If you don't care about the return value and you're
  * using this from your application's main thread, consider
  * using {@link #apply} instead.
  *
  * @return Returns true if the new values were successfully written
  * to persistent storage.
  */
  boolean commit();

大概意思就是说,commit提交会返回一个修改后的结果,自动执行修改的请求,替换掉当前sp里的东西.当两个commit动作同时发生时,最后一个动作会成功.如果不考虑结果且在主线程可以用apply方法替代,如果成功写入磁盘则会返回

apply的官方注释

/**
* <p>Unlike {@link #commit}, which writes its preferences out
* to persistent storage synchronously, {@link #apply}
* commits its changes to the in-memory
* {@link SharedPreferences} immediately but starts an
* asynchronous commit to disk and you won't be notified of
* any failures.  If another editor on this
* {@link SharedPreferences} does a regular {@link #commit}
* while a {@link #apply} is still outstanding, the
* {@link #commit} will block until all async commits are
* completed as well as the commit itself.
*
* <p>As {@link SharedPreferences} instances are singletons within
* a process, it's safe to replace any instance of {@link #commit} with
* {@link #apply} if you were already ignoring the return value.
*
* <p>You don't need to worry about Android component
* lifecycles and their interaction with <code>apply()</code>
* writing to disk.  The framework makes sure in-flight disk
* writes from <code>apply()</code> complete before switching
* states.
*
* <p class='note'>The SharedPreferences.Editor interface
* isn't expected to be implemented directly.  However, if you
* previously did implement it and are now getting errors
* about missing <code>apply()</code>, you can simply call
* {@link #commit} from <code>apply()</code>.
*/
void apply();

这个注释写了很长,大概就是说apply采用的是异步处理,而不是commit的同步处理方式, 它会立即将更改提交到内存中,然后异步提交到硬盘,并且失败是没有任何提示.

接下来,看看这两种方式的代码实现:

SharedPreferencesImpl.Editor

 public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

public void apply() {
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
        public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };
        QueuedWork.add(awaitCommit);
        Runnable postWriteRunnable = new Runnable() {
            public void run() {
            awaitCommit.run();
            QueuedWork.remove(awaitCommit);
        }
    };
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);
}

这两个方法都是首先修改内存中缓存的map的值,然后将数据写到磁盘中,他们主要区别在于commit会等待写入磁盘后再返回,而apply则是在调用写磁盘操作后就返回了,但是这时候可能磁盘的数据还没被修改.

跨进程通信

@deprecated MODE_MULTI_PROCESS does not work reliably in
some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.

Note: This class does not support use across multiple processes.

乍一看,SP提供跨进程的FLAG-MODE_MULTI_PROCESS,但是文档也说明了,这个功能再某些Android版本上并不可靠,而且未来也不会提供支持,建议使用contentprovider.

看看他跨进程的实现:

ContextImpl的getSharedPreference()

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    checkMode(mode);
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            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;
}

可以看到,flag的作用就是保证在api11以前的系统上,如果内存里面有sp,再次获取的时候,有该flag,会重新读一遍文件.所以不要指望用sp去跨进程通信.

-------------看啥呢?没了-------------