概述
SharedPreferences
作为Android系统的轻量级数据存储方式之一,能够比较方便的存取一些简单的临时数据,现回顾下使用流程及实现原理。
调用方式
//Context.MODE_PRIVATE表示该文件只能被创建他的应用访问
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
//存入数据,以存入int类型数据为例
editor.putInt(key, value);
editor.commit();
//editor.apply();如果不关心返回结果,可以使用该方法进行异步提交
//取出数据,如果不存在返回设置的默认数据
int value = sp.getInt(key, defValue);
对于context.getSharedPreferences()
方法的注释如下:
根据文件名来获取并保存内容,返回一个SharedPreferences对象,您可以通过它来获取和修改它的值。 如果多个调用者使用同样的名字进行调用,那么只会返回一个SharedPreferences实例,意味着他们会看到彼此编辑的内容
存储方式
由于SharedPreferences
本质上使用的是文件存储,存储位置在
/data/data/<程序包名>/shared_prefs/文件名.xml
,能够保存基本的数据类型,基本格式如下:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="key">value</string>
</map>
实现原理
首先看看context.getSharedPreferences()
是如何获取的,context
的真正实现类为ContextImpl
:
class ContextImpl extends Context {
//使用全局的静态Map对象来保存SharedPreferencesImpl的所有实现
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
//文件名和文件存储的Map对象
private ArrayMap<String, File> mSharedPrefsPaths;
//...省略代码
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
//...省略代码
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {//如果文件没有,生成一个
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {//同样的如果缓存中找不到sp,那么创建一个
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// If somebody else (some other process) changed the prefs
// file behind our back, we reload it. This has been the
// historical (if undocumented) behavior.
//如果小于3.0版本或者多进程,将重新加载刷新数据
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
//根据包名来获取
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {//没有的话新建一个放入进去
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
//...省略代码
}
上面就是获取SharedPreferences
对象的主要流程了,主要使用了全局的静态Map
保存相关引用。
之后来看看’SharedPreferences’的真正实现,其实SharedPreferences
本身是一个接口,真正的实现类是SharedPreferencesImpl
,首先看看其构造方法:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);//备份文件,用于备份和恢复内容
mMode = mode;
mLoaded = false;
mMap = null;//内存缓存
startLoadFromDisk();//从磁盘加载存储的内容
}
构造方法里面创建了mMap
的内存缓存,后面存放及获取值的时候都是从这个缓存里面维护的,
startLoadFromDisk()
就是从文件读取存储内容赋值给mMap
的,看看其实现:
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();//在新的线程中进行内容的读取
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;//如果正在加载就reture
}
if (mBackupFile.exists()) {//如果备份文件存在就使用备份文件
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {//从文件读取存储内容
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);//将内容读取出来赋值给map
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (mLock) {
mLoaded = true;
if (map != null) {
mMap = map;//将内容再次赋值给mMap对象,这样mMap将负责在内存中的数据维护
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {//如果没有内容的话,就直接创建一个Map对象
mMap = new HashMap<>();
}
mLock.notifyAll();
}
}
SharedPreferences.edit()
可以返回一个Editor
对象,而Editor
的接口实现类为EditorImpl
:
public final class EditorImpl implements Editor {
private final Object mLock = new Object();
//存入的内容首先会放入这个mModified的Map对象中
@GuardedBy("mLock")
private final Map<String, Object> mModified = Maps.newHashMap();
//clear标志
@GuardedBy("mLock")
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
synchronized (mLock) {
mModified.put(key, value);//将key,value放入
return this;
}
}
//...省略代码
public Editor remove(String key) {
synchronized (mLock) {
mModified.put(key, this);//移除key的话,先存入自身对象
return this;
}
}
public Editor clear() {
synchronized (mLock) {
mClear = true;//clear内容的话,首先将mClear置为true
return this;
}
}
}
之后看看commit()
方法的实现:
public boolean commit() {
//...省略代码
//首先是提交到内存,也就是将内容先提交到mMap中
MemoryCommitResult mcr = commitToMemory();
//之后再写入文件
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
//...省略代码
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
接着看看commitToMemory()
方法的实现:
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
//这里如果mDiskWritesInFlight>0说明正在进行写入文件操作,此时
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 (mLock) {
boolean changesMade = false;
//如果之前调用了clear()方法,那么先清空mMap
if (mClear) {
if (!mMap.isEmpty()) {
changesMade = true;
mMap.clear();
}
mClear = false;
}
//从mModified中循环取出值开始对比
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.
//如果设定的null或者this对象,那么说明是移除这个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;
}
}
mMap.put(k, v);//更新mMap
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();//清空
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
apply()
方法的实现区别:
public void apply() {
final long startTime = System.currentTimeMillis();
//同样是先将数据同步到内存缓存
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
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);
//该任务在文件写入完毕后才执行,主要利用了mcr对象中的CountDownLatch
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
//之后同样是写入文件
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);
}
最后都调用到的SharedPreferences.enqueueDiskWrite()
方法实现:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//根据postWriteRunnable参数来判断是同步提交还是异步提交
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
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) {//如果是同步提交,也就是调用的commit()提交的话,那么会走到这里
boolean wasEmpty = false;
synchronized (mLock) {//之前commitToMemory()方法中mDiskWritesInFlight++了已经
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {//这里直接在当前线程运行任务,之后reture
writeToDiskRunnable.run();
return;
}
}
//而调用的apply()方法会使用异步提交
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
再看下writeToFile()
方法,这里面实现了真正的写入文件方法:
// Note: must hold mWritingToDiskLock
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
long startTime = 0;
long existsTime = 0;
long backupExistsTime = 0;
long outputStreamCreateTime = 0;
long writeTime = 0;
long fsyncTime = 0;
long setPermTime = 0;
long fstatTime = 0;
long deleteTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
boolean fileExists = mFile.exists();
if (DEBUG) {
existsTime = System.currentTimeMillis();
// Might not be set, hence init them to a default value
backupExistsTime = existsTime;
}
// 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;
}
}
}
}
//如果不需要提交那么直接reture
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
//判断备份文件是否存在
boolean backupFileExists = mBackupFile.exists();
if (DEBUG) {
backupExistsTime = System.currentTimeMillis();
}
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 (DEBUG) {
outputStreamCreateTime = System.currentTimeMillis();
}
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
//写入文件
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
//同步文件
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
if (DEBUG) {
setPermTime = System.currentTimeMillis();
}
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
if (DEBUG) {
fstatTime = System.currentTimeMillis();
}
// Writing was successful, delete the backup file if there is one.
//写入成功就删除备份
mBackupFile.delete();
if (DEBUG) {
deleteTime = System.currentTimeMillis();
}
mDiskStateGeneration = mcr.memoryStateGeneration;
//只有走到这里标识写入成功
mcr.setDiskWriteResult(true, true);
if (DEBUG) {
Log.d(TAG, "write: " + (existsTime - startTime) + "/"
+ (backupExistsTime - startTime) + "/"
+ (outputStreamCreateTime - startTime) + "/"
+ (writeTime - startTime) + "/"
+ (fsyncTime - startTime) + "/"
+ (setPermTime - startTime) + "/"
+ (fstatTime - startTime) + "/"
+ (deleteTime - startTime));
}
long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add((int) fsyncDuration);
mNumSync++;
if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
}
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, false);
}
上面就是整个提交过程,主要流程就是先提交到内存,然后区别是同步还是异步提交。
取值的话就非常简单了,看看SharedPreferencesImpl
中的实现:
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();//主要就是根据mLoaded来判断,等待数据从文件加载到内存之后才能读取
Integer v = (Integer)mMap.get(key);//直接从mMap中取出值
return v != null ? v : defValue;
}
}
存储对象
由于SharedPreferences
只能保存基本数据类型,那么对于对象的保存,可以将对象转换为json
字符串再进行保存,参考文章:
Android本地最简单的数据存储,没有之一(让SharedPreferences存取JavaBean对象或List)
但是不建议保存复杂数据对象。
修改存储位置
由于SharedPreferences
保存在应用程序的安装路径下,那么随着软件的卸载,保存的内容也随之删除,如果想自定义SharedPreferences
的保存路径,可以参考如下实现,本质上使用了反射修改了文件的保存路径:
SharedPreferences自定义XML文件保存位置
总结
1.尽量不存入体积庞大的数据,数据从文件加载到内存中后将会一直占用内存;
2.使用commit()
方法也尽量不存入提交庞大的数据,不需要同步的话尽量使用apply()
方法异步提交;
3.使用的时候尽量将变化的数据put完毕后再进行commit
或者apply
操作;
参考文章:
工匠若水:Android应用Preference相关及源码浅析(SharePreferences篇)
其中涉及了如何进行跨进程使用SharedPreferences