网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
// 将数据保存在 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 不能用于跨进程通信
我们在创建 SP 实例的时候,需要传入一个 mode
,如下所示:
val sp = getSharedPreferences(“ByteCode”, Context.MODE_PRIVATE)
复制代码
Context 内部还有一个 mode
是 MODE_MULTI_PROCESS
,我们来看一下这个 mode
做了什么
public SharedPreferences getSharedPreferences(File file, int mode) {
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// 重新读取 SP 文件内容
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
复制代码
在这里就做了一件事,当遇到 MODE_MULTI_PROCESS
的时候,会重新读取 SP 文件内容,并不能用 SP 来做跨进程通信。
到这里关于 SharedPreferences 部分分析完了,接下来分析一下 DataStore 为我们解决什么问题?
DataStore 解决了什么问题
Preferences DataStore 主要用来替换 SharedPreferences,Preferences DataStore 解决了 SharedPreferences 带来的所有问题
Preferences DataStore 相比于 SharedPreferences 优点
- DataStore 是基于 Flow 实现的,所以保证了在主线程的安全性
- 以事务方式处理更新数据,事务有四大特性(原子性、一致性、 隔离性、持久性)
- 没有
apply()
和commit()
等等数据持久的方法 - 自动完成 SharedPreferences 迁移到 DataStore,保证数据一致性,不会造成数据损坏
- 可以监听到操作成功或者失败结果
另外 Jetpack DataStore 提供了 Proto DataStore 方式,用于存储类的对象(typed objects ),通过 protocol buffers 将对象序列化存储在本地,protocol buffers 现在已经应用的非常广泛,无论是微信还是阿里等等大厂都在使用,我们在部分场景中也使用了 protocol buffers,在后续的文章会详细的分析。
注意:
Preferences DataStore 只支持 Int
, Long
, Boolean
, Float
, String
键值对数据,适合存储简单、小型的数据,并且不支持局部更新,如果修改了其中一个值,整个文件内容将会被重新序列化,可以运行 AndroidX-Jetpack-Practice/DataStoreSimple 体验一下,如果需要局部更新,建议使用 Room。
在项目中使用 Preferences DataStore
Preferences DataStore 主要应用在 MVVM 当中的 Repository 层,在项目中使用 Preferences DataStore 非常简单,只需要 4 步。
1. 需要添加 Preferences DataStore 依赖
implementation “androidx.datastore:datastore-preferences:1.0.0-alpha01”
复制代码
2. 构建 DataStore
private val PREFERENCE_NAME = “DataStore”
var dataStore: DataStore = context.createDataStore(
name = PREFERENCE_NAME
复制代码
3. 从 Preferences DataStore 中读取数据
Preferences DataStore 以键值对的形式存储在本地,所以首先我们应该定义一个 Key.
val KEY_BYTE_CODE = preferencesKey(“ByteCode”)
复制代码
这里和我们之前使用 SharedPreferences 的有点不一样,在 Preferences DataStore 中 Key 是一个 Preferences.Key<T>
类型,只支持 Int
, Long
, Boolean
, Float
, String
,源码如下所示:
inline fun preferencesKey(name: String): Preferences.Key {
return when (T::class) {
Int::class -> {
Preferences.Key(name)
}
String::class -> {
Preferences.Key(name)
}
Boolean::class -> {
Preferences.Key(name)
}
Float::class -> {
Preferences.Key(name)
}
Long::class -> {
Preferences.Key(name)
}
… // 如果是其他类型就会抛出异常
}
}
复制代码
当我们定义好 Key 之后,就可以通过 dataStore.data
来获取数据
override fun readData(key: Preferences.Key): Flow =
dataStore.data
.catch {
// 当读取数据遇到错误时,如果是 IOException
异常,发送一个 emptyPreferences 来重新使用
// 但是如果是其他的异常,最好将它抛出去,不要隐藏问题
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map { preferences ->
preferences[key] ?: false
}
复制代码
- Preferences DataStore 是基于 Flow 实现的,所以通过
dataStore.data
会返回一个Flow<T>
,每当数据变化的时候都会重新发出 catch
用来捕获异常,当读取数据出现异常时会抛出一个异常,如果是IOException
异常,会发送一个emptyPreferences()
来重新使用,如果是其他异常,最好将它抛出去
4. 向 Preferences DataStore 中写入数据
在 Preferences DataStore 中是通过 DataStore.edit()
写入数据的,DataStore.edit()
是一个 suspend 函数,所以只能在协程体内使用,每当遇到 suspend 函数以挂起的方式运行,并不会阻塞主线程。
以挂起的方式运行,不会阻塞主线程 :也就是协程作用域被挂起, 当前线程中协程作用域之外的代码不会阻塞。
首先我们需要创建一个 suspend 函数,然后调用 DataStore.edit()
写入数据即可。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
dit()` 写入数据即可。
[外链图片转存中…(img-Fe6qJyvh-1715480960064)]
[外链图片转存中…(img-KAMHRQIa-1715480960064)]
[外链图片转存中…(img-CuGBRbCv-1715480960064)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新