注册

Android性能优化 - 从SharedPreferences跨越到DataStore

再谈SharedPreferences


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


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

final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public 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() {
@Override
public 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;
}

谷歌官方也有解释
image.png


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


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


DataStore



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



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


使用例子


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() }

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.value

val 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)

我们看到,我们所有的消息会被放到一个叫messageQueue的Channel对象中,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 update
transformAndWrite(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了吧!


旧sp数据迁移


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


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

无侵入替换sp为DataStore


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


我们来分析一下,怎么把


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

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


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

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


image.png


我们来看一下自定义的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 = defValue
runBlocking {

}
runBlocking {
context.dataStore.data.first {
value = it[booleanPreferencesKey(key)] ?: defValue
true
}
}
// 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都可以以空实现的方式替代。同时我们也声明一个扩展函数


StoreTest.kt
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))
}

}

方案的“不足”


当然,我们这个方案并不是百分比完美的


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)] ?: defValue
Log.e("hello","value os $value")
}
}

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


总结


到这里,我们又完成了性能优化的一篇,sp迁移至DataStore的后续适配,等笔者有空了会写一个工具库(挖坑),虽然sp是一个非常久远的话题了,但是依旧值得我们分析,同时也希望DataStore能够被真正利用起来,适当的选用DataStore与MMKV。


作者:Pika
链接:https://juejin.cn/post/7151719783842381832
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册