注册

听说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 {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Greeting("Android")
Hello()
}
}
}

那么setContent到底做了什么事情呢?我们看下源码


public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView

if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// 第一步走到这里
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}

由于是第一次进入,那么一定就走到了else分支,其实就是创建了一个ComposeView,放在了android.R.id.content里面的第一个child中,这里就可以看到,compose并不是完全脱了原有的view体系,而是采用了移花接木的方式,把compose体系迁移了过来!ComposeView就是我们能用Compose的前提啦!所以在原有的view体系中,我们也可以通过ComposeView去“嫁接”到view体系中,我们举个例子


class CustomActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom)
val recyclerView = this.findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.adapter = MyRecyclerViewAdapter()
recyclerView.layoutManager = LinearLayoutManager(this)
}
}


class MyRecyclerViewAdapter:RecyclerView.Adapter<MyComposeViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyComposeViewHolder {
val view = ComposeView(parent.context)
return MyComposeViewHolder(view)
}

override fun onBindViewHolder(holder: MyComposeViewHolder, position: Int) {
holder.composeView.setContent {
Text(text = "test $position", modifier = Modifier.size(200.dp).padding(20.dp), textAlign = TextAlign.Center)
}

}

override fun getItemCount(): Int {
return 200
}
}

class MyComposeViewHolder(val composeView:ComposeView):RecyclerView.ViewHolder(composeView){

}

这样一来,我们的compose就被移到了RecyclerView中,当然,每一列其实就是一个文本。嗯!普普通通,好像也没啥特别的对吧,假如这个时候你打开了profiler,当我们向下滑动的时候,会发现内存会慢慢的往上浮动


image.png
滑动嘛!有点内存很正常,毕竟谁不生成对象呢,但是这跟我们平常用RecyclerView的时候有点差异,因为RecyclerView滑动的涨幅可没有这个大,那究竟是什么原因导致的呢?


探究Compose


有过对Compose了解的同学可能会知道,Compose的界面构成会有一个重组的过程,当然!本文就不展开聊重组了,因为这类文章有挺多的(填个坑,如果有机会就填),我们聊点特别的,那么什么时候停止重组呢?或者说什么时候这个Compose被dispose掉,即不再参与重组!


Dispose策略


其实我们的ComposeView,以1.1.1版本为例,其实创建的时候,也创建了取消重组策略,即


@Suppress("LeakingThis")
private var disposeViewCompositionStrategy: (() -> Unit)? =
ViewCompositionStrategy.DisposeOnDetachedFromWindow.installFor(this)

这个策略是什么呢?我们点进去看源码


object DisposeOnDetachedFromWindow : ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}

override fun onViewDetachedFromWindow(v: View?) {
view.disposeComposition()
}
}
view.addOnAttachStateChangeListener(listener)
return { view.removeOnAttachStateChangeListener(listener) }
}
}

看起来是不是很简单呢,其实就加了一个监听,在onViewDetachedFromWindow的时候调用的view.disposeComposition(),声明当前的ComposeView不参与接下来的重组过程了,我们再继续看


fun disposeComposition() {
composition?.dispose()
composition = null
requestLayout()
}

再看dispose方法


override fun dispose() {
synchronized(lock) {
if (!disposed) {
disposed = true
composable = {}
val nonEmptySlotTable = slotTable.groupsSize > 0
if (nonEmptySlotTable || abandonSet.isNotEmpty()) {
val manager = RememberEventDispatcher(abandonSet)
if (nonEmptySlotTable) {
slotTable.write { writer ->
writer.removeCurrentGroup(manager)
}
applier.clear()
manager.dispatchRememberObservers()
}
manager.dispatchAbandons()
}
composer.dispose()
}
}
parent.unregisterComposition(this)
}

那么怎么样才算是不参与接下里的重组呢,其实就是这里


slotTable.write { writer ->
writer.removeCurrentGroup(manager)
}

...
composer.dispose()

而removeCurrentGroup其实就是把当前的group移除了


for (slot in groupSlots()) {
when (slot) {
....
is RecomposeScopeImpl -> {
val composition = slot.composition
if (composition != null) {
composition.pendingInvalidScopes = true
slot.composition = null
}
}
}
}

这里又多了一个概念,slottable,我们可以这么理解,这里面就是Compose的快照系统,其实就相当于对应着某个时刻view的状态!之所以Compose是声明式的,就是通过slottable里的slot去判断,如果最新的slot跟前一个slot不一致,就回调给监听者,实现更新!这里又是一个大话题了,我们点到为止


image.png


跟RecyclerView有冲突吗


我们看到,默认的策略是当view被移出当前的window就不参与重组了,嗯!这个在99%的场景都是有效的策略,因为你都看不到了,还重组干嘛对吧!但是这跟我们的RecyclerView有什么冲突吗?想想看!诶,RecyclerView最重要的是啥,Recycle呀,就是因为会重复利用holder,间接重复利用了view才显得高效不是嘛!那么问题就来了


image.png
如图,我们item5其实完全可以利用item1进行显示的对不对,差别就只是Text组件的文本不一致罢了,但是我们从上文的分析来看,这个ComposeView对应的composition被回收了,即不参与重组了,换句话来说,我们Adapter在onBindViewHolder的时候,岂不是用了一个没有compositon的ComposeView(即不能参加重组的ComposeView)?这样怎么行呢?我们来猜一下,那么这样的话,RecyclerView岂不是都要生成新的ComposeView(即每次都调用onCreateViewHolder)才能保证正确?emmm,很有道理,但是却不是的!如果我们把代码跑起来看的话,复用的时候依旧是会调用onBindViewHolder,这就是Compose的秘密了,那么这个秘密在哪呢


