SharedPreferences由浅入深学习
学而不思则罔,思而不学则殆
功能
- Android平台上一个轻量级的存储辅助类,用来保存应用的一些常用配置
- 提供了string,set,int,long,float,boolean六种数据类型
- 数据是以xml形式进行存储,目录是 /data/data/包名/shared_prefs 文件夹下
- 在应用中通常做一些简单数据的持久化缓存
简单范例
获取SharedPreferences
private SharedPreferencesManager(Context context) {
sharedPreferences = context.getSharedPreferences("dit_test", Context.MODE_PRIVATE);
}
添加数据:
public void test() {
SharedPreferences.Editor edit = sharedPreferences.edit();
Log.d("zhangyu", "SharedPreferences:" + sharedPreferences);
Log.d("zhangyu", "Editor:" + edit);
//08-05 14:20:03.436 13213 13213 D zhangyu : SharedPreferences:android.app.SharedPreferencesImpl@2b8f84a
//08-05 14:20:03.437 13213 13213 D zhangyu : Editor:android.app.SharedPreferencesImpl$EditorImpl@7a473bb
edit.putBoolean("test_boolean", false);
edit.putFloat("test_float", Float.MAX_VALUE);
edit.putInt("test_int", Integer.MAX_VALUE);
edit.putLong("test_long", Long.MAX_VALUE);
edit.putString("test_string", "test_string");
Set<String> set = new HashSet<>();
set.add("set1");
set.add("set2");
set.add("set3");
edit.putStringSet("test_set", set);
edit.apply();
Log.d("zhangyu", "Editor:" + edit);
}
最终结果如下,在文件中的保存格式是xml的形式:
///data/data/包名/shared_prefs
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<float name="test_float" value="3.4028235E38" />
<int name="test_int" value="2147483647" />
<string name="test_string">test_string</string>
<long name="test_long" value="9223372036854775807" />
<boolean name="test_boolean" value="false" />
<set name="test_set">
<string>set3</string>
<string>set2</string>
<string>set1</string>
</set>
</map>
这里打印了SharedPreferences和Editor对象,发现其实现类分别是:SharedPreferencesImpl和SharedPreferencesImpl$EditorImpl
08-05 14:27:45.183 13484 13484 D zhangyu : SharedPreferences:android.app.SharedPreferencesImpl@2b8f84a
08-05 14:27:45.183 13484 13484 D zhangyu : Editor:android.app.SharedPreferencesImpl$EditorImpl@e55c1ab
原理
SharedPreferences类图
SharedPreferences初始化
最终是通过ContextImpl的getSharedPreferences方法来获取SharedPreferencesImpl对象,该对象有缓存,如果是第一次加载,会重新新建,内部采用的是静态类同步锁,在第一次所获SharedPreferences 的时候,如果有竞争,会发生等待锁的情况。
//ContextImpl SDK-29
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
...
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name); //根据名称拿file缓存
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
//1获取缓存
sp = cache.get(file);
//2没有缓存,新建
if (sp == null) {
//3检查mode
checkMode(mode);
...
//4新建SharedPreferencesImpl
sp = new SharedPreferencesImpl(file, mode);
//5保存到缓存中
cache.put(file, sp);
return sp;
}
}
...
return sp;
}
Mode
原生定义了四种PreferencesMode
/** @hide */
@IntDef(flag = true, prefix = { "MODE_" }, value = {
MODE_PRIVATE,
MODE_WORLD_READABLE, //后面已弃用
MODE_WORLD_WRITEABLE, //后面已弃用
MODE_MULTI_PROCESS,
})
@Retention(RetentionPolicy.SOURCE)
public @interface PreferencesMode {}
checkMode方法中在大于等于Build.VERSION_CODES.N的版本中使用MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE会抛出SecurityException。开发者需要注意!
//ContextImpl SDK-29
private void checkMode(int mode) {
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
if ((mode & MODE_WORLD_READABLE) != 0) {
throw new SecurityException("MODE_WORLD_READABLE no longer supported");
}
if ((mode & MODE_WORLD_WRITEABLE) != 0) {
throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
}
}
}
SharedPreferencesImpl
SharedPreferencesImpl初始化很重要,经常在开发中容易出现ANR,源码如下:
SharedPreferencesImpl(File file, int mode) {
...
startLoadFromDisk();
}
@UnsupportedAppUsage
private void startLoadFromDisk() {
synchronized (mLock) {
//是否加载完成标识改为flase
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
//子线程中加载xml数据到内存中
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
...
//从文件中读取数据到内存中
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) {
// An errno exception means the stat failed. Treat as empty/non-existing by
// ignoring.
} catch (Throwable t) {
thrown = t;
}
...
synchronized (mLock) {
//是否加载标识改为true
mLoaded = true;
...
try {
...
} catch (Throwable t) {
mThrowable = t;
} finally {
//通知所有等待锁的地方,重新竞争锁
mLock.notifyAll();
}
}
}
等待数据初始化加载完成的方法,该方法在所有获取数据的地方都会调用,如果数据没有加载完成,会调用mLock.wait()进入等待状态,等待 mLock.notifyAll()的唤醒
@GuardedBy("mLock")
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
第一次XML加载的时候,会mLoaded 标识改为false,标识数据还没有load完成,此时这个时候去获取数据有ANR的风险,所以理论上如果初始化时间过长(XML文件数据过多,性能不好),就会增加ANR的几率;
改进方案有:
- SP文件分职责,不同的职责不同的SP文件,减小SP文件大小
- 主线程获取SP数据的时候需要考虑初始化时间
EditorImpl 类
EditorImpl 是真正实现数据插入的地方,先看看该类的整体实现,主要就一个对象锁对象和一个保存数据的map.
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 putStringSet(String key, @Nullable Set<String> values) {
synchronized (mEditorLock) {
mModified.put(key,
(values == null) ? null : new HashSet<String>(values));
return this;
}
}
@Override
public Editor putInt(String key, int value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor putLong(String key, long value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor putFloat(String key, float 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() { //提交,异步方法
...
}
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() { //commit 数据到 Memory
...
}
@Override
public boolean commit() { //提交,同步方法
...
}
private void notifyListeners(final MemoryCommitResult mcr) { //通知观察者
...
}
}
查看其代码可以得出几点结论:
- sharedPreferences.edit()每次的时候都会新建一个EditorImpl 对象
- 该对象putXXX(),clear()都加了对象锁,是线程安全的方法,但是一般也不会在不同的线程使用同一个EditorImpl 对象去更改数据,一般操作都是新建一个EditorImpl 对象去操作数据。
- 提交分为同步提交和异步提交
apply 异步提交
这里所说的同步和异步都是指内存中的数据同步到文件中的方式是异步还是同步的。
@Override
public void apply() {
...
//更改内存中的数据
final MemoryCommitResult mcr = commitToMemory();
...忽略一些不重要的逻辑...
//这里很重要,把mcr对象(包含一份内存映射),添加到队列中去执行,等待数据同步到内存中
//这里主要是postWriteRunnable这个参数决定是否是同步还是异步,
//postWriteRunnable== null 则同步执行
//postWriteRunnable!= null 则异步执行
//这里不等于null,异步执行
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);
}
commit 同步提交
commit 同步提交,是指同步写入最新的数据到xml文件中去。
@Override
public boolean commit() {
...
//更新内存,返回一个完整的内存映射对象
MemoryCommitResult mcr = commitToMemory();
//这里第二个参数为null,会同步去执行,把mcr 写入到内存中
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//等待执行writtenToDiskLatch.countDown()
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;
}
notifyListeners通知观察者
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));
}
}
内存数据写到xml文件
将已提交内存的结果排队以写入磁盘
/**
* Enqueue an already-committed-to-memory result to be written
* to disk.
*
* They will be written to disk one-at-a-time in the order
* that they're enqueued.
*
* @param postWriteRunnable if non-null, we're being called
* from apply() and this is the runnable to run after
* the write proceeds. if null (from a regular commit()),
* then we're allowed to do this disk write on the main
* thread (which in addition to reducing allocations and
* creating a background thread, this has the advantage that
* we catch them in userdebug StrictMode reports to convert
* them where possible to apply() ...)
*/
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//commit 方法传入的是null
//apply 传入的是非null
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
//同步锁保证只有一个线程在写入数据到xml文件中,很重要
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//先判断Commit的提交 isFromSyncCommit = true
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//如果没有,直接执行
writeToDiskRunnable.run();
return;
}
}
//扔到写入队列中去,内部是一个new HandlerThread("queued-work-looper",Process.THREAD_PRIORITY_FOREGROUND)
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
前面说了更新操作,接下来看看获取数据操作
SharedPreferencesImpl获取数据
获取数据的操作都到SharedPreferencesImpl中
...
@Override
public Map<String, ?> getAll() {
synchronized (mLock) {
awaitLoadedLocked();
//noinspection unchecked
return new HashMap<String, Object>(mMap);
}
}
@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
@Nullable
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
synchronized (mLock) {
awaitLoadedLocked();//等待数据load结束,否则会一直等待,有ANR风险
Set<String> v = (Set<String>) mMap.get(key);
return v != null ? v : defValues;
}
}
@Override
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public long getLong(String key, long defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Long v = (Long)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public float getFloat(String key, float defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Float v = (Float)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 boolean contains(String key) {
synchronized (mLock) {
awaitLoadedLocked();
return mMap.containsKey(key);
}
}
@Override
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
...
可以看到所有的方法都有synchronized (mLock),表示也是线程安全的,但是到mLock被其他线程持有的时候,会发生阻塞情况,主要是初始化的时候,在一些机型上要注意。