1. 场景
首先我们来看看,SharedPreferences在什么场景比较适用:
- SharedPreferences是一种轻量级的存储,数据会写在本地的一个xml文件中,以键值对的形式存在,如果程序卸载,此文件也跟着卸载
- 以Map形式存放简单的配置参数
- 可以保存的数据类型有:int、boolean、float、long、String、StringSet。
- 保存位置:data/data/程序包名/share_prefs
- 主要用途:
- 1.保存应用的设置 例如:设置静音,下次进入还是静音
- 2.判断是否是第一次登陆
性能:
- ShredPreferences是单例对象,第一次打开后,之后获取都无需创建,速度很快。
- 当第一次获取数据后,数据会被加载到一个缓存的Map中,之后的读取都会非常快。
- 当由于是XML<->Map的存储方式,所以,数据越大,操作越慢,get、commit、apply、remove、clear都会受影响,所以尽量把数据按功能拆分成若干份。
2. SharedPreferences的使用
- SharedPreferences对象本身只能获取数据而不支持存储和修改,存储修改是通过SharedPreferences.edit()获取的内部接口Editor对象实现
- 使用Preference来存取数据,用到了SharedPreferences接口和SharedPreferences的一个内部接口SharedPreferences.Editor,这两个接口在android.content包中
2.1 存储数据:
- 使用Activity类的getSharedPreferences方法获得SharedPreferences对象;
- 使用SharedPreferences接口的edit获得SharedPreferences.Editor对象;
- 通过SharedPreferences.Editor接口的putXXX方法保存key-value对;
- 通过过SharedPreferences.Editor接口的commit方法保存key-value对。
2.2 读取数据:
- 使用Activity类的getSharedPreferences方法获得SharedPreferences对象;
- 通过SharedPreferences对象的getXXX方法获取数据
2.3 方法:
获取SharedPreferences对象:
public abstract SharedPreferences getSharedPreferences (String name, int mode)
参数: name:命名 mode:模式,包括
- MODE_PRIVATE(只能被自己的应用程序访问)
- MODE_WORLD_READABLE(除了自己访问外还可以被其它应该程序读取)
- MODE_WORLD_WRITEABLE(除了自己访问外还可以被其它应该程序读取和写入)
若该Activity只需要创建一个SharedPreferences对象的时候,可以使用getPreferences方法,不需要为SharedPreferences对象命名,只要传入参数mode即可
public SharedPreferences getPreferences (int mode)
获取Editor对象(由SharedPreferences对象调用):
abstract SharedPreferences.Editor edit()
写入数据(由Editor对象调用):
参数: key:指定数据对应的key value:指定的值
abstract SharedPreferences.Editor putBoolean(String key, boolean value)
abstract SharedPreferences.Editor putFloat(String key, float value)
abstract SharedPreferences.Editor putInt(String key, int value)
abstract SharedPreferences.Editor putLong(String key, long value)
abstract SharedPreferences.Editor putString(String key, String value)
abstract SharedPreferences.Editor putStringSet(String key, Set<String> values)
移除指定key的数据(由Editor对象调用):
abstract SharedPreferences.Editor remove(String key)
清空数据(由Editor对象调用):
abstract SharedPreferences.Editor clear()
提交数据(由Editor对象调用):
abstract boolean commit()
读取数据(由SharedPreferences对象调用):
参数 key:指定数据的key defValue:当读取不到指定的数据时,使用的默认值defValue
abstract Map<String, ?> getAll()
abstract boolean getBoolean(String key, boolean defValue)
abstract float getFloat(String key, float defValue)
abstract int getInt(String key, int defValue)
abstract long getLong(String key, long defValue)
abstract String getString(String key, String defValue)
abstract Set<String> getStringSet(String key, Set<String> defValues)
2.4 例子
1)写入数据:
SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("name", “Tom”);
editor.putInt("age", 28);
editor.putBoolean("marrid",false);
editor.commit();
2)读取数据:
SharedPreferences sharedPreferences= getSharedPreferences("data", Context .MODE_PRIVATE);
String userId=sharedPreferences.getString("name","");
3)删除指定数据
editor.remove("name");
editor.commit();
4)清空数据
editor.clear();
editor.commit();
3 Tips
tip 1:
是否在Acitivty里执行?
在Acitivty中执行的:
getSharedPreferences (String name, int mode)
getPreferences (int mode)
MODE_PRIVATE
不在Activity中:
context.getSharedPreferences (String name, int mode)
context.getPreferences (int mode)
Conetxt.MODE_PRIVATE
tip 2:
- 所保存的SharedPreferences数据将一直存在,除非被覆盖、移除、清空或文件被删除。
- SharedPreferences保存的数据会随着应用的卸载而被删除
tip 3:
同时执行这两句代码的时候,第一行代码所写的内容会被第二行代码取代。
editor.putInt("age", 20);
editor.putInt("age", 32);
editor.putString("age", "20");
editor.putInt("age", 32);
tip 4:
执行以下代码会出现异常。 (指定key所保存的类型和读取时的类型不同)
editor.putInt("age", 32);
String age = userInfo.getString("age", "null");
tip 5:
在这些动作之后,记得commit
editor.putInt("age", 20);
editor.remove("age");
editor.clear();
editor.commit();
4. SharedPreferences的问题
其实SharedPreferences作为一种轻量级的数据存储方式,使用起来也非常方便,以键值对的形式存储在本地,初始化 SharedPreference 的时候,会将整个文件内容加载内存中,因此会带来以下问题:
- getXXX()获取值的形式有可能会导致主线程阻塞
- SharedPreferences不能保证类型安全, 加载的数据会一直留在内存中,浪费内存
- apply()方法虽然是异步的,可能会发生 ANR
- apply() 方法无法获取到操作成功或者失败的结果
4.1 为什么getXXX()方法会导致主线程阻塞
因为getXXX()都是同步的,在主线程调用 get 方法时,同步方法内调用了 wait() 方法,会一直等待 getSharedPreferences() 方法开启的线程读取完数据才能继续往下执行,如果数据量读取的小,并没有什么影响,如果读取的文件较大会导致主线程阻塞
具体大家可以查看haredPreferences源码: frameworks/base/core/java/android/app/SharedPreferencesImpl.java
4.2 SharedPreferences不能保证类型安全,并且一直会留在内存中
val key = "DataStore"
val sp = getSharedPreferences("文件名", Context.MODE_PRIVATE)
sp.edit { putInt(key, 0) }
sp.getString(key, "");
使用 Int 类型的数据覆盖掉相同的 key,然后使用相同的 key 读取 Sting 类型的数就会造成ClassCastException异常 而通过 getSharedPreferences() 方法加载的数据,最后会将数据存储在静态的成员变量中。
4.3 apply()方法是异步的,可能会发生ANR
异步的提交为什么会发生ANR呢?
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) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
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);
}
在 QueuedWork.java 中:
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
private static Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}
private static class QueuedWorkHandler extends Handler {
static final int MSG_RUN = 1;
QueuedWorkHandler(Looper looper) {
super(looper);
}
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork();
}
}
}
private static void processPendingWork() {
long startTime = 0;
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
}
}
}
}
在上面的方法中注意到 对每个 apply 都会创建一个相应的 awaitCommit,并添加到 QueuedWork 的一个队列中,但是在 QueuedWork 注意到有这样一个方法 waitToFinish
public static void waitToFinish() {}
这个方法会在 Activity 暂停时或者 BroadcastReceiver 的 onReceive 方法调用后或者 service 的命令处理后被调用,并且调用这个方法的目的是为了确保异步任务被及时完成。 可以看到 waitToFinish 都是在 ActivityThread 中,也就是主线程调用的
public static void waitToFinish() {
boolean hadMessages = false;
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
}
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
...
}
这种设计是为了保证 SP 可靠的、保证写入完成的存储机制。
总结一下就是:
- apply 没有返回值
- apply 是在主线程将修改数据提交到内存, 然后再子线程(HandleThread)提交到磁盘
- apply 会将 Runnble 添加到 QueueWork 中,如果主线程 Activity 暂停时或者 BroadcastReceiver 的 onReceive 方法调用后或者 service 的- 命令处理,就会在主线程执行 提交到硬盘的方法,这个过程就会造成主线程 ANR
5 Google推荐使用DataStore 替换 SharedPreferences
Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。
如果您当前在使用 SharedPreferences 存储数据,请考虑迁移到 DataStore。
DataStore | Android开发者平台
|