这段时间公司的项目真的太忙了,所以博客更新的相对慢了,对自己找借口的行为深表不满,所以昨天极力抽出时间整理了一下SharedPreferences 的相关知识拿出来分享,那下面就说一下 SharedPreferences 的相关知识点。
一、概述
二、相关 API 和使用方法
我们首先看一下它的 API,API 相对来说非常少也非常简单:
首先是获取 SharedPreferences 对象:
getXXX(String key)
通过此方法可以获取 key 对应的 value 值,XXX可以是基本类型
abstract Map< String key,XXX value> getAll()
获取 SharedPreferences 的全部 key-value 键值对
boolean contains(String key)
判断 key 对应的键值对是否存在
以上就是 SharedPreferences 的相关 API 了,基本没什么难度,从上面我们可以看出我们只看到了读数据的 API 没看到写数据的 API ,如果说到写数据,那就用到了下面这个类 SharedPreferences.Editor
sharedPreferences.edit()
通过此方法获取 SharedPreferences.Editor 实例
SharedPreferences.Editor clear()
清除 sharedPreferences 中的全部数据
SharedPreferences.Editor putXXX(String key,XXX value)
向 sharedPreferences 插入数据,XXX 为基本类型的数据
SharedPreferences.Editor remover(String key)
删除 key 对应的键值对
Boolean commit ()
当我们对 sharedPreferences 进行操作之后我们要进行提交,那就会用到这个方法,如果我们不调用这个方法,那么我们的操作就不会生效
void apply()
这个方法和 commit 方法差不多,只不过这个方法没有返回值,而且我认为最大的区别就是通过这个方法提交的话,写入是在线程中进行的
三、使用中遇到的坑
虽然我们的 sharedPreferences 很简单,但是在使用过程中还是发现了很多问题首先就是存储复杂数据的时候有时候不成功,最主要的还是跨进程读取数据的时候不能实时更新,我先把结论写出来,然后我们再从源码的层面去分析问题
四、源码分析
首先我们先分析一下我们在获取 sharedPreferences 所做的操作,下面看代码:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
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);
}
从这段代码中我们可以看出,首先先去获取了 name 对应的 file 文件,如果没有对应的文件就去创建一个新的文件,创建之后去把这个文件存入 mSharedPrefsPath 中,那我们接着往下面看 getsharedPreferences 方法做了什么
@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 = 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.
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
从这段代码中我们可以看到,我们先从缓存查找有没有缓存的 sharedPreferences 如果没有就去创建一个新的,那现在重点就是这个 new 方法是怎么去创建的,下面看代码
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);
} 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();
}
}
当我们创建 SharedPreference 最后会走到这一步,那这一步比较关键的地方就是系统把 sharedPreference 对应的 file 转换成了 map,并且这个 map 还是 SharedPreference 成员变量,那我们是不是可以有一个猜想,我们在读写数据的时候是不是直接从这个 map 中进行读写的呢,那么我们还是从源码找一下答案
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
可以看到我们的读取数据是直接从 map 中读取的,那接下来看一下写的操作
public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
可以看到我们的写的操作也是一样的,所以这样我们可以得出结论,在我们读写数据的时候,不会每次都加载 file,而是通过内存中的 map 进行读写的,所以对我们来说速度是非常快的,那么最后我们需要看的就是 apply 的源码了
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
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);
// 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);
}
上面的代码中我们看到,提交数据总共分两步,第一步是 commitToMemory ,第二步是 enqueueDiskWrite 那我们先看一下第一步的代码:
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
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);
}
mcr.mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (this) {
if (mClear) {
if (!mMap.isEmpty()) {
mcr.changesMade = true;
mMap.clear();
}
mClear = false;
}
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.
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);
}
mcr.changesMade = true;
if (hasListeners) {
mcr.keysModified.add(k);
}
}
mModified.clear();
}
}
return mcr;
}
这段代码主要就是把我们的 map 更新一下,那下一段代码其实也很简单其实就是把数据写入我们的 file 中,
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
好了源码基本上就这些主要内容,最后如果想知道为什么不能多进程读取数据的话可以在 apply 方法的最后一句找到答案
总结