SP推荐实践
1.在工作线程中写入sp时,直接调用commit就可以,不必调用apply,这种情况下,commit的开销更小
2.在主线程中写入sp时,不要调用commit,要调用apply
3.sp对应的文件尽量不要太大,按照模块名称去读写对应的sp文件,而不是一个整个应用都读写一个sp文件
4.sp的适合读写轻量的、小的配置信息,不适合保存大数据量的信息,比如长串的json字符串。
5.当有连续的调用PutXxx方法操作时(特别是循环中),当确认不需要立即读取时,最后一次调用commit或apply即可。
源码解析
apply 方法,首先创建了一个 awaitCommit 的 Runnable,然后加入到 QueuedWork 中,awaitCommit 中包含了一个等待锁,需要在其它地方释放。我们在上面看到的 QueuedWork.waitToFinish() 其实就是等待这个队列中的 awaitCommit 全部释放。
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);
}
然后通过 SharedPreferencesImpl.this.enqueueDiskWrite 创建了一个任务来执行真正的 SP 持久化。
commit 则会同步的执行 writeToFile,apply 则会将 writeToFile 加入到一个任务队列中异步的执行
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
writeToFile 执行完成会释放等待锁,之后会回调传递进来的第二个参数 Runnable 的 run 方法,并将 QueuedWork 中的这个等待任务移除。
- 总结来看,SP
调用 apply方法,会创建一个等待锁放到 QueuedWork 中,并将真正数据持久化封装成一个任务放到异步队列中执行,任务执行结束会释放锁。Activity onStop 以及 Service 处理 onStop,onStartCommand 时,执行 QueuedWork.waitToFinish() 等待所有的等待锁释放。
Android8.0对Sp的优化主要是有两个方面:
1、改变原来被动等待线程调度执行写入的方式,改为主动去调用,涉及主要方法是SharedPreferencesImpl.waitToFinish
2、 增加版本号控制的逻辑,原来是所有的提交都会执行写入磁盘一遍,现在是只执行最后、最新的提交写入磁盘,涉及的主要方法是:SharedPreferencesImpl.writeToFile
针对8.0以下优化:
public static void tryHackActivityThreadH() {
try {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentAtyThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
Object activityThread = currentAtyThreadMethod.invoke(null);
Field mHField = activityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
Handler handler = (Handler) mHField.get(activityThread);
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(handler,new SpCompatCallback());
Log.d(TAG,"hook success");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (Throwable e){
e.printStackTrace();
}
}
//自定义callbak:SpCompatCallback,在这个方法中做清理等待锁列表的操作:
public class SpCompatCallback implements Handler.Callback {
public SpCompatCallback(){
}
//handleServiceArgs
private static final int SERVICE_ARGS = 115;
//handleStopService
private static final int STOP_SERVICE = 116;
//handleSleeping
private static final int SLEEPING = 137;
//handleStopActivity
private static final int STOP_ACTIVITY_SHOW = 103;
//handleStopActivity
private static final int STOP_ACTIVITY_HIDE = 104;
//handlePauseActivity
private static final int PAUSE_ACTIVITY = 101;
//handlePauseActivity
private static final int PAUSE_ACTIVITY_FINISHING = 102;
@Override
public boolean handleMessage(Message msg) {
switch (msg.what){
case SERVICE_ARGS:
SpHelper.beforeSpBlock("SERVICE_ARGS");
break;
case STOP_SERVICE:
SpHelper.beforeSpBlock("STOP_SERVICE");
break;
case SLEEPING:
SpHelper.beforeSpBlock("SLEEPING");
break;
case STOP_ACTIVITY_SHOW:
SpHelper.beforeSpBlock("STOP_ACTIVITY_SHOW");
break;
case STOP_ACTIVITY_HIDE:
SpHelper.beforeSpBlock("STOP_ACTIVITY_HIDE");
break;
case PAUSE_ACTIVITY:
SpHelper.beforeSpBlock("PAUSE_ACTIVITY");
break;
case PAUSE_ACTIVITY_FINISHING:
SpHelper.beforeSpBlock("PAUSE_ACTIVITY_FINISHING");
break;
default:
break;
}
return false;
}
}
//清理等待列表的操作
public class SpHelper {
private static final String TAG = "SpHelper";
private static boolean init = false;
private static String CLASS_QUEUED_WORK = "android.app.QueuedWork";
private static String FIELD_PENDING_FINISHERS = "sPendingWorkFinishers";
private static ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = null;
public static void beforeSpBlock(String tag){
if(!init){
getPendingWorkFinishers();
init = true;
}
Log.d(TAG,"beforeSpBlock "+tag);
if(sPendingWorkFinishers != null){
sPendingWorkFinishers.clear();
}
}
private static void getPendingWorkFinishers() {
Log.d(TAG,"getPendingWorkFinishers");
try {
Class clazz = Class.forName(CLASS_QUEUED_WORK);
Field field = clazz.getDeclaredField(FIELD_PENDING_FINISHERS);
field.setAccessible(true);
sPendingWorkFinishers = (ConcurrentLinkedQueue<Runnable>) field.get(null);
Log.d(TAG,"getPendingWorkFinishers success");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Throwable e){
e.printStackTrace();
}
}
}
链接:www.jianshu.com/p/3f64caa56…
关联整理:
apply 与commit的对比
-
apply没有返回值, commit有返回值能知道修改是否提交成功
-
apply是将修改提交到内存,再异步提交到磁盘文件; commit是同步的提交到磁盘文件;
-
多并发的提交commit时,需等待正在处理的commit数据更新到磁盘文件后才会继续往下执行,从而降低效率; 而apply只是原子更新到内存,后调用apply函数会直接覆盖前面内存数据,从一定程度上提高很多效率。
获取SP与Editor:
-
getSharedPreferences()是从ContextImpl.sSharedPrefsCache唯一的SPI对象;
-
edit()每次都是创建新的EditorImpl对象.
优化建议:
-
强烈建议不要在sp里面存储特别大的key/value, 有助于减少卡顿/anr
-
请不要高频地使用apply, 尽可能地批量提交;commit直接在主线程操作, 更要注意了
-
不要使用MODE_MULTI_PROCESS;
-
高频写操作的key与高频读操作的key可以适当地拆分文件, 由于减少同步锁竞争;
-
不要一上来就执行getSharedPreferences().edit(), 应该分成两大步骤来做, 中间可以执行其他代码.
-
不要连续多次edit(), 应该获取一次获取edit(),然后多次执行putxxx(), 减少内存波动; 经常看到大家喜欢封装方法, 结果就导致这种情况的出现.
-
每次commit时会把全部的数据更新的文件, 所以整个文件是不应该过大的, 影响整体性能;