- CompoundButton 源码分析
- LinearLayout 源码分析
- SearchView 源码解析
- LruCache 源码解析
- ViewDragHelper 源码解析
- BottomSheets 源码解析
- Media Player 源码分析
- NavigationView 源码解析
- Service 源码解析
- Binder 源码分析
- Android 应用 Preference 相关及源码浅析 SharePreferences 篇
- ScrollView 源码解析
- Handler 源码解析
- NestedScrollView 源码解析
- SQLiteOpenHelper/SQLiteDatabase/Cursor 源码解析
- Bundle 源码解析
- LocalBroadcastManager 源码解析
- Toast 源码解析
- TextInputLayout
- LayoutInflater 和 LayoutInflaterCompat 源码解析
- TextView 源码解析
- NestedScrolling 事件机制源码解析
- ViewGroup 源码解析
- StaticLayout 源码分析
- AtomicFile 源码解析
- AtomicFile 源码解析
- Spannable 源码分析
- Notification 之 Android 5.0 实现原理
- CoordinatorLayout 源码分析
- Scroller 源码解析
- SwipeRefreshLayout 源码分析
- FloatingActionButton 源码解析
- AsyncTask 源码分析
- TabLayout 源码解析
3.3 SharePreferences 内部类 Editor 实现 EditorImpl 分析
还记不记得 set 是在 SharePreference 接口的 Editor 接口中定义的,而 SharePreference 提供了 edit() 方法来获取 Editor 实例,我们先来看下这个 edit() 方法吧,如下:
public Editor edit() {
//握草!这也和异步 load 用的一把锁
synchronized (this) {
//阻塞等待,不解释吧,向上看。。。
awaitLoadedLocked();
}
//异步加载 OK 以后通过 EditorImpl 创建 Editor 实例
return new EditorImpl();
}
可以看见,SharePreference 的 edit() 方法其实就是阻塞等待返回一个 Editor 的实例(Editor 的实现是 EditorImpl),那我们就顺藤摸瓜一把,来看下这个 EditorImpl 这个类,如下:
public final class EditorImpl implements Editor {
//创建一个 mModified 的 key-value 集合,用来在内存中暂存数据
private final Map<String, Object> mModified = Maps.newHashMap();
//一个是否清除 preference 的 flag
private boolean mClear = false;
......//省略类似的 putXXX 方法
public Editor putBoolean(String key, boolean value) {
//同步锁操作
synchronized (this) {
//将我们要存储的数据放入 mModified 集合中
mModified.put(key, value);
//返回当前对象实例,方便这种模式的代码写法:putXXX().putXXX();
return this;
}
}
//不用过多解释,同步删除 mModified 中包含 key 的数据
public Editor remove(String key) {
synchronized (this) {
mModified.put(key, this);
return this;
}
}
//不解释,要清楚所有数据则直接置位 mClear 标记
public Editor clear() {
synchronized (this) {
mClear = true;
return this;
}
}
......
}
好了,到此你可以发现 Editor 的 setXXX 及 clear 操作仅仅只是将相关数据暂存到内存中或者设置好标记为,也就是说调运了 Editor 的 putXXX 后其实数据是没有存入 SharePreference 的。那么通过我们一开始的实例可以知道,要想将 Editor 的数据存入 SharePreference 文件需要调运 Editor 的 commit 或者 apply 方法来生效。所以我们接下来先来看看 Editor 类常用的 commit 方法实现原理,如下:
public boolean commit() {
//1.先通过 commitToMemory 方法提交到内存
MemoryCommitResult mcr = commitToMemory();
//2.写文件操作
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//阻塞等待写操作完成,UI 操作需要注意!!!所以如果不关心返回值可以考虑用 apply 替代,具体原因等会分析 apply 就明白了。
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
//3.通知数据发生变化了
notifyListeners(mcr);
//4.返回写文件是否成功状态
return mcr.writeToDiskResult;
}
我去,小小一个 commit 方法做了这么多操作,主要分为四个步骤,我们先来看下第一个步骤,通过 commitToMemory 方法提交到内存返回一个 MemoryCommitResult 对象。分析 commitToMemory 方法前先看下 MemoryCommitResult 这个类,具体如下:
// Return value from EditorImpl#commitToMemory()
//也是内部类,只是为了组织数据结构而诞生,也就是 EditorImpl.commitToMemory() 的返回值
private static class MemoryCommitResult {
public boolean changesMade; // any keys different?
public List<String> keysModified; // may be null
public Set<android.content.SharedPreferences.OnSharedPreferenceChangeListener> listeners; // may be null
public Map<?, ?> mapToWriteToDisk;
public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
public volatile boolean writeToDiskResult = false;
public void setDiskWriteResult(boolean result) {
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
回过头现在来看 commitToMemory 方法,具体如下:
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
//啥也不说,先整一个实例化对象
MemoryCommitResult mcr = new MemoryCommitResult();
//和 SharedPreferencesImpl 共用一把锁
synchronized (SharedPreferencesImpl.this) {
// 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);
}
//构造数据结构,把通过 SharedPreferencesImpl 构造函数里异步加载的文件 xml 解析结果 mMap 赋值给要写到 disk 的 Map
mcr.mapToWriteToDisk = mMap;
//增加一个未完成的写 opt
mDiskWritesInFlight++;
//判断有没有监听设置
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
//创建监听队列
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<android.content.SharedPreferences.OnSharedPreferenceChangeListener>(mListeners.keySet());
}
//再加一把自己的锁
synchronized (this) {
//如果调运的是 Editor 的 clear 方法,则这里 commit 时这么处理
if (mClear) {
//如果从文件里加载出来的 xml 不为空
if (!mMap.isEmpty()) {
//设置数据结构中数据变化标志为 true
mcr.changesMade = true;
//清空内存中 xml 数据
mMap.clear();
}
//处理完毕,标记复位,程序继续执行,所以如果这次 Editor 中如果有写数据且还未 commit,则执行完这次 commit 之后不会清掉本次写操作的数据,只会 clear 以前 xml 文件中的所有数据
mClear = false;
}
//mModified 是调运 Editor 的 setXXX 零时存储的 map
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.
//删除需要删除的 key-value
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
//把变化和新加的数据更新到 SharePreferenceImpl 的 mMap 中
mMap.put(k, v);
}
//设置数据结构变化标记
mcr.changesMade = true;
if (hasListeners) {
//设置监听
mcr.keysModified.add(k);
}
}
//清空 Editor 中零时存储的数据
mModified.clear();
}
}
//返回重新更新过 mMap 值封装的数据结构
return mcr;
}
到此我们 Editor 的 commit 方法的第一步已经完成,根据写操作组织内存数据,返回组织后的数据结构。接下来我们继续回到 commit 方法看下第二步—-写到文件中,其核心是调运 SharedPreferencesImpl 类的 enqueueDiskWrite 方法实现。具体如下:
//按照队列把内存数据写入磁盘,commit 时 postWriteRunnable 为 null,apply 时不为 null
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//创建一个 writeToDiskRunnable 的 Runnable 对象
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//真正的写文件操作
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
//写完一个计数器-1
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
//等会 apply 分析
postWriteRunnable.run();
}
}
};
//判断是同步写还是异步
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
//commit 方式走这里
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
//如果当前只有一个写操作
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//一个写操作就直接在当前线程中写文件,不用另起线程
writeToDiskRunnable.run();
//写完文件就返回
return;
}
}
//如果是 apply 就在线程池中执行
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
可以发现,commit 从内存写文件是在当前调运线程中直接执行的。那我们再来看看这个写内存到磁盘方法中真正的写方法 writeToFile,如下:
// Note: must hold mWritingToDiskLock
private void writeToFile(MemoryCommitResult mcr) {
if (mFile.exists()) {
if (!mcr.changesMade) {
//如果文件存在且没有改变的数据则直接返回写 OK
mcr.setDiskWriteResult(true);
return;
}
if (!mBackupFile.exists()) {
//如果要写入的文件已经存在,并且备份文件不存在时就先把当前文件备份一份,因为如果本次写操作失败时数据可能已经乱了,所以下次实例化 load 数据时可以从备份文件中恢复
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
//命名失败直接返回写失败了
mcr.setDiskWriteResult(false);
return;
}
} else {
//备份文件存在就把源文件删掉,因为要写新的
mFile.delete();
}
}
try {
//创建 mFile 文件
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
//创建失败直接返回写失败了
mcr.setDiskWriteResult(false);
return;
}
//把数据写入 mFile 文件
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
//彻底同步到磁盘文件中
FileUtils.sync(str);
str.close();
//设置文件权限 mode
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
//和刚开始实例化 load 时一样,更新文件时间戳和大小
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.
//写成功了 mFile 那就把备份文件直接删掉,没用了。
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()) {
//上面如果出错了就删掉,因为写之前已经备份过数据了,下次 load 时 load 备份数据
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
//写失败了
mcr.setDiskWriteResult(false);
}
回过头可以发现,上面 commit 的第二步写磁盘操作其实是做了类似数据库的事务操作机制的(备份文件)。接着可以继续分析 commit 方法的第三四步,很明显可以看出,第三步就是回调设置的监听方法,通知数据变化了,第四步就是返回 commit 写文件是否成功。
总体到这里你可以发现,一个常用的 SharePreferences 过程已经完全分析完毕。接下来我们就再简单说说 Editor 的 apply 方法原理,先来看下 Editor 的 apply 方法,如下:
public void apply() {
//有了上面 commit 分析,这个雷同,写数据到内存,返回数据结构
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
//等待写文件结束
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
//一个收尾的 Runnable
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
//这个上面 commit 已经分析过的,这里 postWriteRunnable 不为 null,所以会在一个新的线程池调运 postWriteRunnable 的 run 方法
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);
}
看到了吧,其实和 commit 类似,只不过他是异步写的,没在当前线程执行写文件操作,还有就是他不像 commit 一样返回文件是否写成功状态。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论