Android提供了SharedPreferences用于保存应用少量,且格式简单(key-value)的数据,如配置信息等。
SharedPreferences与Editor简介
SharedPreferences接口主要负责读取应用程序的Preferences数据,主要方法如下:
- getXxx(String key, xxx defValue):获取SharedPreferences数据里制定key对应的value,如果该key不存在,则返回默认值defValue。其中Xxx可以是boolean、float、int、long、String等基本类型的值;
- boolean contains(String key):判断SharedPreference中是否包含特定key的数据;
- Map<String, ?> getAll():获取SharedPreferences数据里的全部key-value对;
但SharedPreferences接口并未提供写入数据的能力,通过SharedPreferences.Editor才允许写入,SharedPreferences通过 edit() 方法即可获取对应的 Editor 对象,其中包含以下写入方法: - Editor putXxx(String key, @Nullable xxx value):向SharedPreferences中存入指定key对应的数据value,其中Xxx可以是boolean、float、int、long、String等基本类型;
- Editor remove(String key):删除指定key对应的数据项;
- Editor clear():清空SharedPreference中的所有数据;
- boolean commit():当前线程提交修改;
- void apply():后台提交修改;
SharedPreferences本身是一个接口,应用无法直接创建SharedPreferences实例,只能通过Context的 getSharedPreferences(String name, @PreferencesMode int mode) 方法来获取SharedPreferences实例,其中第二个参数一般为 Context.MODE_PRIVATE:默认模式,仅支持本应用或是同属userID的所有应用读写,其他 MODE_WORLD_WRITEABLE/ MODE_WORLD_READABLE 已注释 @Deprecated,这两种模式允许其他应用写/读本应用创建的数据,易导致安全漏洞。
SharedPreferences使用
普遍性的使用方法如下所示,在MainActivity创建时提示当前已使用多少次,流程如下:获取SharedPreferences对象->获取该SharedPreferences的Editor对象->通过editor来取出/存放值->提交修改。
public class MainActivity extends AppCompatActivity {
private SharedPreferences preferences;
private SharedPreferences.Editor editor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
preferences = getSharedPreferences("monster", Context.MODE_PRIVATE);
editor = preferences.edit();
int count = preferences.getInt("count", 0);
editor.putInt("count", ++count);
Toast.makeText(this,"当前应用已经被使用"+count+"次!",Toast.LENGTH_SHORT).show();
editor.apply();
}
SharedPreferences数据保存在当前应用的shared_prefs目录下,文件内容如下格式:根元素为<map…/>,该元素下每一个子元素代表一个key-value对,当value为Integer时,使用<int…/>子元素,以此类推。
原理概览
整个框架较为简单,主要分为如下几个部分:
- APP尝试获取SP时,会从cache加载,首次则会新建SharedPreferencesImpl对象,所有的XML键值队会加载到本地变量mMap中;
- APP尝试获取value时,会直接从mMap中获取value;
- APP尝试改变value时,会将修改提交到EditorImpl的mModified中;
- APP尝试提交修改时,会经由EditorImpl去做进一步的修改,后文详述逻辑;
此处值得注意的是getXxx()和putXxx()操作的对象不一样,因此信息来源/去向也不一样,分别是SharedPreferencesImpl和EditorImpl对象下的成员变量,因此即使已经修改了value值,在真正的提交修改请求前,拿到的都会是初始值。Demo如下所示:
private void onClick(View view) {
switch (view.getId()){
case R.id.get:
Log.d("MONSTER_DEBUG","Now try to get Value");
Log.d("MONSTER_DEBUG", String.valueOf(preferences.getInt("test", 0)));
break;
//put并未真正的提交
case R.id.put:
Log.d("MONSTER_DEBUG","Now try to put Value");
int value = preferences.getInt("test", 0);
Log.d("MONSTER_DEBUG", "previous calue = " + value + " and now set to " + (value+1));
editor.putInt("test",value+1);
break;
case R.id.commit:
Log.d("MONSTER_DEBUG","Now try to commit Value");
editor.commit();
break;
default:
break;
}
}
commit()与apply()
commit()和apply()均为提交修改的方法,二者在提交时机存在一定的差异,但流程大体一致:
- 先调用 commitToMemory(), 将数据同步到 SharedPreferencesImpl 的 mMap, 并保存到 MemoryCommitResult 的 mapToWriteToDisk,
- 再调用 enqueueDiskWrite(), 写入到磁盘文件; 先之前把原有数据保存到 .bak 为后缀的文件,用于在写磁盘的过程出现任何异常可恢复数据;
commitToMemory()
方法的主要功能: 把 EditorImpl数据更新到 SharedPreferencesImpl
● 将 mMap 信息赋值给 mapToWriteToDisk, 并 mDiskWritesInFlight 加 1;
● 当 mClear 为 true, 则直接清空 mMap;
● 当 value 值为 this 或 null, 则移除相应的 key;
● 当 value 值发生改变, 则会更新到 mMap;
● 只要有 key/value 发生改变(新增, 删除), 则设置 mcr.changesMade = true. 最后会清空EditorImpl 中的 mModified数据
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
//mMap是SharedPreferencesImpl的成员变量HashMap,记录了XML文件的键值对
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
//是否有监听key改变的监听者
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
//mClear是EditorImpl下的一个成员变量,当且仅当调用clear()方法时才会设置为true,此处当mClear为true时,直接清空mapToWriteToDisk并重新将mClear置为false
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
keysCleared = true;
mClear = false;
}
//mModified是EditorImpl下的一个成员变量,所有的putXxx动作都会将键值对暂时保存在此处
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
//在remove()方法中,实际为mModified.put(key, this),当v为this或为null时,表示需要移除该key
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
//一次提交之前可能有多次修改,判断是否需要覆盖之前的修改
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
//目前提交的所有改变已经同步到map3ToWriteToDisk中,清空mModified
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
enqueueDiskWrite()
enqueueDiskWrite()方法则封装了一个Runnable对象,如果是commit请求则直接调用run()方法在当前线程执行,反之如果是apply请求则添加到QueueWork的队列中延后执行。
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();
}
}
};
//apply和commit的差异点,对于commit为true
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
//mDiskWritesInFlight在commitToMemory会自增
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//正常情况下commit直接在此当前线程执行
writeToDiskRunnable.run();
return;
}
}
//apply或commit特殊情况下会添加到Queue Work队列
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
writeToFile()
//代码量较大,删除了局部变量及debug相关
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
//mFile是SharedPreferencesImpl的成员变量,初始化为getSharedPreferences的第一个入参即文件名
boolean fileExists = mFile.exists();
// Rename the current file so it may be used as a backup during the next read
if (fileExists) {
boolean needsWrite = false;
// Only need to write if the disk state is older than this commit
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
// No need to persist intermediate states. Just wait for the latest state to
// be persisted.
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
//wasWritten及writeToDiskResult
mcr.setDiskWriteResult(false, true);
return;
}
//mBackupFile是初始化SharedPreferencesImpl对象时创建的一个以文件名开头的bak文件
boolean backupFileExists = mBackupFile.exists();
//备份文件不存在时,mFile重命名为备份文件
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {
mFile.delete();
}
}
// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
//将mapToWriteToDisk写下并sync
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
// 成功写入,删除backup文件
mBackupFile.delete();
mDiskStateGeneration = mcr.memoryStateGeneration;
mcr.setDiskWriteResult(true, true);
long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add((int) fsyncDuration);
mNumSync++;
//正常路径,写入并返回
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// 发生异常
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
}
QueuedWork.queue()
所有添加到QueuedWork的任务都是交由一个名为“queued-work-looper”的handler线程完成。
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
//添加到sWork队列
sWork.add(work);
//并向handler发送RUN信息,sCanDelay默认为true,仅waitToFinish会临时设置为false
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
private static class QueuedWorkHandler extends Handler {
static final int MSG_RUN = 1;
QueuedWorkHandler(Looper looper) {
super(looper);
}
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
//只有一个活儿,拿到RUN信息去处理work
processPendingWork();
}
}
}
processPendingWork()
private static void processPendingWork() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = sWork;
sWork = new LinkedList<>();
//因为处理的是sWork的所有任务,移除其他的RUN MSG
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
if (DEBUG) {
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+(System.currentTimeMillis() - startTime) + " ms");
}
}
}
}
WaitToFinish()
在handleServiceArgs()/handleStopService()/ handlePauseActivity()/ handleStopActivity()时,会在应用主线程调用WaitToFinish()方法阻塞等待所有SharedPreferences同步完成。
public static void waitToFinish() {
long startTime = System.currentTimeMillis();
boolean hadMessages = false;
Handler handler = getHandler();
synchronized (sLock) {
//前面说过每添加一个work时,会(延迟)添加一个MSG_RUN,既然此处主线程执行,移除所有的MSG
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
if (DEBUG) {
hadMessages = true;
Log.d(LOG_TAG, "waiting");
}
}
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
synchronized (sLock) {
long waitTime = System.currentTimeMillis() - startTime;
if (waitTime > 0 || hadMessages) {
mWaitTimes.add(Long.valueOf(waitTime).intValue());
mNumWaits++;
if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
mWaitTimes.log(LOG_TAG, "waited: ");
}
}
}
}
由上,主线程的WaitToFinish()方法和SharedPreferences的正常逻辑存在交叉processPendingWork(),且该方法是持锁的,因此在IO压力较大的情况下,可能存在等锁导致的ANR问题。
SP同步导致的ANR
如下所示,主线程在stop Activity时需等待SP同步完成,而processPendingWork()方法是持锁的,因此若SP在Handler线程同步耗时,会导致主线程等锁导致input类型的ANR。同理也可能出现processPendingWork()耗时导致input类型的ANR或是Service类型的ANR。
04-22 19:50:13.190 1000 2265 32284 I am_anr : [0,16321,com.miui.home,819576389,Input dispatching timed out (5f69703 GestureStubHome (server) is not responding. Waited 5000ms for MotionEvent(deviceId=8, eventTime=9850953060000, source=0x00001002, displayId=0, action=DOWN, actionButton=0x00000000, flags=0x00100000, metaState=0x00000000, buttonState=0x00000000, classification=NONE, edgeFlags=0x00000000, xPrecision=10.0, yPrecision=10.0, xCursorPosition=nan, yCursorPosition=nan, pointers=[0: (600.5, 2398.5)]), policyFlags=0x62000000)]
04-22 19:50:16.556 1000 2265 32284 I am_kill : [0,16321,com.miui.home,100,bg anr]
04-22 19:50:16.086 10085 16321 16321 I dvm_lock_sample: [com.miui.home,1,main,8774,QueuedWork.java,264,void android.app.QueuedWork.processPendingWork(),-,285,void android.app.QueuedWork.processPendingWork(),16427]
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x7286eb68 self=0xb400007db49ddc00
| sysTid=16321 nice=0 cgrp=foreground sched=0/0 handle=0x7db60164f8
| state=S schedstat=( 20469752064 6186697285 46289 ) utm=1497 stm=549 core=4 HZ=100
| stack=0x7fe5458000-0x7fe545a000 stackSize=8188KB
| held mutexes=
at android.app.QueuedWork.processPendingWork(QueuedWork.java:264)
- waiting to lock <0x07860975> (a java.lang.Object) held by thread 19
at android.app.QueuedWork.waitToFinish(QueuedWork.java:186)
at android.app.ActivityThread.handleStopActivity(ActivityThread.java:5325)
at android.app.servertransaction.StopActivityItem.execute(StopActivityItem.java:43)
at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2260)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:210)
at android.os.Looper.loop(Looper.java:299)
at android.app.ActivityThread.main(ActivityThread.java:8108)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:556)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1045)
"queued-work-looper" prio=5 tid=19 Native
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x13000808 self=0xb400007ce35e3400
| sysTid=16427 nice=-2 cgrp=foreground sched=0/0 handle=0x7c87bd5cb0
| state=D schedstat=( 109318025 445833169 722 ) utm=3 stm=7 core=5 HZ=100
| stack=0x7c87ad2000-0x7c87ad4000 stackSize=1039KB
| held mutexes=
native: (backtrace::Unwind failed for thread 16427: Thread has not responded to signal in time)
at java.io.FileDescriptor.sync(Native method)
at android.os.FileUtils.sync(FileUtils.java:263)
at android.app.SharedPreferencesImpl.writeToFile(SharedPreferencesImpl.java:807)
at android.app.SharedPreferencesImpl.access$900(SharedPreferencesImpl.java:59)
at android.app.SharedPreferencesImpl$2.run(SharedPreferencesImpl.java:672)
- locked <0x03bac7f1> (a java.lang.Object)
at android.app.QueuedWork.processPendingWork(QueuedWork.java:277)
- locked <0x07860975> (a java.lang.Object)
at android.app.QueuedWork.access$000(QueuedWork.java:56)
at android.app.QueuedWork$QueuedWorkHandler.handleMessage(QueuedWork.java:297)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:210)
at android.os.Looper.loop(Looper.java:299)
at android.os.HandlerThread.run(HandlerThread.java:67)
借用今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待中的示意图(Android版本较低,但原理基本一致):造成这个问题的根源就是太多 pending 的 apply 行为没有写入到文件,主线程在执行到指定消息的时候会有等待行为,等待时间过长就会出现 ANR。
因此,对于APP开发者而言,务必仅使用SharedPreferences保存轻量级的信息,而对于系统开发者而言,在IO压力大sync耗时时,可考虑直接skip掉waitToFinish()方法。