1.实现类
SharedPreferences 只是一个接口,其实现类是SharedPreferencesImpl。
工作流程分析:
创建sp 的时候,会去查看是否有bak文件,如果有的话,把bak文件,重命名成file的真正文件名,读取到内存。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
如果在读取的过程中,你调用了getString,那么该方法会等到io 读取到map 完成,返回结果。
2.getString 会直接从磁盘里面直接读取吗?
不会,会从内存里面读取。
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
3.apply 和 commit 的 区别。
apply 不会立马写在磁盘里面,commit 会的。
关键位置是SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
我们看下这个方法:
/**
* Enqueue an already-committed-to-memory result to be written
* to disk.
*
* They will be written to disk one-at-a-time in the order
* that they're enqueued.
*
* @param postWriteRunnable if non-null, we're being called
* from apply() and this is the runnable to run after
* the write proceeds. if null (from a regular commit()),
* then we're allowed to do this disk write on the main
* thread (which in addition to reducing allocations and
* creating a background thread, this has the advantage that
* we catch them in userdebug StrictMode reports to convert
* them where possible to apply() ...)
*/
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);
}
如果是commit 的话,writeToDiskRunnable.run(); 写文件的这个操作直接进行。如果不是的话,会放到一个队列里面去执行。QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
疑问:
如果我apply 是异步进行的,那么为什么我putString (“aaa”,“111”)之后调用apply ,立马就getString(“aaa”,"")能够返回正确的结果呢?
因为apply 和commit 一样,都会先把改动保存到内存,然后写到文件里面。
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// 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);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that 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);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
mapToWriteToDisk = mMap;
这一句话就把mMap 赋值给mapToWriteToDisk 。而所有的改动都会在这个mapToWriteToDisk 上去修改。其实最终修改的就是mMap。所以不需要等到写到文件里面,你就可以拿到正确的结果。
4.apply 的实现方法
比如当我们调用sp.setString().apply 的时候,首先会把你设置的String 提交到内存里面,也就是map 里面。
然后会调用QueuedWork.addFinisher(awaitCommit);
把这个等待的runnable 添加到QueueWork 的finish 队列里。
等待的Runnable 代码如下:
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
然后把写文件的runnable 放入QueueWork 的队列里面。QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
在QueueWork 里面会发送一个延迟100ms 的消息,在消息里面会去处理写文件的Runnable。
写文件成功之后,会把等待的Runnable 从QueueWork 的 finisher 里面移除。QueuedWork.removeFinisher(awaitCommit);
在ActivityThread hanlderStopActivity 的时候,会调用QueueWork 的waitTofinish() 方法,等待所有的apply 的写文件完成。
commit 的实现
commit 的实现比较简单,当我们commit 的时候,会把写文件的runnable 发送到QueueWork 的 队列里,所以就算是commit 写文件也不是在主线程写的。但是commit 方法会调用mcr.writtenToDiskLatch.await();
去等待QueueWork 写文件完成。
5.设计优缺点
6.SP设计里面的备份文件
SP会涉及两个文件,一个真正的文件,一个备份文件。
如果sp 改变了,是要写文件的话,如果当前的sp 文件file存在,首先把文件真正的file重命名为file.bak.
如果重命名不成功的话,整个操作以失败告终。
接着创建fileoutputstream.把map 写进去,改文件权限。
如果整个操作成功,那么把备份文件删除掉。如果失败,把file 文件删除掉。
// Note: must hold mWritingToDiskLock
private void writeToFile(MemoryCommitResult mcr) {
// Rename the current file so it may be used as a backup during the next read
if (mFile.exists()) {
if (!mcr.changesMade) {
// If the file already exists, but no changes were
// made to the underlying map, it's wasteful to
// re-write the file. Return as if we wrote it
// out.
mcr.setDiskWriteResult(true);
return;
}
if (!mBackupFile.exists()) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(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);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (this) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
mcr.setDiskWriteResult(true);
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// Clean up an unsuccessfully written file
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false);
}
下次进来去读文件的时候,如果back 文件存在,直接把file 文件删除掉,并且把back 文件,重新命名成file.
private void loadFromDiskLocked() {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
总之,所有正确的都以bak 文件为准。
7.wait notify 的使用
8.SharedPreferences 支持多进程吗?
不知道,如果是多进程,可能在一个进程里面写的值,被另外一个进程都给冲掉了。
9.apply 是完全异步的吗?会不会导致ANR?
"main@10722" prio=5 tid=0x2 nid=NA waiting
java.lang.Thread.State: WAITING
at sun.misc.Unsafe.park(Unsafe.java:-1)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:868)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1023)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1334)
at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:232)
at android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:466)
at android.app.QueuedWork.waitToFinish(QueuedWork.java:194)
at android.app.ActivityThread.handleStopActivity(ActivityThread.java:4318)
at android.app.servertransaction.StopActivityItem.execute(StopActivityItem.java:41)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:145)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1872)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6743)
at java.lang.reflect.Method.invoke(Method.java:-1)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:486)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:882)
我们看这段堆栈,发现当activity onStop 的时候,会执行QueuedWork.waitToFinish()方法。这个QueuedWork 类,是供SP apply 异步写文件的一个类,里面会有HandlerThread去负责写文件。
waitToFinish 方法会把没有执行的所有runnable,放到主线程执行。所以handleStopActivity 会等待所有的apply 没有完成的runnable 去执行完成。所以,apply 并不是说完全异步的。也有可能导致ANR。但是,apply 这种只会在调用waitToFinish() 的场景才会触发ANR. 如果一个点击事件,如果里面处理的很多的业务逻辑,最后调用了commit 方法,那么有可能因为commit 产生ANR,但是不会因为apply 产生ANR.
我们看下QueueWork 所有waitToFinish() 方法调用的地方:
我们发现基本上都在ActivityThread 这个类里面。
模拟apply 产生ANR:
Class<?> aClass = null;
try {
aClass = Class.forName("android.app.QueuedWork");
Method addFinisher = aClass.getMethod("addFinisher", Runnable.class);
if (addFinisher != null) {
addFinisher.invoke(null, new Runnable() {
@Override
public void run() {
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
我们可以通过以上代码,在QueueWork 里面添加一个finisher,然后点击手机back键,会发现会卡在onStop 方法那里。
总结:
1.面试不会面试业务代码,他们根本不熟悉,只会面android 源码 相关的问题。而sp 从来没有看过。所以很多问题不知道。