写在前面
最近看了不少大牛的推文,再加上最近工作的一些心得,发现做了这么久的Android开发,到最后还是最基础的知识点才是往高级进阶的重中之重。因此也萌生了做一个介绍基础知识的系列文章。目前来说先从support包里的一些控件,工具类开始然后延伸出去。
前几天写项目无意间IDE提示了SharedPreferencesCompat
类,然后就多看了两眼...
关于SharedPreferencesCompat
SharedPreferencesCompat是V4包里的类,讲真,这个类并没有什么卵用。它本身对SharedPreferences并没有什么操作,还是需要通过传入SharedPreferences.Editor
来实现数据的存储。
既然没有什么卵用,那为什么要出现这么一个鸡肋呢?还得从Editor.apply()
和Editor.commit()
的区别说起。这个后面再讲,先来看下SharedPreferencesCompat的用法
SharedPreferencesCompat的用法
var editor = getPreferences(Context.MODE_PRIVATE).edit()
editor.putString(DEFAULT_KEY,value_sharedpref.text.toString())
SharedPreferencesCompat.EditorCompat.getInstance().apply(editor)
上面是用kotlin写的,java的实现差不多。可以看到的是,还是需要获取Editor的对象,然后将键值对存放在Editor中,而SharedPreferencesCompat的使用仅仅是代替了最后一步apply
或者是commit
的操作。那是不是呢?答案是肯定的!
SharedPreferencesCompat源码解析
public void apply(@NonNull SharedPreferences.Editor editor) {
try {
editor.apply();
} catch (AbstractMethodError unused) {
// The app injected its own pre-Gingerbread
// SharedPreferences.Editor implementation without
// an apply method.
editor.commit();
}
}
核心的代码部分就是上面这些了,可以看到,实际上就是先调用了editor.apply()
方法,如果发生异常的话,则再调用editor.commit()
的方法。简单说就是优先使用apply()
方法。
Commit与Apply的对比
在看源码之前,我们先来分别实现下,来看下在时间效率上的差异
commit存储数据
var editor = getPreferences(Context.MODE_PRIVATE).edit()
for (i in 1..1000){
editor.putString(DEFAULT_KEY+i,"value$i")
}
var startTime = System.currentTimeMillis()
editor.commit()
Toast.makeText(this@SharedPreferencesCompatActivity,"commit use time ${System.currentTimeMillis()-startTime}",Toa
apply存储数据
var editor = getPreferences(Context.MODE_PRIVATE).edit()
for (i in 1..1000){
editor.putString(DEFAULT_KEY+i,"value$i")
}
var startTime = System.currentTimeMillis()
editor.apply()
Toast.makeText(this@SharedPreferencesCompatActivity,"apply use time ${System.currentTimeMillis()-startTime}",Toast.LENGTH_LONG).show()
在三星S6 edge plus上对比发现,1000条数据,commit大概需要10ms左右,而apply只需要1ms
居然有将近10倍的差距,那么commit和apply究竟是怎么实现的呢?
源码分析
SharedPreferences本身是一个接口,其实现的代码都在android.app.SharedPreferencesImpl
类中,而Editor的实现都在EditorImpl类中,这是SharedPreferencesImpl的内部类。commit 和 apply 函数本身并不复杂,先来看源码:
- commit
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;
}
- apply
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);
}
从上面的实现就可以看出为什么效率有这么大差距了,原因就是commit是同步的,而apply是异步执行的
commit的操作是在主线程中执行的,在enqueueDiskWrite
方法中,如果第二个参数为null,并且mDiskWritesInFlight
为1的时候,那么写操作则在当前线程中直接完成:
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
等写操作完成后再继续下面的步骤通知listener,最后返回写操作成功与否的标记
apply的实现是直接把写任务抛给了子线程,然后就通知listener,然后就没有然后了。所有的写操作都在子线程了,函数并不关心是否成功
这里有个疑问的地方,就是notifyListeners(mcr)
。之前一直认为apply的时候,写磁盘和notify是在两个线程同时运行的,应该会出现读取的时候,还没写入的情况;但实际测试中并没有出现这种情况。原因是因为在获取内容值的时候,会先判断当前的SharepdPreferences是否加载,加载了,则直接返回mMap
中的值,因为apply函数中在获取MemoryCommitResult
的时候,已经把需要修改的值放到mMap
中了,因此这个值并没有从磁盘里读出来,而是直接从内存里返回的。
主要的代码如下:
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() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}
}
可以看到,如果mLoaded==true
的话这个方法是直接跳过的;而mLoaded
的赋值是在loadFromDisk()
的方法里,而这个在SharedPreferencesImpl
初始化的时候,或者在调用getSharedPreferences
的时候都会调用的。因此回调里是可以接受到正确的值
总结
- 对于存储结果不需要太关注时,可以使用apply的方法,否则使用commit的方法
- 如果使用apply的方法,则可以考虑使用SharedPreferencesCompat来避免兼容的问题,毕竟apply的方法是API9 才添加的。
网友评论