Android性能优化 - 从SharedPreferences到DataStore

前言

对于android开发者们来说,SharedPreferences已经是一个老生常谈的话题了,之所以还在性能优化这个专栏中再次提到,是因为在实际项目中还是会有很多使用到的地方,同时它也有足够的“坑”,比如常见的主进程阻塞,虽然SharedPreferences 提供了异步操作api apply,但是apply方法依旧有可能造成ANR。

下面我们分析一下异步操作api apply方法的源码:

public void apply() {final long startTime = System.currentTimeMillis();final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic 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() {@Overridepublic void run() {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);
}

通过分析会,我们看到runnable被写入了队列,而这个队列会在handleStopService() 、handlePauseActivity() 、 handleStopActivity() 的时候会一直等待 apply() 方法将数据保存成功,否则会一直等待,从而阻塞主线程造成 ANR。

@Override
public void handlePauseActivity(ActivityClientRecord r, boolean finished, boolean userLeaving,int configChanges, PendingTransactionActions pendingActions, String reason) {if (userLeaving) {performUserLeavingActivity(r);}r.activity.mConfigChangeFlags |= configChanges;performPauseActivity(r, finished, reason, pendingActions);// Make sure any pending writes are now committed.if (r.isPreHoneycomb()) {// 这里就是元凶QueuedWork.waitToFinish();}mSomeActivitiesChanged = true;
}

Google 分析的 SharedPreferences 和 DataStore 的区别的一张图:
在这里插入图片描述

虽然QueuedWork在android 8中有了新的优化,但是实际上依旧有ANR的出现,在低版本的机型上更加出现频繁,所以我们不可能把sp真的逃避掉。

目前业内有很多替代的方案,就是采用MMKV去解决,但是官方并没有采用像mmkv的方式去解决,而是另起炉灶,在jetpack中引入DataStore去替代旧时代的SharedPreferences。

下面我们就分析一下DataStore的事项原理。

走进DataStore

Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。

DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore(基于protocol buffers)。

  • Preferences DataStore:仅使用键存储和访问值数据。此实现不需要预定义的架构,并且不提供类型安全性。
  • Proto DataStore:将数据存储为自定义数据类型的实例。此实现要求您使用协议缓冲区(protobuf - PB协议)定义架构,但它提供类型安全性。

我们这里主要以Preferences DataStore作为分析,同时在kotlin中,datastore采取了flow的良好架构,进行了内部的调度实现,同时也提供了java兼容版本(采用RxJava实现)

1:使用例子

val Context.dataStore : DataStore<Preferences> by preferencesDataStore(“文件名”)

因为datastore需要依靠协程的环境,所以我们可以有以下方式实现读写:

  • 读取数据
CoroutineScope(Dispatchers.Default).launch {context.dataStore.data.collect {value = it[booleanPreferencesKey(key)] ?: defValue}
}
  • 写入数据
CoroutineScope(Dispatchers.IO).launch {context.dataStore.edit { settings ->settings[booleanPreferencesKey(key) ] = value}}

其中booleanPreferencesKey代表着存入的value是boolean类型,同样的,假设我们需要存入的数据类型是String,相应的key就是通过stringPreferencesKey(key名) 创建。同时因为返回的是flow,我们是需要调用collect这种监听机制去获取数值的改变,如果想要像sp一样采用同步的方式直接获取,官方通过runBlocking进行获取,比如

val exampleData = runBlocking { context.dataStore.data.first() }

2:DataStore实现原理

DataStore提供给了我们非常简洁的api,所以我们也能够很快速的入门使用,但是其中的原理实现,我们是要了解的,因为其创建过程十分简单,我们就从数据更新(context.dataStore.edit)的角度出发,看看DataStore究竟做了什么.

首先我们看到edit方法:

public suspend fun DataStore<Preferences>.edit(transform: suspend (MutablePreferences) -> Unit
): Preferences {return this.updateData {// It's safe to return MutablePreferences since we freeze it in// PreferencesDataStore.updateData()it.toMutablePreferences().apply { transform(this) }}
}

可以看到edit方法是一个suspend的函数,其主要的实现就是依靠updateData方法

interface DataStore<T> 中:public suspend fun updateData(transform: suspend (t: T) -> T): T

DataStore是有两种实现,我们要看的就是Preferences DataStore的实现,其实现类是

internal class PreferenceDataStore(private val delegate: DataStore<Preferences>) :DataStore<Preferences> by delegate {override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences):Preferences {return delegate.updateData {val transformed = transform(it)// Freeze the preferences since any future mutations will break DataStore. If a user// tunnels the value out of DataStore and mutates it, this could be problematic.// This is a safe cast, since MutablePreferences is the only implementation of// Preferences.(transformed as MutablePreferences).freeze()transformed}}
}

可以看到PreferenceDataStore中updateData方法的具体实现其实在delegate中,而这个delegate的创建是在PreferenceDataStoreFactory中,


public fun create(corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,migrations: List<DataMigration<Preferences>> = listOf(),scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),produceFile: () -> File
): DataStore<Preferences> {val delegate = DataStoreFactory.create(serializer = PreferencesSerializer,corruptionHandler = corruptionHandler,migrations = migrations,scope = scope) {忽略}return PreferenceDataStore(delegate)
}

而在DataStoreFactory.create方法中:

public fun <T> create(serializer: Serializer<T>,corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,migrations: List<DataMigration<T>> = listOf(),scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),produceFile: () -> File): DataStore<T> =SingleProcessDataStore(produceFile = produceFile,serializer = serializer,corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),scope = scope)
}

