深入理解Android SharedPreferences的co
文章在说什么
Android的SharedPreferences在保存的时候会有commit和apply两个方法,这里主要解释下这两个方法详细区别,以及这两个方法的使用场景。
如果不关心具体实现,你可以直接记住结论,在不涉及到跨进程通过SharedPreferences共享数据的情况,就永远使用apply,apply方法执行后可以立即在任意地方读取到更新后对应的key值,不会出现因为apply是异步就需要等待一会儿,再读取的情况。
详细分析
Android提供了一个简单快捷的保存键值对到文件的类SharedPreferences。通过SharedPreferences读写参数,你大概会写出下面这样的代码。
private static void read(Context context) {
SharedPreferences sharedPreferences = context.getSharedPreferences("文件名", MODE_PRIVATE);
String result = sharedPreferences.getString("key", "default_value");
Log.d(TAG, "read: " + result);
}
private static void write(Context context) {
SharedPreferences.Editor editor = context.getSharedPreferences("文件名", MODE_PRIVATE).edit();
editor.putString("key", "value");
? editor.commit();
? editor.apply();
}
上面两个代码如果深入思考可能会遇到这样几个问题。
- 都知道SharedPreferences最终是保存到文件的,文件IO一般会是一个耗时操作。那么read的时候我们需不需要自己再做一层内存cache缓存数据提高读效率?
- 文件IO是个耗时操作,所以写方法问号那里有commit和apply两个方法,可能大家都知道一个是同步提交,一个是异步提交,而且IDE提示让我们用apply,那么这个异步的apply是否会影响到我们立即读数据?
/**
* 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 apply wins.
*
* <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();
上面是API25中,apply方法的注释。大致意思就是apply是一个原子请求(不需要担心多线程同步问题)。commit将同步的把数据写入磁盘和内存缓存。而apply会把数据同步写入内存缓存,然后异步保存到磁盘,可能会执行失败,失败不会收到错误回调。如果你忽略了commit的返回值,那么可以使用apply替换任何commit的实例。
简单说就是如果你不考虑保存失败的情况,那么你可以把所有使用commit的代码,替换成apply。
注释是这么写了,那如果我们要深究原因的话继续往下从源码一步步看。
我们获取SharedPreferences示例是从Context拿到的。Context有个ContextImpl的实现类。所以进入ContextImpl类看下SharedPreferences是如何创建的。
@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;
}
最终getShredPreferences会重载到上面这个方法。然后从getSharedPreferencesCacheLocked方法来获取SharedPreferences实例。
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;
}
因为SharedPreferences是支持自定义文件名的,所以这里的缓存是个Map。(这里有个比较奇怪的写法是用packageName又做了一个缓存。这是为什么呢?因为以前SharedPreferences是可以设置为MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
的。就是可以被跨App访问。Google为了方便跨App偷数据真是煞费苦心,后来Google就禁止使用这两个FLAG了,但是这个写法还是继续保留。)到这里,如果缓存没有需要的SharedPreferences,就创建一个SharedPreferencesImpl,最终总是可以拿到一个SharedPreferences实例,而且是个单例。所以无论从哪个Context最终都会得到同一个SharedPreferences实例。既然SharedPreferences的实现类是SharedPreferencesImpl,那我们去看下SharedPreferencesImpl的构造方法都干了什么。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
构造方法除了初始化一些基本参数外,调用了一个startLoadFromDisk方法。
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
......
map = XmlUtils.readMapXml(str);
......
mMap = map;
......
}
loadFromDisk代码比较多,但大致意思就是从磁盘把SharedPreferences文件里保存的xml信息读取到内存的Map里。
好,接着我们去看下实际保存数据的时候是怎么处理的。 保存的时候我们会用到Editor,Editor的实现类叫EditorImpl。
public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
......
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
......// 异步保存到磁盘,通知所有观察者有数据变化。
}
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
......// 同步保存到磁盘,通知所有观察者有数据变化,并返回执行结果。
return mcr.writeToDiskResult;
}
}
这时候我们再去看看读取是怎么处理的。
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
读取根本没有涉及到磁盘操作,直接从内存mMap里把数据读出来了。看到这里一下子就全部清楚了。
我们再重新梳理下整个过程。
- SharedPreferences是个单例,所以任意Context拿到的都是同一个实例。
- SharedPreferences在实例化的时候会把SharedPreferences对应的xml文件内容全部读取到内存。
- 对于非多进程兼容的SharedPreferences的读操作是从内存读取的,不涉及IO操作。写入的时候由于内存已经保存了完整的xml数据,然后新写入的数据也会同步更新到内存,所以无论是用commit还是apply都不会影响立即读取。
- 除非你需要关心xml是否写入文件成功,否则你应该在所有调用commit的地方改用apply。
- 我们需要对SharedPreferences在包装一层内存缓存来提高性能吗?完全不需要,因为SharedPreferences本身已经做了内存缓存。