[Google]-再见-SharedPreferences-拥抱-Jetpack-DataStore,就是这么简单

本文探讨了SharedPreferences的并发读写问题,如何导致主线程阻塞,类型不安全及内存消耗,并深入剖析了apply()方法在特定生命周期下引发ANR的机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

synchronized (mLock) {
mLoaded = false;
}
new Thread(“SharedPreferencesImpl-load”) {
public void run() {
loadFromDisk();
}
}.start();
}
复制代码

正如你所看到的,开启一个线程异步读取数据,当我们正在读取一个比较大的数据,还没读取完,接着调用 getXXX() 方法。

public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}

private void awaitLoadedLocked() {

while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}

}
复制代码

在同步方法内调用了 wait() 方法,会一直等待 getSharedPreferences() 方法开启的线程读取完数据才能继续往下执行,如果读取几 KB 的数据还好,假设读取一个大的文件,势必会造成主线程阻塞。

SP 不能保证类型安全

调用 getXXX() 方法的时候,可能会出现 ClassCastException 异常,因为使用相同的 key 进行操作的时候,putXXX 方法可以使用不同类型的数据覆盖掉相同的 key。

val key = “jetpack”
val sp = getSharedPreferences(“ByteCode”, Context.MODE_PRIVATE) // 异步加载 SP 文件内容

sp.edit { putInt(key, 0) } // 使用 Int 类型的数据覆盖相同的 key
sp.getString(key, “”); // 使用相同的 key 读取 Sting 类型的数据
复制代码

使用 Int 类型的数据覆盖掉相同的 key,然后使用相同的 key 读取 Sting 类型的数据,编译正常,但是运行会出现以下异常。

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
复制代码

SP 加载的数据会一直留在内存中

通过 getSharedPreferences() 方法加载的数据,最后会将数据存储在静态的成员变量中。

// 调用 getSharedPreferences 方法,最后会调用 getSharedPreferencesCacheLocked 方法
public SharedPreferences getSharedPreferences(File file, int mode) {

final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
return sp;
}

// 通过静态的 ArrayMap 缓存 SP 加载的数据
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

// 将数据保存在 sSharedPrefsCache 中
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {

ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}

return packagePrefs;
}
复制代码

通过静态的 ArrayMap 缓存每一个 SP 文件,而每个 SP 文件内容通过 Map 缓存键值对数据,这样数据会一直留在内存中,浪费内存。

apply() 方法是异步的,可能会发生 ANR

apply() 方法是异步的,为什么还会造成 ANR 呢?曾今的字节跳动就出现过这个问题,具体详情可以点击这里前去查看 剖析 SharedPreference apply 引起的 ANR 问题 而且 Google 也明确指出了 apply() 的问题。

[图片上传中…(image-ab9199-1602236582506-4)]

简单总结一下:apply() 方法是异步的,本身是不会有任何问题,但是当生命周期处于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的时候会一直等待 apply() 方法将数据保存成功,否则会一直等待,从而阻塞主线程造成 ANR,一起来分析一下为什么异步方法还会阻塞主线程,先来看看 apply() 方法的实现。
frameworks/base/core/java/android/app/SharedPreferencesImpl.java

public void apply() {
final long startTime = System.currentTimeMillis();

final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
mcr.writtenToDiskLatch.await(); // 等待

}
};
// 将 awaitCommit 添加到队列 QueuedWork 中
QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 8.0 之前加入到一个单线程的线程池中执行
// 8.0 之后加入 HandlerThread 中执行写入任务
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
复制代码

  • 将一个 awaitCommit 的 Runnable 任务,添加到队列 QueuedWork 中,在 awaitCommit 中会调用 await() 方法等待,在 handleStopService 、 handleStopActivity 等等生命周期会以这个作为判断条件,等待任务执行完毕
  • 将一个 postWriteRunnable 的 Runnable 写任务,通过 enqueueDiskWrite 方法,将写入任务加入到队列中,而写入任务在一个线程中执行

注意:在 8.0 之前和 8.0 之后 enqueueDiskWrite() 方法实现逻辑各不相同

在 8.0 之前调用 enqueueDiskWrite() 方法,将写入任务加入到 单个线程的线程池 中执行,如果 apply() 多次的话,任务将会依次执行,效率很低,android-7.0.0_r34 源码如下所示。

// android-7.0.0_r34: frameworks/base/core/java/android/app/SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {

QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

// android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java
public static ExecutorService singleThreadExecutor() {
synchronized (QueuedWork.class) {
if (sSingleThreadExecutor == null) {
sSingleThreadExecutor = Executors.newSingleThreadExecutor();
}
return sSingleThreadExecutor;
}
}
复制代码

通过 Executors.newSingleThreadExecutor() 方法创建了一个 单个线程的线程池,因此任务是串行的,通过 apply() 方法创建的任务,都会添加到这个线程池内。

在 8.0 之后将写入任务加入到 LinkedList 链表中,在 HandlerThread 中执行写入任务,android-10.0.0_r14 源码如下所示。

// android-10.0.0_r14: frameworks/base/core/java/android/app/SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

// android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.java

private static final LinkedList sWork = new LinkedList<>();

