本文为稀土技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

再谈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;
}

谷歌官方也有解说

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

虽然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功能优化-线程监控与线程一致也有运用到。

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

咱们来看一下自定义的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。

DataStore的功率真的不比MMKV差,要结合实践运用场景!不能无脑MMKV,可见GDE 朱凯大佬的这篇 面试黑洞】Android 的键值对存储有没有最优解?