听说Compose与RecyclerView结合会有水土不服
背景笔者碎碎谈
最近Compose也慢慢火起来了,作为google力推的ui框架,我们也要用起来才能进步呀!在最新一期的评测中LazyRow等LazyXXX列表组件已经慢慢逼近RecyclerView的性能了!但是还是有很多同学顾虑呀!没关系,我们就算用原有的view开发体系,也可以快速迁移到compose,这个利器就是ComposeView了,那么我们在RecyclerView的基础上,集成Compose用起来!这样我们有RecyclerView的性能又有Compose的好处不是嘛!相信很多人都有跟我一样的想法,但是这两者结合起来可是有隐藏的性能开销!(本次使用compose版本为1。1。1)在原有view体系接入Compose
在纯compose项目中,我们都会用setContent代替原有view体系的setContentView,比如setContent{ComposeTestTheme{AsurfacecontainerusingthebackgroundcolorfromthethemeSurface(modifierModifier。fillMaxSize(),colorMaterialTheme。colors。background){Greeting(Android)Hello()}}}
那么setContent到底做了什么事情呢?我们看下源码publicfunComponentActivity。setContent(parent:CompositionContext?null,content:Composable()Unit){valexistingComposeViewwindow。decorView。findViewByIdViewGroup(android。R。id。content)。getChildAt(0)as?ComposeViewif(existingComposeView!null)with(existingComposeView){setParentCompositionContext(parent)setContent(content)}elseComposeView(this)。apply{第一步走到这里SetcontentandparentbeforesetContentViewtohaveComposeViewcreatethecompositiononattachsetParentCompositionContext(parent)setContent(content)SettheviewtreeownersbeforesettingthecontentviewsothattheinflationprocessandattachlistenerswillseethemalreadypresentsetOwners()setContentView(this,DefaultActivityContentLayoutParams)}}
由于是第一次进入,那么一定就走到了else分支,其实就是创建了一个ComposeView,放在了android。R。id。content里面的第一个child中,这里就可以看到,compose并不是完全脱了原有的view体系,而是采用了移花接木的方式,把compose体系迁移了过来!ComposeView就是我们能用Compose的前提啦!所以在原有的view体系中,我们也可以通过ComposeView去嫁接到view体系中,我们举个例子classCustomActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle?){super。onCreate(savedInstanceState)setContentView(R。layout。activitycustom)valrecyclerViewthis。findViewByIdRecyclerView(R。id。recyclerView)recyclerView。adapterMyRecyclerViewAdapter()recyclerView。layoutManagerLinearLayoutManager(this)}}classMyRecyclerViewAdapter:RecyclerView。AdapterMyComposeViewHolder(){overridefunonCreateViewHolder(parent:ViewGroup,viewType:Int):MyComposeViewHolder{valviewComposeView(parent。context)returnMyComposeViewHolder(view)}overridefunonBindViewHolder(holder:MyComposeViewHolder,position:Int){holder。composeView。setContent{Text(texttestposition,modifierModifier。size(200。dp)。padding(20。dp),textAlignTextAlign。Center)}}overridefungetItemCount():Int{return200}}classMyComposeViewHolder(valcomposeView:ComposeView):RecyclerView。ViewHolder(composeView){}
这样一来,我们的compose就被移到了RecyclerView中,当然,每一列其实就是一个文本。嗯!普普通通,好像也没啥特别的对吧,假如这个时候你打开了profiler,当我们向下滑动的时候,会发现内存会慢慢的往上浮动
滑动嘛!有点内存很正常,毕竟谁不生成对象呢,但是这跟我们平常用RecyclerView的时候有点差异,因为RecyclerView滑动的涨幅可没有这个大,那究竟是什么原因导致的呢?探究Compose
有过对Compose了解的同学可能会知道,Compose的界面构成会有一个重组的过程,当然!本文就不展开聊重组了,因为这类文章有挺多的(填个坑,如果有机会就填),我们聊点特别的,那么什么时候停止重组呢?或者说什么时候这个Compose被dispose掉,即不再参与重组!Dispose策略
其实我们的ComposeView,以1。1。1版本为例,其实创建的时候,也创建了取消重组策略,即Suppress(LeakingThis)privatevardisposeViewCompositionStrategy:(()Unit)?ViewCompositionStrategy。DisposeOnDetachedFromWindow。installFor(this)
这个策略是什么呢?我们点进去看源码objectDisposeOnDetachedFromWindow:ViewCompositionStrategy{overridefuninstallFor(view:AbstractComposeView):()Unit{vallistenerobject:View。OnAttachStateChangeListener{overridefunonViewAttachedToWindow(v:View){}overridefunonViewDetachedFromWindow(v:View?){view。disposeComposition()}}view。addOnAttachStateChangeListener(listener)return{view。removeOnAttachStateChangeListener(listener)}}}
看起来是不是很简单呢,其实就加了一个监听,在onViewDetachedFromWindow的时候调用的view。disposeComposition(),声明当前的ComposeView不参与接下来的重组过程了,我们再继续看fundisposeComposition(){composition?。dispose()compositionnullrequestLayout()}
再看dispose方法overridefundispose(){synchronized(lock){if(!disposed){disposedtruecomposable{}valnonEmptySlotTableslotTable。groupsSize0if(nonEmptySlotTableabandonSet。isNotEmpty()){valmanagerRememberEventDispatcher(abandonSet)if(nonEmptySlotTable){slotTable。write{writerwriter。removeCurrentGroup(manager)}applier。clear()manager。dispatchRememberObservers()}manager。dispatchAbandons()}composer。dispose()}}parent。unregisterComposition(this)}
那么怎么样才算是不参与接下里的重组呢,其实就是这里slotTable。write{writerwriter。removeCurrentGroup(manager)}。。。composer。dispose()
而removeCurrentGroup其实就是把当前的group移除了for(slotingroupSlots()){when(slot){。。。。isRecomposeScopeImpl{valcompositionslot。compositionif(composition!null){composition。pendingInvalidScopestrueslot。compositionnull}}}}
这里又多了一个概念,slottable,我们可以这么理解,这里面就是Compose的快照系统,其实就相当于对应着某个时刻view的状态!之所以Compose是声明式的,就是通过slottable里的slot去判断,如果最新的slot跟前一个slot不一致,就回调给监听者,实现更新!这里又是一个大话题了,我们点到为止。
跟RecyclerView有冲突吗
我们看到,默认的策略是当view被移出当前的window就不参与重组了,嗯!这个在99的场景都是有效的策略,因为你都看不到了,还重组干嘛对吧!但是这跟我们的RecyclerView有什么冲突吗?想想看!诶,RecyclerView最重要的是啥,Recycle呀,就是因为会重复利用holder,间接重复利用了view才显得高效不是嘛!那么问题就来了
如图,我们item5其实完全可以利用item1进行显示的对不对,差别就只是Text组件的文本不一致罢了,但是我们从上文的分析来看,这个ComposeView对应的composition被回收了,即不参与重组了,换句话来说,我们Adapter在onBindViewHolder的时候,岂不是用了一个没有compositon的ComposeView(即不能参加重组的ComposeView)?这样怎么行呢?
我们来猜一下,那么这样的话,RecyclerView岂不是都要生成新的ComposeView(即每次都调用onCreateViewHolder)才能保证正确?emmm,很有道理,但是却不是的!如果我们把代码跑起来看的话,复用的时候依旧是会调用onBindViewHolder,这就是Compose的秘密了,那么这个秘密在哪呢overridefunonBindViewHolder(holder:MyComposeViewHolder,position:Int){holder。composeView。setContent{Text(texttestposition,modifierModifier。size(200。dp)。padding(20。dp),textAlignTextAlign。Center)}}
其实就是在ComposeView的setContent方法中,funsetContent(content:Composable()Unit){shouldCreateCompositionOnAttachedToWindowtruethis。content。valuecontentif(isAttachedToWindow){createComposition()}}funcreateComposition(){check(parentContext!nullisAttachedToWindow){createCompositionrequireseitheraparentreferenceortheViewtobeattachedtoawindow。AttachtheVieworcallsetParentCompositionReference。}ensureCompositionCreated()}
最终调用的是privatefunensureCompositionCreated(){if(compositionnull){try{creatingCompositiontruecompositionsetContent(resolveParentCompositionContext()){Content()}}finally{creatingCompositionfalse}}}
看到了吗!如果composition为null,就会重新创建一个!这样ComposeView就完全嫁接到RecyclerView中而不出现问题了!其他Dispose策略
我们看到,虽然在ComposeView在RecyclerView中能正常运行,但是还存在缺陷对不对,因为每次复用都要重新创建一个composition对象是不是!归根到底就是,我们默认的dispose策略不太适合这种拥有复用逻辑或者自己生命周期的组件使用,那么有其他策略适合RecyclerView吗?别急,其实是有的,比如DisposeOnViewTreeLifecycleDestroyedobjectDisposeOnViewTreeLifecycleDestroyed:ViewCompositionStrategy{overridefuninstallFor(view:AbstractComposeView):()Unit{if(view。isAttachedToWindow){vallcocheckNotNull(ViewTreeLifecycleOwner。get(view)){ViewtreeforviewhasnoViewTreeLifecycleOwner}returninstallForLifecycle(view,lco。lifecycle)}else{Wechangethisreferenceafterwesuccessfullyattachvardisposer:()Unitvallistenerobject:View。OnAttachStateChangeListener{overridefunonViewAttachedToWindow(v:View?){vallcocheckNotNull(ViewTreeLifecycleOwner。get(view)){ViewtreeforviewhasnoViewTreeLifecycleOwner}disposerinstallForLifecycle(view,lco。lifecycle)Ensurethisrunsonlyonceview。removeOnAttachStateChangeListener(this)}overridefunonViewDetachedFromWindow(v:View?){}}view。addOnAttachStateChangeListener(listener)disposer{view。removeOnAttachStateChangeListener(listener)}return{disposer()}}}}
然后我们在ViewHolder的init方法中对composeview设置一下就可以了classMyComposeViewHolder(valcomposeView:ComposeView):RecyclerView。ViewHolder(composeView){init{composeView。setViewCompositionStrategy(ViewCompositionStrategy。DisposeOnViewTreeLifecycleDestroyed)}}
为什么DisposeOnViewTreeLifecycleDestroyed更加适合呢?我们可以看到在onViewAttachedToWindow中调用了installForLifecycle(view,lco。lifecycle)方法,然后就removeOnAttachStateChangeListener,保证了该ComposeView创建的时候只会被调用一次,那么removeOnAttachStateChangeListener又做了什么呢?valobserverLifecycleEventObserver{,eventif(eventLifecycle。Event。ONDESTROY){view。disposeComposition()}}lifecycle。addObserver(observer)return{lifecycle。removeObserver(observer)}
可以看到,是在对应的生命周期事件为ONDESTROY(Lifecycle。Event跟activity生命周期不是一一对应的,要注意)的时候,才调用view。disposeComposition(),本例子的lifecycleOwner就是CustomActivity啦,这样就保证了只有当前被lifecycleOwner处于特定状态的时候,才会销毁,这样是不是就提高了compose的性能了!扩展
我们留意到了Compose其实存在这样的小问题,那么如果我们用了其他的组件类似RecyclerView这种的怎么办,又或者我们的开发没有读过这篇文章怎么办!(ps:看到这里的同学还不点赞点赞),没关系,官方也注意到了,并且在1。3。0alpha02以上版本添加了更换了默认策略,我们来看一下valDefault:ViewCompositionStrategyget()DisposeOnDetachedFromWindowOrReleasedFromPoolobjectDisposeOnDetachedFromWindowOrReleasedFromPool:ViewCompositionStrategy{overridefuninstallFor(view:AbstractComposeView):()Unit{vallistenerobject:View。OnAttachStateChangeListener{overridefunonViewAttachedToWindow(v:View){}overridefunonViewDetachedFromWindow(v:View){注意这里if(!view。isWithinPoolingContainer){view。disposeComposition()}}}view。addOnAttachStateChangeListener(listener)valpoolingContainerListenerPoolingContainerListener{view。disposeComposition()}view。addPoolingContainerListener(poolingContainerListener)return{view。removeOnAttachStateChangeListener(listener)view。removePoolingContainerListener(poolingContainerListener)}}}
DisposeOnDetachedFromWindow从变成了DisposeOnDetachedFromWindowOrReleasedFromPool,其实主要变化点就是一个view。isWithinPoolingContainerfalse,才会进行dispose,isWithinPoolingContainer定义如下:
也就是说,如果我们view的祖先存在isPoolingContainertrue的时候,就不会进行dispose啦!所以说,如果我们的自定义view是这种情况,就一定要把isPoolingContainer变成true才不会有隐藏的性能开销噢!当然,RecyclerView也要同步到1。3。0alpha02以上才会有这个属性改写!现在稳定版本还是会存在本文的隐藏性能开销,请注意噢!不过相信看完这篇文章,性能优化啥的,不存在了对不对!结语
Compose是个大话题,希望开发者都能够用上并深入下去,因为声明式ui会越来越流行,Compose相对于传统view体系也有大幅度的性能提升与架构提升!最后记得点赞关注呀!往期也很精彩!
作者:Pika
链接:https:juejin。cnpost7117176526893744142