一、SharedPreference简介
SharedPreference是Android系统提供的轻量级数据存储方案,常被简称为SP。采用key-value的数据存储方式,数据存储媒介是XML文件。用于存储App的配置、账号等轻量级数据信息,是常用的外部存储数据方案。
二、SharedPreference用法
SharedPreference用法比较简单,包括读取、和写入两种用法。
1)写入
SharedPreference myPreference=getSharedPreferences("config", Context.MODE_PRIVATE);//获取SharedPreferences实例
Editor editor = myPreference.edit();//获取Edit
//put值
editor.putString("string", "string");
editor.putInt("key", 0);
editor.putBoolean("boolean", true);
//提交数据
editor.commit();// 或者editor.apply()
复制代码
commit和apply这两种方式都可以进行提交,区别在于commit是同步提交并且有返回值,apply是异步提交且无返回值。
2)读取
SharedPreference myPreference=getSharedPreferences("config", Context.MODE_PRIVATE);
String name=myPreference.getString("string", "default");
int age=myPreference.getInt("key", 0);//第二个参数是默认值
复制代码
三、源码分析
1)SharedPreferences接口,这个接口已经定义了所有关于SP读写操作的方法,写操作通过内部接口Editor定义,核心代码如下:
public interface SharedPreferences {
public interface OnSharedPreferenceChangeListener {
void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
}
public interface Editor {
Editor putString(String key, @Nullable String value);
........
Editor putBoolean(String key, boolean value);
Editor remove(String key);
Editor clear();
boolean commit();
void apply();
}
Map<String, ?> getAll();
String getString(String key, @Nullable String defValue);
.......
boolean getBoolean(String key, boolean defValue);
boolean contains(String key);
Editor edit();
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}
复制代码
2)实现类SharedPreferencesImpl,SharedPreferences接口中只是定义方法结构,功能实现由实现类SharedPreferencesImpl来完成。核心代码如下:
final class SharedPreferencesImpl implements SharedPreferences {
.......
private final File mFile;
private final int mMode;
private final Object mLock = new Object();
private final Object mWritingToDiskLock = new Object();
@GuardedBy("mLock")
private Map<String, Object> mMap;
@GuardedBy("mLock")
private boolean mLoaded = false;
......
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
......
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
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);
}
}
} catch (ErrnoException e) {
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll();
}
}
}
void startReloadIfChangedUnexpectedly() {
synchronized (mLock) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}
@GuardedBy("mLock")
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
......
@Override
public boolean getBoolean(String key, boolean defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
......
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
......
}
复制代码
1.先来分析sp的读取操作,从startLoadFromDisk方法中可以看到,读取数据开启了一个新的线程,同时通过lock对象加锁来保证同步。通过loadFromDisk方法从xml中解析数据,数据解析后存储在Map数据集合中,解析方式是xml的pull解析。当解析结束之后释放锁。
2.接下来分析根据key获取sp中存储值的操作,获取不同的数据类型操作其实都是一样的,这里以getString为例,通过代码可以发现首先会通过等待加载完成锁来判断是否加载完成,如果数据已经从xml文件中异步解析完成,则从Map集合中读取数据。
从读取数据中可以发现一点,xml中的数据是一次性读入map中,并且通常实在SharedPreferencesImpl构造中进行的,这里就发现了sp的缓存策略。读取数据的时候不是每读取一次就解析一次xml文件,只有第一次读取时候解析xml文件,之后都是读取缓存的数据。这也说明为什么官方建议sp中不要存储大量数据,数据量大了会导致xml解析性能下降,第一次读取的时候花费时间过长。
3)Editor实现类EditorImpl核心代码如下。
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@GuardedBy("mEditorLock")
private boolean mClear = false;
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
......
@Override
public Editor putBoolean(String key, boolean value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor remove(String key) {
synchronized (mEditorLock) {
mModified.put(key, this);
return this;
}
}
@Override
public Editor clear() {
synchronized (mEditorLock) {
mClear = true;
return this;
}
}
@Override
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) {
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (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 (mEditorLock) {
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
@Override
public boolean commit() {
long startTime = 0;
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
private void notifyListeners(final MemoryCommitResult mcr) {
if (mcr.listeners == null || mcr.keysModified == null ||
mcr.keysModified.size() == 0) {
return;
}
if (Looper.myLooper() == Looper.getMainLooper()) {
for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
final String key = mcr.keysModified.get(i);
for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
if (listener != null) {
listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
}
}
}
} else {
// Run this function on the main thread.
ActivityThread.sMainThreadHandler.post(() -> notifyListeners(mcr));
}
}
}
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
复制代码
EditorImpl类是用来完成sp的存值操作,在所有的put方法中都通过mModified的Map集合来维护修改的数据,存储上主要来分析commit和apply操作。
1.commit是进行同步存储,方法中直接通过commitToMemory方法将数据写入map集合中,然后enqueueDiskWrite同步写入xml中。
2.apply是进行异步存储,也是先通过commitToMemory方法将数据写入map集合中,然后通过enqueueDiskWrite方法异步写入xml中。
接下来分析如何将数据保存到内存和如何同步和异步完成数据写入xml,从commitToMemory方法中看到会对mModified集合进行遍历将mapToWriteToDisk集合(也就是mMap集合,因为指向统一内存地址)数据进行修改。执行结束后对mModified集合进行清空,最后返回MemoryCommitResult,写入XML文件需要这个MemoryCommitResult结果。
enqueueDiskWrite方法就比较简单了,apply和commit方法会传入enqueueDiskWrite方法一个Runnable,通过这个Runnable方法判断是同步还是异步,传入的是null则是同步,否则则是异步。同步则立刻执行写入文件,异步则通过QueuedWork进行异步调度写入。QueuedWork异步调度的原理是通过HandlerThread来完成,这里不进行分析。
四、总结
SharedPreferences是我们开发中常用的轻量级数据本地存储,小容量app配置数据可以用sp来存储,如果存储的key-value对过多,建议拆分sp,不要都存入一个sp中,会降低首次读取的性能。写入数据时如果不需要返回值则用apply进行提交,异步来将数据写入xml文件。
转载于:https://juejin.im/post/5ce371ed51882533135c0649