override fun onBindViewHolder(holder: MyComposeViewHolder, position: Int) {
holder.composeView.setContent {
Text(text = "test $position", modifier = Modifier.size(200.dp).padding(20.dp), textAlign = TextAlign.Center)
}

}

其实就是在ComposeView的setContent方法中,


fun setContent(content: @Composable () -> Unit) {
shouldCreateCompositionOnAttachedToWindow = true
this.content.value = content
if (isAttachedToWindow) {
createComposition()
}
}

fun createComposition() {
check(parentContext != null || isAttachedToWindow) {
"createComposition requires either a parent reference or the View to be attached" +
"to a window. Attach the View or call setParentCompositionReference."
}
ensureCompositionCreated()
}

最终调用的是


private fun ensureCompositionCreated() {
if (composition == null) {
try {
creatingComposition = true
composition = setContent(resolveParentCompositionContext()) {
Content()
}
} finally {
creatingComposition = false
}
}
}

看到了吗!如果composition为null,就会重新创建一个!这样ComposeView就完全嫁接到RecyclerView中而不出现问题了!


其他Dispose策略


我们看到,虽然在ComposeView在RecyclerView中能正常运行,但是还存在缺陷对不对,因为每次复用都要重新创建一个composition对象是不是!归根到底就是,我们默认的dispose策略不太适合这种拥有复用逻辑或者自己生命周期的组件使用,那么有其他策略适合RecyclerView吗?别急,其实是有的,比如DisposeOnViewTreeLifecycleDestroyed


object DisposeOnViewTreeLifecycleDestroyed : ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
if (view.isAttachedToWindow) {
val lco = checkNotNull(ViewTreeLifecycleOwner.get(view)) {
"View tree for $view has no ViewTreeLifecycleOwner"
}
return installForLifecycle(view, lco.lifecycle)
} else {
// We change this reference after we successfully attach
var disposer: () -> Unit
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
val lco = checkNotNull(ViewTreeLifecycleOwner.get(view)) {
"View tree for $view has no ViewTreeLifecycleOwner"
}
disposer = installForLifecycle(view, lco.lifecycle)

// Ensure this runs only once
view.removeOnAttachStateChangeListener(this)
}

override fun onViewDetachedFromWindow(v: View?) {}
}
view.addOnAttachStateChangeListener(listener)
disposer = { view.removeOnAttachStateChangeListener(listener) }
return { disposer() }
}
}
}

然后我们在ViewHolder的init方法中对composeview设置一下就可以了


class MyComposeViewHolder(val composeView:ComposeView):RecyclerView.ViewHolder(composeView){
init {
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
}
}

为什么DisposeOnViewTreeLifecycleDestroyed更加适合呢?我们可以看到在onViewAttachedToWindow中调用了
installForLifecycle(view, lco.lifecycle) 方法,然后就removeOnAttachStateChangeListener,保证了该ComposeView创建的时候只会被调用一次,那么removeOnAttachStateChangeListener又做了什么呢?


val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
view.disposeComposition()
}
}
lifecycle.addObserver(observer)
return { lifecycle.removeObserver(observer) }

可以看到,是在对应的生命周期事件为ON_DESTROY(Lifecycle.Event跟activity生命周期不是一一对应的,要注意)的时候,才调用view.disposeComposition(),本例子的lifecycleOwner就是CustomActivity啦,这样就保证了只有当前被lifecycleOwner处于特定状态的时候,才会销毁,这样是不是就提高了compose的性能了!


扩展


我们留意到了Compose其实存在这样的小问题,那么如果我们用了其他的组件类似RecyclerView这种的怎么办,又或者我们的开发没有读过这篇文章怎么办!(ps:看到这里的同学还不点赞点赞),没关系,官方也注意到了,并且在1.3.0-alpha02以上版本添加了更换了默认策略,我们来看一下


val Default: ViewCompositionStrategy
get() = DisposeOnDetachedFromWindowOrReleasedFromPool

object DisposeOnDetachedFromWindowOrReleasedFromPool : ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}

override fun onViewDetachedFromWindow(v: View) {
// 注意这里
if (!view.isWithinPoolingContainer) {
view.disposeComposition()
}
}
}
view.addOnAttachStateChangeListener(listener)

val poolingContainerListener = PoolingContainerListener { view.disposeComposition() }
view.addPoolingContainerListener(poolingContainerListener)

return {
view.removeOnAttachStateChangeListener(listener)
view.removePoolingContainerListener(poolingContainerListener)
}
}
}

DisposeOnDetachedFromWindow从变成了DisposeOnDetachedFromWindowOrReleasedFromPool,其实主要变化点就是一个view.isWithinPoolingContainer = false,才会进行dispose,isWithinPoolingContainer定义如下


image.png


也就是说,如果我们view的祖先存在isPoolingContainer = true的时候,就不会进行dispose啦!所以说,如果我们的自定义view是这种情况,就一定要把isPoolingContainer变成true才不会有隐藏的性能开销噢!当然,RecyclerView也要同步到1.3.0-alpha02以上才会有这个属性改写!现在稳定版本还是会存在本文的隐藏性能开销,请注意噢!不过相信看完这篇文章,性能优化啥的,不存在了对不对!


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

0 个评论

要回复文章请先登录注册