Android(安卓)SharedPreferences 详解 源码解析
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 keysModified = null; Set listeners = null; Map 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(mMap); } mapToWriteToDisk = mMap; mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0; if (hasListeners) { keysModified = new ArrayList(); listeners = new HashSet(mListeners.keySet()); } synchronized (mEditorLock) { boolean changesMade = false; if (mClear) { if (!mapToWriteToDisk.isEmpty()) { changesMade = true; mapToWriteToDisk.clear(); } mClear = false; } for (Map.Entry 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 从来没有看过。所以很多问题不知道。
更多相关文章
- Android(安卓)SQLite 数据库存储详解
- android usb大体流程解析
- Android(安卓)3D开发,OpenGL ES 的使用(一)
- [Android(安卓)Pro] Scroller使用分析
- Android(安卓)pcm文件播放方法(AudioTrack)
- Android(安卓)dex分包方案
- Android获取各种路径方法以及对路径的解释
- Android中SharedPreferences存储数据
- Android(安卓)studio cannot resolve symbol xxx的解决方法