手机网站封装用户体验,微信小程序什么时候上线的,爱站长尾词挖掘工具,网站建设兼职合同Google 新增加了一个新 Jetpack 的成员 DataStore#xff0c;主要用来替换 SharedPreferences#xff0c; DataStore 应该是开发者期待已久的库#xff0c;DataStore 是基于 Flow 实现的#xff0c;一种新的数据存储方案#xff0c;它提供了两种实现方式#xff1a;Proto…Google 新增加了一个新 Jetpack 的成员 DataStore主要用来替换 SharedPreferences DataStore 应该是开发者期待已久的库DataStore 是基于 Flow 实现的一种新的数据存储方案它提供了两种实现方式Proto DataStore存储类的对象typed objects 通过 protocol buffers 将对象序列化存储在本地protocol buffers 现在已经应用的非常广泛无论是微信还是阿里等等大厂都在使用我们在部分业务场景中也用到了 protocol buffers会在后续的文章详细分析Preferences DataStore以键值对的形式存储在本地和 SharedPreferences 类似但是 DataStore 是基于 Flow 实现的不会阻塞主线程并且保证类型安全Jetpack DataStore 将会分为至少 2 篇文章来分析今天这篇文章主要来介绍 Jetpack DataStore 其中一种实现方式 Preferences DataStore文章中的示例代码已经上传到 GitHub 欢迎前去查看 AndroidX-Jetpack-Practice/DataStoreSimple。GitHub 地址https://github.com/hi-dhl/AndroidX-Jetpack-Practice这篇文章会涉及到 Koltin flow 相关内容如果不了解可以先去看另外一篇文章 Kotlin Flow 是什么Channel 是什么通过这篇文章你将学习到以下内容那些年我们所经历的 SharedPreferences 坑为什么需要 DataStore它为我们解决了什么问题如何在项目中使用 DataStore如何迁移 SharedPreferences 到 DataStoreMMKV、DataStore、SharedPreferences 的不同之处?一个新库的出现必定为我们解决了一些问题那么 Jetpack DataStore 为我们解决什么问题呢在分析之前我们需要先来了解 SharedPreferences 都有那些坑。那些年我们所经历的 SharedPreferences 坑SharedPreference 是一个轻量级的数据存储方式使用起来也非常方便以键值对的形式存储在本地初始化 SharedPreference 的时候会将整个文件内容加载内存中因此会带来以下问题通过 getXXX() 方法获取数据可能会导致主线程阻塞SharedPreference 不能保证类型安全SharedPreference 加载的数据会一直留在内存中浪费内存apply() 方法虽然是异步的可能会发生 ANR在 8.0 之前和 8.0 之后实现各不相同apply() 方法无法获取到操作成功或者失败的结果接下来我们逐个来分析一下 SharedPreferences 带来的这些问题在文章中 SharedPreference 简称 SP。getXXX() 方法可能会导致主线程阻塞所有 getXXX() 方法都是同步的在主线程调用 get 方法必须等待 SP 加载完毕会导致主线程阻塞下面的代码我相信小伙伴们并不陌生。val sp getSharedPreferences(ByteCode, Context.MODE_PRIVATE) // 异步加载 SP 文件内容
sp.getString(jetpack, ); // 等待 SP 加载完毕
调用 getSharedPreferences() 方法最终会调用 SharedPreferencesImpl#startLoadFromDisk() 方法开启一个线程异步读取数据。 frameworks/base/core/java/android/app/SharedPreferencesImpl.javaprivate final Object mLock new Object();
private boolean mLoaded false;
private void startLoadFromDisk() {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 ArrayMapFile, SharedPreferencesImpl cache getSharedPreferencesCacheLocked();return sp;
}// 通过静态的 ArrayMap 缓存 SP 加载的数据
private static ArrayMapString, ArrayMapFile, SharedPreferencesImpl sSharedPrefsCache;// 将数据保存在 sSharedPrefsCache 中
private ArrayMapFile, SharedPreferencesImpl getSharedPreferencesCacheLocked() {......ArrayMapFile, SharedPreferencesImpl packagePrefs sSharedPrefsCache.get(packageName);if (packagePrefs null) {packagePrefs new ArrayMap();sSharedPrefsCache.put(packageName, packagePrefs);}return packagePrefs;
}
通过静态的 ArrayMap 缓存每一个 SP 文件而每个 SP 文件内容通过 Map 缓存键值对数据这样数据会一直留在内存中浪费内存。apply() 方法是异步的可能会发生 ANRapply() 方法是异步的为什么还会造成 ANR 呢曾今的字节跳动就出现过这个问题具体详情可以点击这里前去查看 剖析 SharedPreference apply 引起的 ANR 问题 而且 Google 也明确指出了 apply() 的问题。简单总结一下apply() 方法是异步的本身是不会有任何问题但是当生命周期处于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的时候会一直等待 apply() 方法将数据保存成功否则会一直等待从而阻塞主线程造成 ANR一起来分析一下为什么异步方法还会阻塞主线程先来看看 apply() 方法的实现。 frameworks/base/core/java/android/app/SharedPreferencesImpl.javapublic void apply() {final long startTime System.currentTimeMillis();final MemoryCommitResult mcr commitToMemory();final Runnable awaitCommit new Runnable() {Overridepublic void run() {mcr.writtenToDiskLatch.await(); // 等待......}};// 将 awaitCommit 添加到队列 QueuedWork 中QueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable new Runnable() {Overridepublic 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.javaprivate static final LinkedListRunnable 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.javaprivate static final ConcurrentLinkedQueueRunnable sPendingWorkFinishers new ConcurrentLinkedQueueRunnable();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.javaprivate static final LinkedListRunnable 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 主要用来替换 SharedPreferencesPreferences 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 DataStorePreferences DataStore 主要应用在 MVVM 当中的 Repository 层在项目中使用 Preferences DataStore 非常简单只需要 4 步。1. 需要添加 Preferences DataStore 依赖implementation androidx.datastore:datastore-preferences:1.0.0-alpha01
2. 构建 DataStoreprivate val PREFERENCE_NAME DataStore
var dataStore: DataStorePreferences context.createDataStore(name PREFERENCE_NAME
3. 从 Preferences DataStore 中读取数据Preferences DataStore 以键值对的形式存储在本地所以首先我们应该定义一个 Key.val KEY_BYTE_CODE preferencesKeyBoolean(ByteCode)
这里和我们之前使用 SharedPreferences 的有点不一样在 Preferences DataStore 中 Key 是一个 Preferences.KeyT 类型只支持 Int , Long , Boolean , Float , String源码如下所示inline fun reified T : Any preferencesKey(name: String): Preferences.KeyT {return when (T::class) {Int::class - {Preferences.KeyT(name)}String::class - {Preferences.KeyT(name)}Boolean::class - {Preferences.KeyT(name)}Float::class - {Preferences.KeyT(name)}Long::class - {Preferences.KeyT(name)}...... // 如果是其他类型就会抛出异常}
}
当我们定义好 Key 之后就可以通过 dataStore.data 来获取数据override fun readData(key: Preferences.KeyBoolean): FlowBoolean 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 会返回一个 FlowT每当数据变化的时候都会重新发出catch 用来捕获异常当读取数据出现异常时会抛出一个异常如果是 IOException 异常会发送一个 emptyPreferences() 来重新使用如果是其他异常最好将它抛出去4. 向 Preferences DataStore 中写入数据在 Preferences DataStore 中是通过 DataStore.edit() 写入数据的DataStore.edit() 是一个 suspend 函数所以只能在协程体内使用每当遇到 suspend 函数以挂起的方式运行并不会阻塞主线程。以挂起的方式运行不会阻塞主线程 也就是协程作用域被挂起, 当前线程中协程作用域之外的代码不会阻塞。首先我们需要创建一个 suspend 函数然后调用 DataStore.edit() 写入数据即可。override suspend fun saveData(key: Preferences.KeyBoolean) {dataStore.edit { mutablePreferences -val value mutablePreferences[key] ?: falsemutablePreferences[key] !value}
}
到这里关于 Preferences DataStore 读取数据和写入数据就已经分析完了接下来分析一下如何迁移 SharedPreferences 到 DataStore。迁移 SharedPreferences 到 DataStore迁移 SharedPreferences 到 DataStore 只需要 2 步。在构建 DataStore 的时候需要传入一个 SharedPreferencesMigrationdataStore context.createDataStore(name PREFERENCE_NAME,migrations listOf(SharedPreferencesMigration(context,SharedPreferencesRepository.PREFERENCE_NAME))
)
当 DataStore 对象构建完了之后需要执行一次读取或者写入操作即可完成 SharedPreferences 迁移到 DataStore当迁移成功之后会自动删除 SharedPreferences 使用的文件注意 只从 SharedPreferences 迁移一次因此一旦迁移成功之后应该停止使用 SharedPreferences。相比于 MMKV 有什么不同之处最后用一张表格来对比一下 MMKV、DataStore、SharedPreferences 的不同之处如果发现错误或者有其他不同之处期待你来一起完善。另外在附上一张 Google 分析的 SharedPreferences 和 DataStore 的区别全文到这里就结束了这篇文章主要分析了 SharedPreferences 和 DataStore 的优缺点以及为什么需要引入 DataStore 和如何使用 DataStore为了节省篇幅源码分析部分会在后续的文章中分析。关于 SharedPreferences 和 DataStore 相关的代码已经上传到了 GitHub 欢迎前去查看 AndroidX-Jetpack-Practice/DataStoreSimple 可以运行一下示例项目体验一下 SharedPreferences 和 DataStore 效果。GitHub 地址https://github.com/hi-dhl/AndroidX-Jetpack-Practice参考文献Preferences DataStore codelab Now in Android #25Prefer Storing Data with Jetpack DataStore剖析 SharedPreference 引起的 ANR 问题SharedPreferences 问题分析和解决结语我梳理了 LeetCode / 剑指 offer 及国内外大厂面试题解截止到目前为止我已经在 LeetCode 上 AC 了 124 题每题都会用 Java 和 kotlin 去实现并且每题都有多种解法、解题思路、时间复杂度、空间复杂度分析题库逐渐完善中欢迎前去查看。剑指 offer 及国内外大厂面试题解在线阅读LeetCode 系列题解在线阅读最后推荐我一直在更新维护的项目和网站计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章正在逐渐增加 Jetpack 新成员仓库持续更新欢迎前去查看hi-dhl/AndroidX-Jetpack-Practicegithub.comLeetCode / 剑指 Offer / 国内外大厂面试题涵盖 多线程、数组、栈、队列、字符串、链表、树查找算法、搜索算法、位运算、排序等等每道题目都会用 Java 和 kotlin 去实现仓库持续更新欢迎前去查看 hi-dhl/Leetcode-Solutions-with-Java-And-Kotlingithub.com剑指 offer 及国内外大厂面试题解在线阅读LeetCode 系列题解在线阅读最新 Android 10 源码分析系列文章了解系统源码不仅有助于分析问题在面试过程中对我们也是非常有帮助的仓库持续更新欢迎前去查看 Android10-Source-Analysis整理和翻译一系列精选国外的技术文章每篇文章都会有译者思考部分对原文的更加深入的解读仓库持续更新欢迎前去查看 Technical-Article-Translationgithub.com「为互联网人而设计国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址欢迎前去查看 Hi World | 为互联网人而设计的国内国外名站导航site.51git.cn