Android——SharedPreferences
Posted 叫我金城武也行
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android——SharedPreferences相关的知识,希望对你有一定的参考价值。
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对象:
//根据name查找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:指定的值
//写入boolean类型的数据
abstract SharedPreferences.Editor putBoolean(String key, boolean value)
//写入float类型的数据
abstract SharedPreferences.Editor putFloat(String key, float value)
//写入int类型的数据
abstract SharedPreferences.Editor putInt(String key, int value)
//写入long类型的数据
abstract SharedPreferences.Editor putLong(String key, long value)
//写入String类型的数据
abstract SharedPreferences.Editor putString(String key, String value)
//写入Set<String>类型的数据
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()
//读取的数据为boolean类型
abstract boolean getBoolean(String key, boolean defValue)
//读取的数据为float类型
abstract float getFloat(String key, float defValue)
//读取的数据为int类型
abstract int getInt(String key, int defValue)
//读取的数据为long类型
abstract long getLong(String key, long defValue)
//读取的数据为String类型
abstract String getString(String key, String defValue)
//读取的数据为Set<String>类型
abstract Set<String> getStringSet(String key, Set<String> defValues)
2.4 例子
1)写入数据:
//步骤1:创建一个SharedPreferences对象
SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);
//步骤2: 实例化SharedPreferences.Editor对象
SharedPreferences.Editor editor = sharedPreferences.edit();
//步骤3:将获取过来的值放入文件
editor.putString("name", “Tom”);
editor.putInt("age", 28);
editor.putBoolean("marrid",false);
//步骤4:提交
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);
//覆盖key为age的数据,得到的结果:age = 32
editor.putInt("age", 32);
editor.putString("age", "20");
//覆盖key为age的数据,得到的结果:age = 32 (int类型)
editor.putInt("age", 32);
tip 4:
执行以下代码会出现异常。
(指定key所保存的类型和读取时的类型不同)
editor.putInt("age", 32);//保存为int类型
String age = userInfo.getString("age", "null");//读取时为String类型,出现异常
tip 5:
在这些动作之后,记得commit
editor.putInt("age", 20);//写入操作
editor.remove("age"); //移除操作
editor.clear(); //清空操作
editor.commit();//记得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) // 使用 Int 类型的数据覆盖相同的 key
sp.getString(key, ""); // 使用相同的 key 读取 Sting 类型的数据
使用 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();
//这里只是创建了一个 Runnable ,并不是一个线程
final Runnable awaitCommit = new Runnable()
@Override
public void run()
try
//注意这里会进行等待也就是 需要 MemoryCommitResult 的 setDiskWriteResult 方法执行后
//才能返回
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
Runnable postWriteRunnable = new Runnable()
@Override
public void run()
//这里执行了上面的 awaitCommit 的 run 方法
//不是 start
//并将队列中的 awaitCommit 移除
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);
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable)
final boolean isFromSyncCommit = (postWriteRunnable == null);
//创建一个 Runnable ,同样也没有 start
final Runnable writeToDiskRunnable = new Runnable()
@Override
public void run()
synchronized (mWritingToDiskLock)
writeToFile(mcr, isFromSyncCommit);
synchronized (mLock)
mDiskWritesInFlight--;
if (postWriteRunnable != null)
postWriteRunnable.run();
;
//如果是 commit 则执行这里并返回
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit)
boolean wasEmpty = false;
synchronized (mLock)
wasEmpty = mDiskWritesInFlight == 1;
if (wasEmpty)
writeToDiskRunnable.run();
return;
//如果是 apply 就执行这里
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
在 QueuedWork.java 中:
public static void queue(Runnable work, boolean shouldDelay)
//getHandler 获取的是一个 handlerThread 的hanlder ,也就是一个子线程
Handler handler = getHandler();
synchronized (sLock)
sWork.add(work);
if (shouldDelay && sCanDelay)
//发送一个消息 MSG_RUN 到 handler 所在线程,也就是 handlerThread 子线程中去
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
else
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
private static Handler getHandler()
synchronized (sLock)
if (sHandler == null)
//创建一个 handlerThread ,并执行 start 方法
//这就是 apply 写到磁盘的线程
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
return sHandler;
// Handler 的处理
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();
// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
//一个一个执行 run 方法,
if (work.size() > 0)
for (Runnable w : work)
w.run();
在上面的方法中注意到 对每个 apply 都会创建一个相应的 awaitCommit,并添加到 QueuedWork 的一个队列中,但是在 QueuedWork 注意到有这样一个方法 waitToFinish
/**
* Trigger queued work to be processed immediately. The queued work is processed on a separate
* thread asynchronous. While doing that run and process all finishers on this thread. The
* finishers can be implemented in a way to check weather the queued work is finished.
*
* Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
* after Service command handling, etc. (so async work is never lost)
*/
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))
// Delayed work will be processed at processPendingWork() below
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
// We should not delay any work as this might delay the finishers
sCanDelay = false;
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try
//这个方法就会执行 所有的 Runnable 的run 返回
//这个时候 processPendingWork 是执行在主线程中
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。
以上是关于Android——SharedPreferences的主要内容,如果未能解决你的问题,请参考以下文章
Android中如何设置SharedPreference文件名称?
如何在android中使用SharedPreference存储动态表值[重复]
Android 如何从 SharedPreference 设置 EditTextPreference 的默认值?