全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2022-10-08_Android性能优化 - 从SharedPreferences跨越到DataStore

您的位置:首页 >> 新闻 >> 行业资讯

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 的键值对存储有没有最优解? 阅读原文

上一篇:2024-02-05_【招聘】今日上新:bangX、ST. 、Forty Two 42 下一篇:2024-09-09_「营销看点」8月精彩案例复盘,给你好看!

TAG标签:

15
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价