Android性能优化 - 从SharedPreferences跨越到DataStore
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
再谈SharedPreferences对于android开发者们来说,SharedPreferences已经是一个有足够历史的话题了,之所以还在性能优化这个专栏中再次提到,是因为在实际项目中还是会有很多使用到的地方,同时它也有足够的“坑”,比如常见的主进程阻塞,虽然SharedPreferences 提供了异步操作api apply,但是apply方法依旧有可能造成ANR。
publicvoidapply(){
finallongstartTime=System.currentTimeMillis();
finalMemoryCommitResultmcr=commitToMemory();
finalRunnableawaitCommit=newRunnable(){
@Override
publicvoidrun(){
try{
mcr.writtenToDiskLatch.await();
}catch(InterruptedExceptionignored){
}
if(DEBUGmcr.wasWritten){
Log.d(TAG,mFile.getName()+":"+mcr.memoryStateGeneration
+"appliedafter"+(System.currentTimeMillis()-startTime)
+"ms");
}
}
QueuedWork.addFinisher(awaitCommit);
RunnablepostWriteRunnable=newRunnable(){
@Override
publicvoidrun(){
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
//写入队列
SharedPreferencesImpl.this.enqueueDiskWrite(mcr,postWriteRunnable);
//Okaytonotifythelistenersbeforeit'shitdisk
//becausethelistenersshouldalwaysgetthesame
//SharedPreferencesinstanceback,whichhasthe
//changesreflectedinmemory.
notifyListeners(mcr);
}
我们可以看到我们的runnable被写入了队列,而这个队列会在handleStopService()、handlePauseActivity()、handleStopActivity()的时候会一直等待 apply() 方法将数据保存成功,否则会一直等待,从而阻塞主线程造成 ANR。
@Override
publicvoidhandlePauseActivity(ActivityClientRecordr,booleanfinished,booleanuserLeaving,
intconfigChanges,PendingTransactionActionspendingActions,Stringreason){
if(userLeaving){
performUserLeavingActivity(r);
}
r.activity.mConfigChangeFlags|=configChanges;
performPauseActivity(r,finished,reason,pendingActions);
//Makesureanypendingwritesarenowcommitted.
if(r.isPreHoneycomb()){
//这里就是元凶
QueuedWork.waitToFinish();
}
mSomeActivitiesChanged=true;
}
谷歌官方也有解释
虽然QueuedWork在android 8中有了新的优化,但是实际上依旧有ANR的出现,在低版本的机型上更加出现频繁,所以我们不可能把sp真的逃避掉。
目前业内有很多替代的方案,就是采用MMKV去解决,但是官方并没有采用像mmkv的方式去解决,而是另起炉灶,在jetpack中引入DataStore去替代旧时代的SharedPreferences。
DataStoreJetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。
DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore(基于protocol buffers)。我们这里主要以Preferences DataStore作为分析,同时在kotlin中,datastore采取了flow的良好架构,进行了内部的调度实现,同时也提供了java兼容版本(采用RxJava实现)
使用例子valContext.dataStore:DataStorePreferencesbypreferencesDataStore(“文件名”)
因为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进行获取,比如
valexampleData=runBlocking{context.dataStore.data.first()}
DataStore原理DataStore提供给了我们非常简洁的api,所以我们也能够很快速的入门使用,但是其中的原理实现,我们是要了解的,因为其创建过程十分简单,我们就从数据更新(context.dataStore.edit)的角度出发,看看DataStore究竟做了什么。
首先我们看到edit方法
publicsuspendfunDataStorePreferences.edit(
transform:suspend(MutablePreferences)-Unit
):Preferences{
returnthis.updateData{
//It'ssafetoreturnMutablePreferencessincewefreezeitin
//PreferencesDataStore.updateData()
it.toMutablePreferences().apply{transform(this)}
}
}
可以看到edit方法是一个suspend的函数,其主要的实现就是依靠updateData方法的调用
interfaceDataStore中:
publicsuspendfunupdateData(transform:suspend(t:T)-T):T
我们分析到DataStore是有两种实现,我们要看的就是Preferences DataStore的实现,其实现类是
internalclassPreferenceDataStore(privatevaldelegate:DataStorePreferences):
DataStorePreferencesbydelegate{
overridesuspendfunupdateData(transform:suspend(t:Preferences)-Preferences):
Preferences{
returndelegate.updateData{
valtransformed=transform(it)
//FreezethepreferencessinceanyfuturemutationswillbreakDataStore.Ifauser
//tunnelsthevalueoutofDataStoreandmutatesit,thiscouldbeproblematic.
//Thisisasafecast,sinceMutablePreferencesistheonlyimplementationof
//Preferences.
(transformedasMutablePreferences).freeze()
transformed
}
}
}
可以看到PreferenceDataStore中updateData方法的具体实现其实在delegate中,而这个delegate的创建是在
PreferenceDataStoreFactory中
publicfuncreate(
corruptionHandler:ReplaceFileCorruptionHandlerPreferences?=null,
migrations:ListDataMigrationPreferences=listOf(),
scope:CoroutineScope=CoroutineScope(Dispatchers.IO+SupervisorJob()),
produceFile:()-File
):DataStorePreferences{
valdelegate=DataStoreFactory.create(
serializer=PreferencesSerializer,
corruptionHandler=corruptionHandler,
migrations=migrations,
scope=scope
){
忽略
}
returnPreferenceDataStore(delegate)
}
DataStoreFactory.create方法中:
publicfunTcreate(
serializer:Serializer,
corruptionHandler:ReplaceFileCorruptionHandler?=null,
migrations:ListDataMigration=listOf(),
scope:CoroutineScope=CoroutineScope(Dispatchers.IO+SupervisorJob()),
produceFile:()-File
):DataStore=
SingleProcessDataStore(
produceFile=produceFile,
serializer=serializer,
corruptionHandler=corruptionHandler?:NoOpCorruptionHandler(),
initTasksList=listOf(DataMigrationInitializer.getInitializer(migrations)),
scope=scope
)
}
DataStoreFactory.create 创建的其实是一个SingleProcessDataStore的对象,SingleProcessDataStore同时也是继承于DataStore,它就是所有DataStore背后的真正的实现者。而它的updateData方法就是一切谜团解决的钥匙。
overridesuspendfunupdateData(transform:suspend(t:T)-T):T{
valack=CompletableDeferred()
valcurrentDownStreamFlowState=downstreamFlow.value
valupdateMsg=
Message.Update(transform,ack,currentDownStreamFlowState,coroutineContext)
actor.offer(updateMsg)
returnack.await()
}
我们可以看到,update方法中,有一个叫 ack的 CompletableDeferred对象,而CompletableDeferred,是继承于**Deferred**。我们到这里就应该能够猜到了,这个Deferred对象不正是我们协程中常用的异步调用类嘛!它提供了await操作允许我们等待异步的结果。 最后封装好的Message被放入actor.offer(updateMsg) 中,actor是消息处理类对象,它的定义如下
internalclassSimpleActor(
/**
*Thescopeinwhichtoconsumemessages.
*/
privatevalscope:CoroutineScope,
/**
*Functionthatwillbecalledwhenscopeiscancelled.Should*not*throwexceptions.
*/
onComplete:(Throwable?)-Unit,
/**
*Functionthatwillbecalledforeachelementwhenthescopeiscancelled.Should*not*
*throwexceptions.
*/
onUndeliveredElement:(T,Throwable?)-Unit,
/**
*Functionthatwillbecalledonceforeachmessage.
*
*Must*not*throwanexception(otherthanCancellationExceptionifscopeiscancelled).
*/
privatevalconsumeMessage:suspend(T)-Unit
){
privatevalmessageQueue=Channel(capacity=UNLIMITED)
我们看到,我们所有的消息会被放到一个叫messageQueue的Channel对象中,Channel其实就是一个适用于协程信息通信的线程安全的队列。
最后我们回到主题,offer函数干了什么
省略前面
do{
//Wedon'twanttotrytoconsumeanewmessageunlesswearestillactive.
//IfensureActivethrows,thescopeisnolongeractive,soitdoesn't
//matterthatwehaveremainingmessages.
scope.ensureActive()
consumeMessage(messageQueue.receive())
}while(remainingMessages.decrementAndGet()!=0)
其实就是通过consumeMessage消费了我们的消息。到这里我们再一次回到我们DataStore中的SimpleActor实现对象
privatevalactor=SimpleActorMessage(
scope=scope,
onComplete={
it?.let{
downstreamFlow.value=Final(it)
}
//Weexpectittoalwaysbenon-nullbutwewillleavethealternativeasano-op
//justincase.
synchronized(activeFilesLock){
activeFiles.remove(file.absolutePath)
}
},
onUndeliveredElement={msg,ex-
if(msgisMessage.Update){
//TODO(rohitsat):shouldweinsteadusescope.ensureActive()togettheoriginal
//cancellationcause?Shouldweinsteadhavesomethinglike
//UndeliveredElementException?
msg.ack.completeExceptionally(
ex?:CancellationException(
"DataStorescopewascancelledbeforeupdateDatacouldcomplete"
)
)
}
}
){
consumeMessage实际
msg-
when(msg){
isMessage.Read-{
handleRead(msg)
}
isMessage.Update-{
handleUpdate(msg)
}
}
}
可以看到,consumeMessage其实就是以lambada形式展开了,实现的内容也很直观,如果是Message.Update就调用了handleUpdate方法
privatesuspendfunhandleUpdate(update:Message.Update){
//这里就是completeWith调用,也就是回到了外部Deferred的await方法
update.ack.completeWith(
runCatching{
when(valcurrentState=downstreamFlow.value){
isData-{
//Wearealreadyinitialized,wejustneedtoperformtheupdate
transformAndWrite(update.transform,update.callerContext)
}
...
最后通过了transformAndWrite调用writeData方法,写入数据(FileOutputStream)
internalsuspendfunwriteData(newData:T){
file.createParentDirectories()
valscratchFile=File(file.absolutePath+SCRATCH_SUFFIX)
try{
FileOutputStream(scratchFile).use{stream-
serializer.writeTo(newData,UncloseableOutputStream(stream))
stream.fd.sync()
//TODO(b/151635324):fsyncthedirectory,otherwiseabadlytimedcrashcould
//resultinrevertingtoapreviousstate.
}
if(!scratchFile.renameTo(file)){
throwIOException(
"Unabletorename$scratchFile."+
"ThislikelymeansthattherearemultipleinstancesofDataStore"+
"forthisfile.Ensurethatyouareonlycreatingasingleinstanceof"+
"datastoreforthisfile."
)
}
至此,我们整个过程就彻底分析完了,读取数据跟写入数据类似,只是最后调用的处理函数不一致罢了(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登场啦!
我们来分析一下,怎么把
valsp=this.getSharedPreferences("test",0)
valeditor=sp.edit()
editor.putBoolean("testBoolean",true)
editor.apply()
替换成我们想要的DataStore,不及,我们先看一下这串代码的字节码
LINENUMBER24L2
ALOAD0
LDC"test"
ICONST_0
INVOKEVIRTUALcom/example/spider/MainActivity.getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;
ASTORE2
我们可以看到,我们的字节码中存在ALOAD ASTORE这种依赖于操作数栈环境的指令,就知道不能简单的实现指令替换,而是采用同类替换的方式去现实,即我们可以通过继承于SharedPreferences,在自定义SharedPreferences中实现DataStore的操作,严格来说,这个自定义SharedPreferences,其实就相当于一个壳子了。这种替换方式在Android性能优化-线程监控与线程统一也有使用到。
image.png我们来看一下自定义的SharedPreferences操作,这里以putBoolean相关操作举例子
classDataPreference(valcontext:Context,name:String):SharedPreferences{
valContext.dataStore:DataStorePreferencesbypreferencesDataStore(name)
varatomicBoolean=AtomicBoolean(false)
overridefungetAll():MutableMapString,*{
TODO("Notyetimplemented")
}
overridefungetString(key:String?,defValue:String?):String?{
TODO("Notyetimplemented")
}
overridefungetStringSet(key:String?,defValues:MutableSetString?):MutableSetString?{
TODO("Notyetimplemented")
}
overridefungetInt(key:String?,defValue:Int):Int{
TODO("Notyetimplemented")
}
overridefungetLong(key:String?,defValue:Long):Long{
TODO("Notyetimplemented")
}
overridefungetFloat(key:String?,defValue:Float):Float{
TODO("Notyetimplemented")
}
overridefungetBoolean(key:String,defValue:Boolean):Boolean{
varvalue=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","valueos$value")
//}
//}
returnvalue
}
overridefuncontains(key:String?):Boolean{
TODO("Notyetimplemented")
}
overridefunedit():SharedPreferences.Editor{
returnDataEditor(context)
}
overridefunregisterOnSharedPreferenceChangeListener(listener:SharedPreferences.OnSharedPreferenceChangeListener?){
TODO("Notyetimplemented")
}
overridefununregisterOnSharedPreferenceChangeListener(listener:SharedPreferences.OnSharedPreferenceChangeListener?){
TODO("Notyetimplemented")
}
innerclassDataEditor(privatevalcontext:Context):SharedPreferences.Editor{
overridefunputString(key:String?,value:String?):SharedPreferences.Editor{
TODO("Notyetimplemented")
}
overridefunputStringSet(key:String?,values:MutableSetString?):SharedPreferences.Editor{
TODO("Notyetimplemented")
}
overridefunputInt(key:String?,value:Int):SharedPreferences.Editor{
TODO("Notyetimplemented")
}
overridefunputLong(key:String?,value:Long):SharedPreferences.Editor{
TODO("Notyetimplemented")
}
overridefunputFloat(key:String?,value:Float):SharedPreferences.Editor{
TODO("Notyetimplemented")
}
overridefunputBoolean(key:String,value:Boolean):SharedPreferences.Editor{
CoroutineScope(Dispatchers.IO).launch{
context.dataStore.edit{settings-
settings[booleanPreferencesKey(key)]=value
}
}
returnthis
}
overridefunremove(key:String?):SharedPreferences.Editor{
TODO("Notyetimplemented")
}
overridefunclear():SharedPreferences.Editor{
TODO("Notyetimplemented")
}
overridefuncommit():Boolean{
TODO("Notyetimplemented")
}
overridefunapply(){
}
}
}
因为putBoolean中其实就已经把数据存好了,所有我们的commit/apply都可以以空实现的方式替代。同时我们也声明一个扩展函数
StoreTest.kt
funContext.getDataPreferences(name:String,mode:Int):SharedPreferences{
returnDataPreference(this,name)
}
字节码部分操作也比较简单,我们只需要把原本的INVOKEVIRTUAL com/example/spider/MainActivity.getSharedPreferences (Ljava/lang/String;I)Landroid/content/SharedPreferences;指令替换成INVOKESTATIC的StoreTestKt扩展函数getDataPreferences调用即可,同时由于接受的是SharedPreferences类型而不是我们的DataPreference类型,所以需要采用CHECKCAST转换。
staticvoidspToDataStore(
MethodInsnNodenode,
ClassNodeklass,
MethodNodemethod
){
println("init==="+node.name+"--"+node.desc+""+node.owner)
if(node.name.equals("getSharedPreferences")&&node.desc.equals("(Ljava/lang/String;I)Landroid/content/SharedPreferences;")){
MethodInsnNodemethodHookNode=newMethodInsnNode(Opcodes.INVOKESTATIC,
"com/example/spider/StoreTestKt",
"getDataPreferences",
"(Landroid/content/Context;Ljava/lang/String;I)Landroid/content/SharedPreferences;",
false)
TypeInsnNodetypeInsnNode=newTypeInsnNode(Opcodes.CHECKCAST,"android/content/SharedPreferences")
InsnListinsertNodes=newInsnList()
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","valueos$value")
}
}
通过flow的异步特性,我们完全可以对value进行collect,调用层通过collect进行数据的收集,就能够做到万无一失啦(虽然也带来了侵入性)
总结到这里,我们又完成了性能优化的一篇,sp迁移至DataStore的后续适配,等笔者有空了会写一个工具库(挖坑),虽然sp是一个非常久远的话题了,但是依旧值得我们分析,同时也希望DataStore能够被真正利用起来,适当的选用DataStore与MMKV。
DataStore的效率真的不比MMKV差,要结合实际使用场景!不能无脑MMKV,可见GDE 朱凯大佬的这篇面试黑洞】Android 的键值对存储有没有最优解?
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线