public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler(); // 获取 handlerThread.getLooper() 生成 Handler 对象
synchronized (sLock) {
sWork.add(work); // 将写入任务加入到 LinkedList 链表中

if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
复制代码

在 8.0 之后通过调用 handlerThread.getLooper() 方法生成 Handler,任务都会在 HandlerThread 中执行,所有通过 apply() 方法创建的任务,都会添加到 LinkedList 链表中。

当生命周期处于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的时候会调用 QueuedWork.waitToFinish() 会等待写入任务执行完毕,我们以其中 handlePauseActivity() 方法为例。

public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
int configChanges, PendingTransactionActions pendingActions, String reason) {

// 确保写任务都已经完成
QueuedWork.waitToFinish();

}
}
复制代码

正如你所看到的在 handlePauseActivity() 方法中,调用了 QueuedWork.waitToFinish() 方法,会等待所有的写入执行完毕,Google 在 8.0 之后对这个方法做了很大的优化,一起来看一下 8.0 之前和 8.0 之后的区别。

注意:在 8.0 之前和 8.0 之后 waitToFinish() 方法实现逻辑各不相同

在 8.0 之前 waitToFinish() 方法只做了一件事,会一直等待写入任务执行完毕,我先来看看在 android-7.0.0_r34 源码实现。
android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java

private static final ConcurrentLinkedQueue sPendingWorkFinishers =
new ConcurrentLinkedQueue();

public static void waitToFinish() {
Runnable toFinish;
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run(); // 相当于调用 mcr.writtenToDiskLatch.await() 方法
}
}
复制代码

  • sPendingWorkFinishers 是 ConcurrentLinkedQueue 实例,apply 方法会将写入任务添加到 sPendingWorkFinishers 队列中,在 单个线程的线程池 中执行写入任务,线程的调度并不由程序来控制,也就是说当生命周期切换的时候,任务不一定处于执行状态

  • toFinish.run() 方法,相当于调用 mcr.writtenToDiskLatch.await() 方法,会一直等待

  • waitToFinish() 方法就做了一件事,会一直等待写入任务执行完毕,其它什么都不做,当有很多写入任务,会依次执行,当文件很大时,效率很低,造成 ANR 就不奇怪了,尤其像字节跳动这种大规模的 App

在 8.0 之后 waitToFinish() 方法做了很大的优化,当生命周期切换的时候,会主动触发任务的执行,而不是一直在等着,我们来看看 android-10.0.0_r14 源码实现。
android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.java

private static final LinkedList sFinishers = new LinkedList<>();
public static void waitToFinish() {

try {
processPendingWork(); // 主动触发任务的执行
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}

try {
// 等待任务执行完毕
while (true) {
Runnable finisher;

synchronized (sLock) {
finisher = sFinishers.poll(); // 从 LinkedList 中取出任务
}

if (finisher == null) { // 当 LinkedList 中没有任务时会跳出循环
break;
}

finisher.run(); // 相当于调用 mcr.writtenToDiskLatch.await()
}
}


}
复制代码

在 waitToFinish() 方法中会主动调用 processPendingWork() 方法触发任务的执行,在 HandlerThread 中执行写入任务。

另外还做了一个很重要的优化,当调用 apply() 方法的时候,执行磁盘写入,都是全量写入,在 8.0 之前,调用 N 次 apply() 方法,就会执行 N 次磁盘写入,在 8.0 之后,apply() 方法调用了多次,只会执行最后一次写入,通过版本号来控制的。

SharedPreferences 的另外一个缺点就是 apply() 方法无法获取到操作成功或者失败的结果,而 commit() 方法是可以接收 MemoryCommitResult 里面的一个 boolean 参数作为结果,来看一下它们的方法签名。

public void apply() { … }

public boolean commit() { … }
复制代码

SP 不能用于跨进程通信

总结

现在新技术层出不穷,如果每次出新的技术,我们都深入的研究的话,很容易分散精力。新的技术可能很久之后我们才会在工作中用得上,当学的新技术无法学以致用,很容易被我们遗忘,到最后真的需要使用的时候,又要从头来过(虽然上手会更快)。

我觉得身为技术人,针对新技术应该是持拥抱态度的,入了这一行你就应该知道这是一个活到老学到老的行业,所以面对新技术,不要抵触,拥抱变化就好了。

Flutter 明显是一种全新的技术,而对于这个新技术在发布之初,花一个月的时间学习它,成本确实过高。但是周末花一天时间体验一下它的开发流程,了解一下它的优缺点、能干什么或者不能干什么。这个时间,并不是我们不能接受的。

如果有时间,其实通读一遍 Flutter 的文档,是最全面的一次对 Flutter 的了解过程。但是如果我们只有 8 小时的时间,我希望能关注一些最值得关注的点。

Android学习PDF+架构视频+面试文档+源码笔记

(跨平台开发(Flutter)、java基础与原理,自定义view、NDK、架构设计、性能优化、完整商业项目开发等)

om/a120464/Android-P7/blob/master/Android%E5%BC%80%E5%8F%91%E4%B8%8D%E4%BC%9A%E8%BF%99%E4%BA%9B%EF%BC%9F%E5%A6%82%E4%BD%95%E9%9D%A2%E8%AF%95%E6%8B%BF%E9%AB%98%E8%96%AA%EF%BC%81.md)

(跨平台开发(Flutter)、java基础与原理,自定义view、NDK、架构设计、性能优化、完整商业项目开发等)

[外链图片转存中…(img-aEnXzTUw-1646559345746)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值