1、前言
SharedPreferences是 Android 中比较常用的存储方法,它可以用来存储一些比较小的键值对集合。内存采用HashMap来存储,文件采用xml格式来存储;
2、源码
2.1 SharedPreferences对象获取
context.getSharedPreferences(sharedName, Context.MODE_PRIVATE)
通过Context实例来获取,Context实例是ComtextImpl对象;流程和下面两个集合有关联
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
private ArrayMap<String, File> mSharedPrefsPaths;
流程大致如下:
- 若sp名字对应的文件在mSharedPrefsPaths,不存在,则创建,并存储;文件目录:应用目录下/shared_prefs/name.xml
- 若sp文件对应的SharedPreferencesImpl在sSharedPrefsCache中不存在,则创建,并缓存
- 返回新创建实例或者缓存中实例
2.2 SharedPreferences存储
其存储又分为内存存储和本地文件存储;相关变量如下:
private final File mFile;
private final File mBackupFile;
private Map<String, Object> mMap;
2.2.1 xml文件解析
在SharedPreferencesImpl进行实例化时通过调用startLoadFromDisk方法,从文件中通过XmlPullParser进行解析,解析值对存储在mMap中;
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
.................................
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
}
...........................
- 读取采用了锁机制,保证数据安全
- 采用文件备份,进行回退操作;这也可能存在丢失
- 通过XmlUtils.readMapXml工具方法进行读写;XmlUtils是不可见工具类
2.2.2 内存读写
读方法是通过SharedPreferencesImpl实例的getXXX系列方法;写方法是通过EditorImpl实例putXXX系列方法;
读数据如下面代码:首先要等待已经从文件解析完毕,然后从集合mMap冲获取结果
synchronized (mLock) {
awaitLoadedLocked();
Long v = (Long)mMap.get(key);
return v != null ? v : defValue;
}
写数据时:mModified为EditorImpl中HashMap实例,用来存储修改值对
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
2.2.2 提交到内存
有两种方法均可提交到内存,那就是apply、commit方法;这两个方法现在推荐使用applay方法
commit方法
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
if (DEBUG) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " committed after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
apply方法
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
区别就在mcr.writtenToDiskLatch.await()这句执行的环境,这个使用CountDownLatch锁机制,也就apply必须等待写任务执行完毕,才可以继续,而apply方法异步线程等待
commitToMemory方法:这个方法是根据EditorImpl中mModified内容来修改SharedPreferencesImpl中mMap集合内容;达到当前进程不死,就可以读到写的内容;但进程重新启动,可不一定;因为写入文件不一定操作成功
enqueueDiskWrite方法:其中调用writeToFile方法进行写入
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
.....................................
if (fileExists) {
................................................
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
............................
try {
FileOutputStream str = createFileOutputStream(mFile);
.............................
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
........................................
mcr.setDiskWriteResult(true, true);
..........................
return;
}
..............................
mcr.setDiskWriteResult(false, false);
}
主要方法内容大致如上:
- 使用工具XmlUtils.writeMapXml方法写回
- mcr.setDiskWriteResult中进行CountDownLatch锁释放,使apply和commit方法中同步或者异步代码继续执行
3、小结
- xml文件存储,多了很多无用信息
- 使用锁机制保证同步,同样也增加读写的成本
- 文件内容会载入内存,并一直存在;这可能造成内存溢出,也存在读写慢;因此,不可存入大量数据、每个sp中不要存过多的值对
其实sp的存储思想不难,其难点就是谁来使用都稳定;但这个稳定在中小型项目或者有些场景很多是不需要考虑的;我们可以从以下几个方面进行优化:
- 改变sp存储格式、减少文件大小
- 较少sp文件读写次数
- 分为需要线程安全、非线程安全场景;较少锁竞争造成的资源开销
我根据sp原理写了一个简单、高效的存储框架:这个框架未进行线程、进程处理,个人认为在中小项目中基本够用了
技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给与关注和点赞;如果文章存在错误,也请多多指教!