简介
SharedPreferences是Android系统API提供的一种数据存储和读取机制,内部通过xml的格式存储一些数据,可以作为一种简易的硬盘缓存来使用
public interface SharedPreferences
SharedPreferences本身只是一个接口而已
例子
先看一个简单的使用例子
private String tryGetStringFromSP(String fileName,String key,String defaultValue){
if(null == fileName || null == key){
return null;
}
SharedPreferences sharedPreferences = mContext.getSharedPreferences(fileName,Context.MODE_PRIVATE);
return sharedPreferences.getString(key,defaultValue);
}
private void tryStoreStringToSP(String fileName,String key,String value){
if(null == fileName || null == key){
return;
}
SharedPreferences sharedPreferences = mContext.getSharedPreferences(fileName,Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(key,value);
editor.apply();
}
先通过文件名获得对应的SharedPreferences,然后通过键值对的方式来使用。
存储的时候需要额外开启类似数据库操作的事务的editor。
获得SharedPreferences实例
默认通过Context获取SharedPreferences,Context的实现类为ContextImpl:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 在API19之前,支持name为null,默认为null.xml文件
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {//这是个类锁
//private ArrayMap<String, File> mSharedPrefsPaths;
//可以看到是一个String->File的键值对
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
//其实name就是文件名前缀(不包括.xml),通过名称获取文件
//也可以看出来这是一个缓存机制
file = mSharedPrefsPaths.get(name);
if (file == null) {//当前文件没在缓存中
//这个其实就是组装一下文件的真实路径,然后获取到文件
file = getSharedPreferencesPath(name);
//存入缓存之中,这样就可以在后续使用中直接从缓存中拿对应的文件
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public File getSharedPreferencesPath(String name) {
// 代码有整合
File base = null;
synchronized (mSync) {
//这里就是data里面应用包名里面的shared_prefs目录
//用DDMS查看的话就是data/data/应用的包名/shared_prefs
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
//确保目录存在,否则尝试建立一次当前目录
base = ensurePrivateDirExists(mPreferencesDir);
}
String fileName = name + ".xml";//以.xml结尾,可以看出是一个正常的xml文件
if (fileName.indexOf(File.separatorChar) < 0) {//不允许有文件路径分隔符,比方说/
return new File(base, fileName);
}
throw new IllegalArgumentException(
"File " + fileName + " contains a path separator");
}
private static File ensurePrivateDirExists(File file) {
if (!file.exists()) {
try {//如果当前目录不存在,尝试创建一次,并且给予指定的权限
Os.mkdir(file.getAbsolutePath(), 0771);
Os.chmod(file.getAbsolutePath(), 0771);
} catch (ErrnoException e) {
if (e.errno == OsConstants.EEXIST) {
// We must have raced with someone; that's okay
} else {
Log.w(TAG, "Failed to ensure " + file + ": " + e.getMessage());
}
}
}
return file;
}
可以看到在从ContextImpl中获取SharedPreferences的时候有一级缓存,文件名前缀和对应文件的缓存,在使用app的过程中多次使用同一个文件名的SharedPreferences是很常见的事情,这里优化了这个操作。
此外,可以看到SharedPreferences本质上是一个存储在data/data/应用的包名/shared_prefs下面的xml文件。
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);//API24以上不再支持MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//获得当前应用已经缓存的文件和SharedPreferences实现类
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {//没有命中缓存
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) {
//在多进程模式下或者API11以前,如果文件发生变化,要重新从硬盘中读取数据
sp.startReloadIfChangedUnexpectedly();
}//不过实际上官方不推荐多进程模式下的SP,在某一些版本下会有问题,推荐使用ContentProvider来替代
return sp;
}
至此,经过两级的缓存成功的获取SharedPreferences实现类SharedPreferencesImpl
SharedPreferencesImpl
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);//在同一级目录下构建.bak备份文件
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
/**
* 在子线程中读取SP文件中的数据
*/
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (SharedPreferencesImpl.this) {
if (mLoaded) {//当前读取已经完成,不需要重复读取
return;
}
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);//解析文件中的xml内容转为map
} catch (XmlPullParserException | IOException e) {
Log.w(TAG, "getSharedPreferences", e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (SharedPreferencesImpl.this) {
mLoaded = true;//标记读取完成
if (map != null) {
mMap = map;//记录新的读取数据
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
notifyAll();
}
}
在第一次构建SharedPreferencesImpl的时候会新开线程读取文件中的数据,并且保存到SharedPreferencesImpl的map中。
public String getString(String key, @Nullable String defValue) {
synchronized (this) {//这里有锁,意味着可能会阻塞线程
awaitLoadedLocked();//如果当前在进行IO操作从硬盘中读取新的数据,等待数据更新完成,这里可能也会阻塞线程
//绝大部分操作都是在主线程进行的话那么没有阻塞线程的可能,主要的场景是在startLoadFromDisk中
//这个会在第一次创建SP的时候调用
//这意味着如果File过大,读取时间过长,可能会出现问题,这里需要特别注意
String v = (String)mMap.get(key);//直接从map中获取数据
return v != null ? v : defValue;
}
}
从SharedPreferencesImpl中获取数据实际上就是从map也就是内存中获取数据。
有一点需要注意,每一次获取数据的时候都有一把锁,这个锁的异步操作主要是在读取文件到map这个过程中持有,即startLoadFromDisk中,一般这个读取操作是在创建SharedPreferencesImpl时候进行,这意味着如果是第一次使用并且快速的进行get操作,可能会导致主线程卡顿。
所以为了避免阻塞的情况,可以利用ContextImpl中SharedPreferencesImpl的缓存,然后通过预加载尽可能的在使用SP之前完成从硬盘读取数据到内存中这个步骤。同时也要注意SharedPreferences文件在存储时候的大小,尽量不要存储太大的东西,加快IO操作的速度,这也是SharedPreferences被称为轻量级缓存的一个原因。
EditorImpl
在SharedPreferencesImpl进行写入操作的时候要开启一个类似事务的存在,这个就是EditorImpl
public Editor edit() {
synchronized (this) {//等待从硬盘中读取数据到内存中
awaitLoadedLocked();
}
return new EditorImpl();
}
//当前数据变化的集合
private final Map<String, Object> mModified = Maps.newHashMap();
//是否标记clear数据
private boolean mClear = false;
/**
* 放入String类型的数据
* @param key 数据的键名
* @param value 数据
*/
public SharedPreferences.Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);//只是单纯的把数据修改在EditorImpl中的集合
return this;
}
}
/**
* 移除指定的key的数据
*/
public SharedPreferences.Editor remove(String key) {
synchronized (this) {
mModified.put(key, this);//并没有直接移除key,而是标记value为EditorImpl
return this;
}
}
/**
* 清理
*/
public SharedPreferences.Editor clear() {
synchronized (this) {
mClear = true;//标记清理标志
return this;
}
}
EditorImpl内部也维持一个map,用于存储需要变化的数据,但是也注意到单纯的操作是没有任何的效果的。
/**
* 在当前线程执行内存和硬盘数据更新
*/
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();//先尝试将数据的修改同步到SP中的内存数据
//尝试将数据写入硬盘当中
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//在任务完成之前,这里都会阻塞
//可能导致ANR
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
/**
* 同步更新内存,异步写入硬盘
*/
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
//该任务在QueuedWork中自动调用,一般是在activity的onPause之类的情况下进行
//用于等待IO操作的完成,从而避免数据丢失
//但是这个也有可能导致ANR
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();//等待写入任务完成
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
//尝试将数据写入硬盘当中
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
//因为内存中的数据已经更新,所以一般来说通知观察者变化也是没有问题的
notifyListeners(mcr);
}
/**
* 将变化数据提交到SP的内存数据中
*/
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {//map修改的锁
if (mDiskWritesInFlight > 0) {//一般来说在apply操作的时候有可能导致flight>1
mMap = new HashMap<String, Object>(mMap);//此时拷贝一份内存数据
}
mcr.mapToWriteToDisk = mMap;//标记当前SP内部的数据
mDiskWritesInFlight++;//写入数量
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<SharedPreferences.OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (this) {
if (mClear) {//Editor中标记clear数据
if (!mMap.isEmpty()) {//当前SP中的数据不为空
mcr.changesMade = true;//标记数据发生变化
mMap.clear();//清除SP中的所有数据
}
mClear = false;
}
//可以看到clear的优先级不是太高
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) {//editor中标记移除或者value为空
if (!mMap.containsKey(k)) {//当前元素本来就不在SP中,不需要处理
continue;
}
mMap.remove(k);//移除当前元素,SP当中不存在value为null的元素
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
//当前SP已有改数据,不需要处理
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);//修改或者添加数据到SP中
}
mcr.changesMade = true;//标记数据发生变化
if (hasListeners) {//添加对应键值的监听
mcr.keysModified.add(k);
}
}
mModified.clear();//数据变化已经更新到SP的内存数据中,清空editor的数据
}
}
return mcr;
}
/**
* 将数据写入硬盘中
*/
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//新建一个将变化后的数据写到硬盘xml文件的任务
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);//写入硬盘中
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//commit时候为null,标记同步提交
final boolean isFromSyncCommit = (postWriteRunnable == null);
if (isFromSyncCommit) {
boolean wasEmpty = false;
//如果之前存在apply异步操作,那么可能在mDiskWritesInFlight--之前再次commit
//此时也应该在apply之后执行
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {//如果可以同步进行
writeToDiskRunnable.run();//在当前线程写入数据到硬盘中
return;
}
}
//apply或者commit但是之前的apply没有执行完成,这两种情况下都在单线程池中排队进行
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
/**
* 将数据写入文件当中
*/
private void writeToFile(MemoryCommitResult mcr) {
if (mFile.exists()) {
if (!mcr.changesMade) {
//如果文件已经存在,而且数据没有变化,不需要重写文件
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;
}
//将xml数据写入文件当中
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
}
//写入完成,删除备份文件
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);
}
可以看到SharedPreferences的数据实际上有两层,在应用中使用的数据绝大部分都是内存中的数据,这样保证了操作的速度,内存中的数据修改都是在当前线程中进行,完成之后便可以使用最新数据,但是写入文件的操作有一点区别。
apply:在特定的单线程池中异步执行文件修改操作,频繁的apply将会导致IO操作串行排队进行。为了保证IO操作的完成,QueuedWork会在一些时机等待IO的操作完成,比方说activity的onPause,这样极端情况下会导致ANR。
commit:在调用线程中同步写入文件,如果当前在UI线程中进行,那么要注意卡顿的问题,一般来说如果不是必须等待返回值,采用apply更为合适。
此外如果之前的apply写入文件操作还在进行中,此时写入操作也会在apply的单线程池中进行,不过阻塞的依然是当前的执行线程,这个需要注意。
总结
SharedPreferences本质上是一个xml的文件存储。
个人不推荐SharedPreferences存储太大的数据,还是相对希望一个比较快的读取和写入效率,作为一个轻量级的存储机制。
SharedPreferences最好进行预加载,然后Editor在修改数据的时候最好在完成所有数据的修改之后再进行commit或者apply,免得造成频繁的IO操作。
commit和apply的时候注意阻塞的可能,一般来说采用apply即可,因为内存中的数据会同步更新,可以接着使用。除非关心本次IO写入操作的结果,可以考虑commit,但也不应该阻塞UI线程,可以考虑新建线程使用之,但是数据同步性需要注意。总之需要根据具体的场景灵活使用。
SharedPreferences不应该使用多进程模式。