因为Jetpack组件出现了一个DataStore组件,来准备替换SharedPreference。所以在此分类下讲解一下SharedPreferences。
一.基本使用
1.简介
SharedPreferences是Android平台上轻量级的数据存储方式,用来保存App的各种配置信息。其本质是一个以 键值对(key-value)的方式保存数据的xml文件,其保存在/data/data/shared_prefs目录下。使用SharedPreferences最常用到的是两个接口。SharedPreferences接口和Editor接口。
源码
public interface SharedPreferences {
public interface Editor {
...
}
...
}
官网
SharedPreferences | Android Developers
2.简单使用
代码
package com.wjn.rxdemo.datastore;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.wjn.rxdemo.R;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class SharedPreferenceActivity extends AppCompatActivity implements View.OnClickListener {
private TextView textView1;
private TextView textView2;
private TextView textView3;
private SharedPreferences sharedPreferences;
private SharedPreferences.Editor editor;
private final static String SHARED_PREFERENCES_NAME = "MySharedPreferences";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sharedpreference);
findView();
}
/**
* 初始化各种View
*/
private void findView() {
textView1 = findViewById(R.id.activity_sharedperference_textview1);
textView2 = findViewById(R.id.activity_sharedperference_textview2);
textView3 = findViewById(R.id.activity_sharedperference_textview3);
textView1.setOnClickListener(this);
textView2.setOnClickListener(this);
textView3.setOnClickListener(this);
//获取SharedPreferences对象
sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
}
/**
* 各种点击事件的方法
*/
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.activity_sharedperference_textview1://存数值
saveSharedPreferences();
break;
case R.id.activity_sharedperference_textview2://取数值
getSharedPreferences();
break;
case R.id.activity_sharedperference_textview3://清空数值
clearSharedPreferences();
break;
default:
break;
}
}
/**
* 存数值
*/
private void saveSharedPreferences() {
//存数值时 首先获取SharedPreferences.Editor对象
editor = sharedPreferences.edit();
//存Int类型
editor.putInt("intType", 12);
//存Long类型
editor.putLong("longType", 123456);
//存Float类型
editor.putFloat("floatType", 12.34f);
//存Boolean类型
editor.putBoolean("booleanType", false);
//存String类型
editor.putString("stringType", "张三");
//存StringSet类型
Set<String> strings = new HashSet<>();
strings.add("Set111");
strings.add("Set222");
strings.add("Set333");
editor.putStringSet("setType", strings);
//最终提交 apply异步提交
editor.apply();
}
/**
* 取数值
*/
private void getSharedPreferences() {
int i = sharedPreferences.getInt("intType", -1);
long l = sharedPreferences.getLong("longType", -1);
float f = sharedPreferences.getFloat("floatType", -1);
boolean b = sharedPreferences.getBoolean("booleanType", false);
String s = sharedPreferences.getString("stringType", "");
Set<String> set = sharedPreferences.getStringSet("setType", null);
Log.d("TAG", "SharedPreferences取值 Int类型 ----:" + i);
Log.d("TAG", "SharedPreferences取值 Long类型 ----:" + l);
Log.d("TAG", "SharedPreferences取值 Float类型 ----:" + f);
Log.d("TAG", "SharedPreferences取值 Boolean类型 ----:" + b);
Log.d("TAG", "SharedPreferences取值 String类型 ----:" + s);
Log.d("TAG", "SharedPreferences取值 Set类型 ----:" + set);
if (null != set) {
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String s1 = iterator.next();
Log.d("TAG", "SharedPreferences取值 Set类型 ----:" + s1);
}
}
}
/**
* 清空数值
*/
private void clearSharedPreferences() {
editor = sharedPreferences.edit();
editor.clear();
editor.apply();
}
}
结果
存值 然后 取值
D/TAG: SharedPreferences取值 Int类型 ----:12
D/TAG: SharedPreferences取值 Long类型 ----:123456
D/TAG: SharedPreferences取值 Float类型 ----:12.34
D/TAG: SharedPreferences取值 Boolean类型 ----:false
D/TAG: SharedPreferences取值 String类型 ----:张三
D/TAG: SharedPreferences取值 Set类型 ----:[Set222, Set333, Set111]
D/TAG: SharedPreferences取值 Set类型 ----:Set222
D/TAG: SharedPreferences取值 Set类型 ----:Set333
D/TAG: SharedPreferences取值 Set类型 ----:Set111
此时 MySharedPreferences.xml文件内容
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<float name="floatType" value="12.34" />
<boolean name="booleanType" value="false" />
<int name="intType" value="12" />
<string name="stringType">张三</string>
<set name="setType">
<string>Set222</string>
<string>Set333</string>
<string>Set111</string>
</set>
<long name="longType" value="123456" />
</map>
结果
清空 然后 取值
D/TAG: SharedPreferences取值 Int类型 ----:-1
D/TAG: SharedPreferences取值 Long类型 ----:-1
D/TAG: SharedPreferences取值 Float类型 ----:-1.0
D/TAG: SharedPreferences取值 Boolean类型 ----:false
D/TAG: SharedPreferences取值 String类型 ----:
D/TAG: SharedPreferences取值 Set类型 ----:null
此时 MySharedPreferences.xml文件内容
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map />
说明
上面是清空全部xml文件的数据。使用
editor.clear();
方法。当然也可以清除单独Key对应的数据。使用
editor.remove("intType");//清除Key:intType对应的 Value值
二.方法说明
由上述代码可知,使用SharedPreferences。一般的步骤如下。
1.获取SharedPreferences对象。
SharedPreferences sharedPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
用的是 getSharedPreferences方法
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
参数name:生成xml文件的文件名。
参数mode:模式 有四种取值
<1> Context.MODE_PRIVATE
默认操作模式 代表该文件是私有数据只能被应用本身访问。在该模式下 写入的内容会覆盖原文件的内容。
<2> Context.MODE_APPEND
该模式会检查文件是否存在 存在就往文件追加内容 否则就创建新文件。
<3> Context.MODE_WORLD_READABLE
该模式下 当前文件可以被其他应用读取。
<4> Context.MODE_WORLD_WRITEABLE
该模式下 当前文件可以被其他应用写入。
一般使用 Context.MODE_PRIVATE 即默认模式即可。
2.获取SharedPreferences.Editor 对象
SharedPreferences.Editor editor = sharedPreferences.edit();
利用SharedPreferences对象的edit()方法获取SharedPreferences.Editor 对象。
3.用SharedPreferences.Editor对象添加 各种类型的值 共支持6种类型。下面的6种类型举例。
//存Int类型
editor.putInt("intType", 12);
//存Long类型
editor.putLong("longType", 123456);
//存Float类型
editor.putFloat("floatType", 12.34f);
//存Boolean类型
editor.putBoolean("booleanType", false);
//存StringSet类型
editor.putString("stringType", "张三");
//存String类型
Set<String> strings = new HashSet<>();
strings.add("Set111");
strings.add("Set222");
strings.add("Set333");
editor.putStringSet("setType", strings);
4.用SharedPreferences.Editor对象 提交 存储的内容到xml文件
//异步提交
editor.apply();
//同步提交
editor.commit();
有两个方法,分别支持异步提交和同步提交。具体分别下面讲解。
5.用SharedPreferences对象获取存储Key对应的Value值
int i = sharedPreferences.getInt("intType", -1);
long l = sharedPreferences.getLong("longType", -1);
float f = sharedPreferences.getFloat("floatType", -1);
boolean b = sharedPreferences.getBoolean("booleanType", false);
String s = sharedPreferences.getString("stringType", "");
Set<String> set = sharedPreferences.getStringSet("setType", null);
getXXX方法都是两个参数
参数1:Key。
参数2:默认值 即对应的Key取不到内容时默认值。
6.清空全部或者移除指定Key对应的Value值
//清空整个xml文件的内容
editor.clear();
//移除指定Key对应的Value
editor.remove("Key");
注意:无论是clear清空还是remove移除指定Key对应的Value。最后都要apply提交。否则无效。
三.源码分析
上述讲解了 SharedPreferences的基础使用。比如
存储数值使用putXXX()方法。
获取数值使用getXXX()方法。
提交内容使用apply()方法。
清空全部数值使用clear()方法。
删除单独Key对应的Value使用 remove("XXXKey")方法。等等。
截图
那么SharedPreferences有何缺点呢?为什么会有DataStore来替换它呢。下面从SharedPreferences源码解析说明。
由于SharedPreferences和Editor都是接口。所以要看源码需要找到两个接口的实现类。SharedPreferencesImpl类和EditorImpl类
(Editor接口是SharedPreferences接口的内部接口。同样EditorImpl实现类是SharedPreferencesImpl实现类的内部类)
1.SharedPreference缺点一:不能保证类型安全
众所周知SharedPreference是以键值对的形式存取值的。那么在使用的过程中,如果没有合理的管理项目中众多的Key-Value。可能就会出现问题。
比如 项目中 同一个.xml文件中 某个地方使用Key:age 存放Int类型的字段 表示年龄。如果没有合理的管理Key。其他地方可能使用Key:age 存放的是String类型的字段 也表示年龄。
这样以Int类型存值的地方如果不知道 还是按照Int类型取数据就会有问题。
存数据
editor = sharedPreferences.edit();
editor.putInt("age", 12);//某个地方
editor.putString("age", "12岁");//其他地方修改
editor.apply();
取数据 还是按照原来Int类型取值
int age1 = sharedPreferences.getInt("age", -1);
报错 ClassCastException
2.SharedPreference缺点二:写入方式是全量更新方式 效率低下
由于SharedPreference使用xml格式来保存数据。所以每次更新数据只能全量替换更新数据。这意味着如果我们有1000个数据,如果只更新一项数据,也需要将所有数据转化成xml格式,然后再通过IO写入文件中。
这也就是SharedPreference效率比较低的原因。
源码分析
SharedPreferencesImpl实现类 构造方法中
SharedPreferencesImpl(File file, int mode) {
...
startLoadFromDisk();
}
startLoadFromDisk()方法源码 (这个方法就是看是读取磁盘中的xml文件) 也就是获取 SharedPreferences对象时调用的
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
此方法中新开了一个线程,执行 loadFromDisk()方法。
loadFromDisk()方法源码
private void loadFromDisk() {
synchronized (mLock) {
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<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) {
// An errno exception means the stat failed. Treat as empty/non-existing by
// ignoring.
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
// It's important that we always signal waiters, even if we'll make
// them fail with an exception. The try-finally is pretty wide, but
// better safe than sorry.
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
// In case of a thrown exception, we retain the old map. That allows
// any open editors to commit and store updates.
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll();
}
}
}
源码中,也可以看出。由于SharedPreference的xml保存数据方式,导致每次都要重新读取磁盘中xml文件的流。也就是全量更新。
3.SharedPreference缺点三:commit()提交容易造成ANR
/**
* Commit your preferences changes back from this Editor to the
* {@link SharedPreferences} object it is editing. This atomically
* performs the requested modifications, replacing whatever is currently
* in the SharedPreferences.
*
* <p>Note that when two editors are modifying preferences at the same
* time, the last one to call commit wins.
*
* <p>If you don't care about the return value and you're
* using this from your application's main thread, consider
* using {@link #apply} instead.
*
* @return Returns true if the new values were successfully written
* to persistent storage.
*/
boolean commit();
翻译
如果是在主线程使用,建议使用apply()方法替换。因为commit()方法是同步的。 而apply()方法是异步的。
commit()方法源码
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
//向内存中写入数据
MemoryCommitResult mcr = commitToMemory();
/* sync write on this thread okay */
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null );
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
看到源码中有一行代码
mcr.writtenToDiskLatch.await();
通过await()暂停主线程,直到写入磁盘操作完成。也就是说使用commit()方法提交时,会堵塞主线程。一般不会察觉有什么问题,是因为写入的xml文件比较小。如果文件过大,导致读取时间变长。堵塞主线程时间也就变长。这时候就会导致ANR了。
综上:SharedPreference文件不易过大,容易造成ANR。且一般使用apply()方法异步提交。
commit()方法因为是同步的,需要让主线程等待。所以xml文件过大时容易造成ANR。官网也建议使用apply异步的方法提交。那么一步的apply就没有问题了吗。下面看一下apply()方法的源码。
apply()方法源码
@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);
// 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);
}
可以,看出apply()方法。也是首先 向内存中写入数据。也会执行
mcr.writtenToDiskLatch.await();
通过await()暂停主线程,直到写入磁盘操作完成。但是区别于commit()方法。apply()方法是在一个新的Runnable中(也就是新线程)执行的这行代码。所以相比commit()方法在主线程执行要好的多。
但是毕竟也是有可能堵塞主线程,所以apply()方法也有可能造成ANR。不过相比commit()方法。要好的多。
4.SharedPreference缺点四:getXXX() 导致ANR
源码中也可以看出。SharedPreference 获取内容时的getXXX()方法都是同步的。比如getString()方法。
getString()方法源码
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
getLong()方法源码
@Override
public long getLong(String key, long defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Long v = (Long)mMap.get(key);
return v != null ? v : defValue;
}
}
getFloat()方法源码
@Override
public float getFloat(String key, float defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Float v = (Float)mMap.get(key);
return v != null ? v : defValue;
}
}
可以看出所有的getXXX()方法。大致都是一样的都是在同步方法中执行
awaitLoadedLocked();
然后在mMap中通过Key取不同类型的数值。
awaitLoadedLocked方法源码
@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);
}
}
@GuardedBy("mLock")
private boolean mLoaded = false;
此方法中,有一个while循环,如果非mLoaded 就一直等待。那么mLoaded 字段在什么地方赋值true呢。
上面可知,是在loadFromDisk()方法里。也就是说是在获取SharedPreferences对象时,读取完整个xml文件流内容后,才会将字段mLoaded变成true。然后getXXX()时,才不会一直堵塞线程。那么如果xml文件文件过大,导致读取时间边长。这个时候恰巧使用getXXX()方法获取相关内容。这时候mLoaded字段还是false这样就会在while循环中一直等待。造成ANR。
四.总结
结合以上基本使用和源码分析,SharedPreferences使用时,应该注意一下几点。
<1> 尽量保证一个xml文件不要过大。
<2> 提交SharedPreferences时,尽量使用apply()异步方法,不推荐使用commit()方法同步提交,尤其在主线程中。
<3> 尽量保证所有的xml文件名和getXXX()的Key 要统一维护。不要出现一个Key在一个地方是Int类型,在一个地方是String类型。
附:FrameWorks源码地址