DataStoreFactory.create 创建的其实是一个SingleProcessDataStore的对象,SingleProcessDataStore同时也是继承于DataStore,它就是所有DataStore背后的真正的实现者。而它的updateData方法就是解决问题的幕后助力。

override suspend fun updateData(transform: suspend (t: T) -> T): T {val ack = CompletableDeferred<T>()val currentDownStreamFlowState = downstreamFlow.valueval updateMsg =Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)actor.offer(updateMsg)return ack.await()
}

我们可以看到,update方法中,有一个叫 ack的 CompletableDeferred对象,而CompletableDeferred,是继承于Deferred。我们到这里就应该能够猜到了,这个Deferred对象不正是我们协程中常用的异步调用类嘛!它提供了await操作允许我们等待异步的结果。 最后封装好的Message被放入actor.offer(updateMsg) 中,actor是消息处理类对象,它的定义如下

internal class SimpleActor<T>(/*** The scope in which to consume messages.*/private val scope: CoroutineScope,/*** Function that will be called when scope is cancelled. Should *not* throw exceptions.*/onComplete: (Throwable?) -> Unit,/*** Function that will be called for each element when the scope is cancelled. Should *not** throw exceptions.*/onUndeliveredElement: (T, Throwable?) -> Unit,/*** Function that will be called once for each message.** Must *not* throw an exception (other than CancellationException if scope is cancelled).*/private val consumeMessage: suspend (T) -> Unit
) {private val messageQueue = Channel<T>(capacity = UNLIMITED)

我们看到,我们所有的消息会被放到一个叫messageQueueChannel对象中,Channel其实就是一个适用于协程信息通信的线程安全的队列。

最后我们看看offer函数干了什么:

.......do {// We don't want to try to consume a new message unless we are still active.// If ensureActive throws, the scope is no longer active, so it doesn't// matter that we have remaining messages.scope.ensureActive()consumeMessage(messageQueue.receive())
} while (remainingMessages.decrementAndGet() != 0)
.........

其实就是通过consumeMessage消费了我们的消息。到这里我们再一次回到我们DataStore中的SimpleActor实现对象

private val actor = SimpleActor<Message<T>>(scope = scope,onComplete = {it?.let {downstreamFlow.value = Final(it)}// We expect it to always be non-null but we will leave the alternative as a no-op// just in case.synchronized(activeFilesLock) {activeFiles.remove(file.absolutePath)}},onUndeliveredElement = { msg, ex ->if (msg is Message.Update) {// TODO(rohitsat): should we instead use scope.ensureActive() to get the original//  cancellation cause? Should we instead have something like//  UndeliveredElementException?msg.ack.completeExceptionally(ex ?: CancellationException("DataStore scope was cancelled before updateData could complete"))}}
) { consumeMessage 实际msg ->when (msg) {is Message.Read -> {handleRead(msg)}is Message.Update -> {handleUpdate(msg)}}
}

可以看到,consumeMessage其实就是以lambada形式展开了,实现的内容也很直观,如果是Message.Update就调用了handleUpdate方法

private suspend fun handleUpdate(update: Message.Update<T>) {// 这里就是completeWith调用,也就是回到了外部Deferred的await方法update.ack.completeWith(runCatching {when (val currentState = downstreamFlow.value) {is Data -> {// We are already initialized, we just need to perform the updatetransformAndWrite(update.transform, update.callerContext)}...

最后通过了transformAndWrite调用writeData方法,写入数据(FileOutputStream)

internal suspend fun writeData(newData: T) {file.createParentDirectories()val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX)try {FileOutputStream(scratchFile).use { stream ->serializer.writeTo(newData, UncloseableOutputStream(stream))stream.fd.sync()// TODO(b/151635324): fsync the directory, otherwise a badly timed crash could//  result in reverting to a previous state.}if (!scratchFile.renameTo(file)) {throw IOException("Unable to rename $scratchFile." +"This likely means that there are multiple instances of DataStore " +"for this file. Ensure that you are only creating a single instance of " +"datastore for this file.")}

至此,我们整个过程就彻底分析完了,读取数据跟写入数据类似,只是最后调用的处理函数不一致罢了(consumeMessage 调用handleRead),同时我们也分析出来handleUpdate的update.ack.completeWith让我们也回到了协程调用完成后的世界。

实战SharedPreferences全局替换成DataStore

分析完DataStore,我们已经有了足够的了解,那么我们就实战SharedPreferences迁移至DataStore了吧!

1:如何将旧sp数据迁移到datastore?

已存在的sp对象数据,我们可以通过以下方法无缝迁移到datastore

dataStore = context.createDataStore( name = preferenceName, migrations = listOf( SharedPreferencesMigration( context, "sp的名称" ) ) )

2:如何实现无侵入替换sp为DataStore?

当然,我们项目中可能会存在很多历史遗留的sp使用,此时用手动替换会容易出错,而且不方便,其次是三方库所用到sp我们也无法手动更改,那么有没有一种方案可以无需对原有项目改动,就可以迁移到DataStore呢?这个时候就是我们的性能优化系列的老朋友,ASM登场啦!

我们来分析一下,怎么把SharedPreferences

val sp = this.getSharedPreferences("test",0)
val editor = sp.edit()
editor.putBoolean("testBoolean",true)
editor.apply()

替换成我们想要的DataStore,不及,我们先看一下这串代码的字节码

LINENUMBER 24 L2ALOAD 0LDC "test"ICONST_0INVOKEVIRTUAL com/example/spider/MainActivity.getSharedPreferences (Ljava/lang/String;I)Landroid/content/SharedPreferences;ASTORE 2

我们可以看到,我们的字节码中存在ALOAD ASTORE这种依赖于操作数栈环境的指令,就知道不能简单的实现指令替换,而是采用同类替换的方式去现实,即我们可以通过继承于SharedPreferences,在自定义SharedPreferences中实现DataStore的操作,严格来说,这个自定义SharedPreferences,其实就相当于一个壳子了。这种替换方式在Android性能优化-线程监控与线程统一也有使用到。
在这里插入图片描述
我们来看一下自定义的SharedPreferences操作,这里以putBoolean相关操作举例子

class DataPreference(val context: Context,name:String):SharedPreferences {val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name)override fun getBoolean(key: String, defValue: Boolean): Boolean {var value = defValuerunBlocking {}runBlocking {context.dataStore.data.first {value = it[booleanPreferencesKey(key)] ?: defValuetrue}}
//        CoroutineScope(Dispatchers.Default).launch {
//            context.dataStore.data.collect {
//
//                value = it[booleanPreferencesKey(key)] ?: defValue
//                Log.e("hello","value os $value")
//            }
//        }return value}override fun edit(): SharedPreferences.Editor {return DataEditor(context)}inner class DataEditor(private val context: Context): SharedPreferences.Editor {override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {CoroutineScope(Dispatchers.IO).launch {context.dataStore.edit { settings ->settings[booleanPreferencesKey(key) ] = value}}return this}override fun commit(): Boolean {// 空实现即可}override fun apply() {// 空实现即可}}
}

因为putBoolean中其实就已经把数据存好了,所有我们的commit/apply都可以以空实现的方式替代。同时我们也声明一个扩展函数

fun Context.getDataPreferences(name:String,mode:Int): SharedPreferences {return DataPreference(this,name)
}

字节码部分操作也比较简单,我们只需要把原本的 INVOKEVIRTUAL com/example/spider/MainActivity.getSharedPreferences (Ljava/lang/String;I)Landroid/content/SharedPreferences; 指令替换成INVOKESTATIC的StoreTestKt扩展函数getDataPreferences调用即可,同时由于接受的是SharedPreferences类型而不是我们的DataPreference类型,所以需要采用CHECKCAST转换。

static void spToDataStore(MethodInsnNode node,ClassNode klass,MethodNode method
) {println("init  ===>  " + node.name+" --"+node.desc + " " + node.owner)if (node.name.equals("getSharedPreferences")&&node.desc.equals("(Ljava/lang/String;I)Landroid/content/SharedPreferences;")) {MethodInsnNode methodHookNode = new MethodInsnNode(Opcodes.INVOKESTATIC,"com/example/spider/StoreTestKt","getDataPreferences","(Landroid/content/Context;Ljava/lang/String;I)Landroid/content/SharedPreferences;",false)TypeInsnNode typeInsnNode = new TypeInsnNode(Opcodes.CHECKCAST, "android/content/SharedPreferences")InsnList insertNodes = new InsnList()insertNodes.add(methodHookNode)insertNodes.add(typeInsnNode)method.instructions.insertBefore(node, insertNodes)method.instructions.remove(node)println("hook  ===>  " + node.name + " " + node.owner + " " + method.instructions.indexOf(node))}}

3:分析实现方案的“不足”之处以及改进方案

当然,我们这个方案并不是百分比完美的,他也是存在一些缺点的。

editor.apply()
sp.getBoolean

原因是如果采用这种方式apply()后立马取数据,因为我们替换后putBoolean其实是一个异步操作,而我们getBoolean是同步操作,所以就有可能没有拿到最新的数据。但是这个使用姿势本身就是一个不好的使用姿势,同时业内的滴滴开源Booster的sp异步线程commit优化也同样有这个问题。因为put之后立马get不是一个规范写法,所以我们也不会对此多加干预。不过对于我们DataStore替换后来说,也有更加好的解决方式:

CoroutineScope(Dispatchers.Default).launch {context.dataStore.data.collect {value = it[booleanPreferencesKey(key)] ?: defValueLog.e("hello","value os $value")}
}

通过flow的异步特性,我们完全可以对value进行collect,调用层通过collect进行数据的收集,就能够做到万无一失啦(虽然也带来了侵入性)

总结

通过分析SP和DataStore的优缺点,以及谷歌官方给出的SP和DataStore的对照图,我们发现DataStore相对于SP的缺做了不少的提升,通过我们分析,也知道使用SP异步存储数据的缺会存在ANR,作为一个开发者,我们必须从Android性能优化方面多家考虑,因此,我觉DataStore替换SP是非常有必要的。

当然也有人会说我们可以使用MMKV,从而更优的优化我们app的性能,别急接下来,我也会专门出一期关于MMKV的讲解,以此来对比三者各自的优缺点和使用场景,让我们在开发时可以进一步灵活选取和使用适合当前开发场景的开发框架。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/232212.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ubuntu下训练自己的yolov5数据集

参考文档 yolov5-github yolov5-github-训练文档 csdn训练博客 一、配置环境 1.1 安装依赖包 前往清华源官方地址 选择适合自己的版本替换自己的源 # 备份源文件 sudo cp /etc/apt/sources.list /etc/apt/sources.list_bak # 修改源文件 # 更新 sudo apt update &&a…

万界星空科技/仓库管理WMS系统/免费仓库管理系统

仓库管理&#xff08;仓储管理&#xff09;&#xff0c;指对仓库及仓库内部的物资进行收发、结存等有效控制和管理&#xff0c;确保仓储货物的完好无损&#xff0c;保证生产经营活动的正常进行&#xff0c;在此基础上对货物进行分类记录&#xff0c;通过报表分析展示仓库状态、…

CAN网络出现错误帧从哪些方面去分析解决

标题&#xff1a;CAN网络出现错误帧从哪些方面去分析 实例1&#xff1a; 断电重启后&#xff0c;会有错误帧产生。 检查方案&#xff1a; 查看收发模块的初始化、使能是否在发送CAN报文之前完成&#xff1f; 实例2&#xff1a; 周期性报文&#xff0c;有时会冒出一帧错误帧&…

网络运维与网络安全 学习笔记2023.11.29

网络运维与网络安全 学习笔记 第三十天 今日更新太晚啦&#xff01;&#xff01;&#xff01; 主要是今天工作时挨了一天骂&#xff0c;服了&#xff0c;下次记得骂的轻一点&#xff01;&#xff01;&#xff01; &#xff08;要不是为了那点微薄的薪资&#xff0c;谁愿意听你…

企业计算机服务器中了mallox勒索病毒如何解密,mallox勒索病毒文件恢复

随着网络技术的不断发展&#xff0c;网络安全威胁也在不断增加&#xff0c;勒索病毒攻击企业计算机服务器的事件时有发生&#xff0c;并且勒索病毒的加密形式也越来越复杂。近期&#xff0c;云天数据恢复中心陆续接到很多企业的求助&#xff0c;企业的计算机服务器遭到了mallox…

Java8 Stream API

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 Stream API与接口默认方…

私家车位共享APP-计算机毕业设计源码24384

目 录 摘要 1 绪论 1.1 课题的研究背景 1.2研究内容与研究目标 1.3ssm框架 1.4论文结构与章节安排 2 2 私家车位共享APP系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据增加流程 2.2.2 数据修改流程 2.2.3数据删除流程 2.3 系统功能分析 2.3.1功能性分析 2…

【SpringCloud原理】OpenFeign原来是这么基于Ribbon来实现负载均衡的

大家好&#xff0c;本文我将继续来剖析SpringCloud中负载均衡组件Ribbon的源码。本来我是打算接着OpenFeign动态代理生成文章直接讲Feign是如何整合Ribbon的&#xff0c;但是文章写了一半发现&#xff0c;如果不把Ribbon好好讲清楚&#xff0c;那么有些Ribbon的细节理解起来就很…

mac安装homebrew/brew遇到443

文章目录 问题描述解决方法方法一方法二 参考文献 问题描述 brew 全称Homebrew 是Mac OSX上的软件包管理工具 想在mac终端安装&#xff0c;运行网上提供的指令 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)&quo…

EMA训练微调

就是取前几个epoch的weight的平均值&#xff0c;可以缓解微调时的灾难性遗忘&#xff08;因为新数据引导&#xff0c;模型权重逐渐&#xff0c;偏离训练时学到的数据分布&#xff0c;忘记之前学好的先验知识&#xff09; class EMA():def __init__(self, model, decay):self.…

TikTok区块链实践:数字社交媒体的去中心化未来

随着区块链技术的日渐成熟&#xff0c;数字社交媒体行业也在探索如何整合区块链&#xff0c;以推动去中心化发展。在这一潮流中&#xff0c;TikTok作为全球领先的短视频平台&#xff0c;积极实践区块链技术&#xff0c;探索数字社交媒体的未来。本文将深入探讨TikTok的区块链实…

功能全面又强大的同步备份软件,你找到了吗?

随着企业规模的不断扩大&#xff0c;许多企业都会拥有自己的数据中心。因此每日员工都需要在服务器与服务中心之间调取文件&#xff0c;同时还需要对每日新增的业务数据进行实时同步。如果量比较小&#xff0c;一般问题不大&#xff1b;一旦数据比较大&#xff0c;量也比较大&a…