之前就了解过SharedPreferences
的apply()
比commit()
的效率要高,因为apply的文件写操作是异步的,放到了一个后台线程中进行。官方文档也是建议我们使用单进程的SharedPreferences
时,尽量使用apply()
。并且系统会保证异步操作极端情况下(进程被系统回收等)也会执行。
* 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.
看到这里本准备高高兴兴地跑去和领导说,我们把项目里的commit
都换成apply
吧,官方文档都建议我们这么做啦。领导在开会,那就写个demon把操作的耗时数据对比下,说服力就更强啦。
于是写个小程序,计算一下两个方法连续执行1000次的耗时
private void spCommit() {
long time = System.currentTimeMillis();
SharedPreferences sp = getSharedPreferences("Test", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
for (int i = 0; i < 1000; i++) {
editor.putInt("number" + i, i).commit();
}
Log.d("Preference探索", "time commit cost:" + (System.currentTimeMillis() - time));
}
private void spApply() {
long time = System.currentTimeMillis();
SharedPreferences sp = getSharedPreferences("Test", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
for (int i = 0; i < 1000; i++) {
editor.putInt("number" + i, i).apply();
}
Log.d("Preference探索", "time apply cost:" + (System.currentTimeMillis() - time));
}
D/Preference探索: time commit cost:156
D/Preference探索: time apply cost:1092
D/Preference探索: time commit cost:86
D/Preference探索: time apply cost:1261
这个结果让我大吃一惊,换了手机还是如此。又被谷歌给忽悠了?心想SharePreference的代码一定是实习生写的,于是开始自己看源码。
最后定位在SharedPreferencesImpl
中enqueueDiskWrite这个方法中。
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
......省去若干行
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();//commit会在此处同步执行
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);//apply将事务抛到线程池中
}
writeToDiskRunnable
中是将editor中更新内容写到本地文件的核心io操作。很明显,commit操作是同步的,而apply直接将操作抛到了单线程的线程池中。
看到此处就更匪夷所思了,于是决定刨根究底,想到了某前辈介绍的Method Profiling功能,能查看系统方法的执行时间,决定进行初次使用。
Method Profiling功能在Android Device Monitor中
点击这个小按钮记录一段时间内方法的耗时,再点一下结束记录。
查看apply()
函数的耗时详情,发现有两个耗时的可疑点,我们一一进行定位。
进入commitToMemory的()
详情
发现居然时间都耗在一个HashMap的初始化函数上。
private MemoryCommitResult commitToMemory() {
......
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
mMap = new HashMap<String, Object>(mMap);
}
mcr.mapToWriteToDisk = mMap;
}
看到这里心中就有点数了,如果mDiskWritesInFlight > 0
(还存在没有被处理的apply()
操作),就需要copy出一个mMap进行后续操作,否则两个线程同时对一个HashMap进行读写操作就会引起crash。测试程序中连续的apply()
一定会导致前面的apply()
处理不完,后面的apply()
就只能开辟新的HashMap。
第二个耗时点
这个耗时是线程池调度的开销,可见把事务抛到后台线程也会有一定开销,并非一定是环保的。
了解了上面那些特性后,开始猜测,如果将apply()
分开操作,就不会因为前面有未完成的apply()
而被迫开辟新的HashMap空间。事实胜于雄辩,测试一下吧。
private void spDoSplit() {
SharedPreferences sp = getSharedPreferences("link", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
while (true) {
Random random = new Random();
for (int i = 0; i < 1000; i++) {
editor.putInt("number" + i, random.nextInt());
}
long time1 = System.currentTimeMillis();
editor.commit();
long time2 = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
editor.putInt("number" + i, random.nextInt());
}
long time3 = System.currentTimeMillis();
editor.apply();
Log.d("Preference探索", "commit cost:" + (time2 - time1) + ", apply cost:" + (System.currentTimeMillis() - time3));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
果不其然,结果如下:
D/Preference探索: commit cost:69, apply cost:4
D/Preference探索: commit cost:63, apply cost:2
D/Preference探索: commit cost:63, apply cost:4
D/Preference探索: commit cost:62, apply cost:9
D/Preference探索: commit cost:59, apply cost:2
平时写程序时,短时间内连续进行多个修改,没必要每个都进行apply()
,可以将它们合并成一个apply()
,在这种普遍情况下,apply()
优于commit()
是必然的,谷歌没有骗我,心中的疑惑解开啦:)
SharedPreference可以跨进程使用?
在上面研究中发现SharedPreferencesImpl
中读取xml文件的函数startLoadFromDisk()
,只在实例化SharedPreferencesImpl
以及通过Context.getSharedPreferences()(mode为Context.MODE_MULTI_PROCESS
)获取时才会执行。
也就是说,使用跨进程的SharedPreferences
时,每次读取操作都需要通过Context.getSharedPreferences()
拿一遍SharedPreferences
,才能保证及时读取到其他进程的改动。每次读操作都牵扯整个XML文件的读取。
写个小程序验证一下
Activity进程写值
private void changeNumberFrequently() {
final SharedPreferences sp = getSharedPreferences("link", Context.MODE_MULTI_PROCESS
| Context.MODE_WORLD_WRITEABLE
| Context.MODE_WORLD_READABLE);
Handler handler = new Handler() {
@Override public void handleMessage(Message msg) {
super.handleMessage(msg);
sp.edit().putInt("number", i).commit();
Log.d("Preference探索", "activity progress write number:" + i++);
sendEmptyMessageDelayed(0, 3000);
}
};
handler.sendEmptyMessage(0);
}
Service进程读取
没有每次读取SharedPreferences
的实例
@Override public int onStartCommand(Intent intent, int flags, int startId) {
final SharedPreferences sp = getSharedPreferences("link",
Context.MODE_MULTI_PROCESS | Context.MODE_WORLD_READABLE | Context.MODE_WORLD_READABLE);
final Handler handler = new Handler() {
@Override public void handleMessage(Message msg) {
super.handleMessage(msg);
//如果没有每次读取`SharedPreferences`的实例
int number = sp.getInt("number", -1);
Log.d("Preference探索", "service progress read number:" + number);
sendEmptyMessageDelayed(0, 3000);
}
};
handler.sendEmptyMessage(0);
return super.onStartCommand(intent, flags, startId);
}
D Preference探索: activity progress write number:0
D Preference探索: service progress read number:0
D Preference探索: activity progress write number:1
D Preference探索: service progress read number:0
D Preference探索: activity progress write number:2
D Preference探索: service progress read number:0
D Preference探索: activity progress write number:3
D Preference探索: service progress read number:0
改成每次读取SharedPreferences
的实例
@Override public int onStartCommand(Intent intent, int flags, int startId) {
final Handler handler = new Handler() {
@Override public void handleMessage(Message msg) {
super.handleMessage(msg);
//每次读取`SharedPreferences`的实例
final SharedPreferences sp = getSharedPreferences("link",
Context.MODE_MULTI_PROCESS | Context.MODE_WORLD_READABLE | Context.MODE_WORLD_READABLE);
int number = sp.getInt("number", -1);
Log.d("Preference探索", "service progress read number:" + number);
sendEmptyMessageDelayed(0, 3000);
}
};
handler.sendEmptyMessage(0);
return super.onStartCommand(intent, flags, startId);
}
D Preference探索: activity progress write number:0
D Preference探索: service progress read number:0
D Preference探索: activity progress write number:1
D Preference探索: service progress read number:1
D Preference探索: activity progress write number:2
D Preference探索: service progress read number:2
D Preference探索: activity progress write number:3
D Preference探索: service progress read number:3
简直弱爆了,怪不得官方文档已经废弃了跨进程使用SharePreferences
@Deprecated
public static final int MODE_MULTI_PROCESS = 0x0004;
关于跨进程数据的存储
根据个人的经验,想到以下几种方法
1 使用ContentProvider连接数据库是比较传统的方法,数据库自己有同步机制。
2 如果数据结构无法存进数据库,可以开辟一个独立进程进行文件读写,其他进程都绑定到这个进程进行读写。
3 文件锁,个人感觉坑会比较多,欢迎各位趟坑。