Escuché que la combinación de Compose y RecyclerView será incómoda.

Antecedentes y pequeña charla del autor

Compose también se ha vuelto popular recientemente. Como marco de interfaz de usuario promovido por Google, ¡debemos usarlo para mejorar! En la última evaluación, los componentes de la lista LazyXXX como LazyRow se han acercado lentamente al rendimiento de RecyclerView. ¡Pero todavía hay muchos estudiantes preocupados por eso! No importa, incluso si usamos el sistema de desarrollo de vista original, podemos migrar rápidamente a componer. ¡Esta arma es ComposeView , luego integramos Compose sobre la base de RecyclerView! De esta manera, tenemos el rendimiento de RecyclerView y los beneficios de Compose, ¿verdad? Creo que muchas personas tienen la misma idea que yo, ¡pero la combinación de los dos tiene una sobrecarga de rendimiento oculta! (La versión de redacción utilizada esta vez es 1.1.1)

Acceda a Redactar en el sistema de vista original

En proyectos de composición pura, usaremos setContent en lugar de setContentView del sistema de vista original, como

setContent {
    ComposeTestTheme {
        // A surface container using the 'background' color from the theme
        Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
            Greeting("Android")
            Hello()
        }
    }
}

Entonces, ¿qué hace exactamente setContent? Veamos el código fuente.

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)
    }
}

Como es la primera vez que ingresa, debe ir a la rama else. De hecho, se crea un ComposeView y se coloca en el primer elemento secundario en android.R.id.content. Como puede ver aquí, compose no es completamente Se eliminó el sistema de visualización original, pero se migró el sistema de composición mediante el método de mover flores y árboles. ComposeView es la premisa de que podemos usar Compose. Entonces, en el sistema de vista original, también podemos "injertar" en el sistema de vista a través de ComposeView. Tomemos un ejemplo.

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){
    
}

De esta manera, nuestra composición se mueve a RecyclerView, por supuesto, cada columna es en realidad un texto. ¡Um! Ordinario, no parece ser nada especial, cierto, si abres el perfilador en este momento, cuando deslizamos hacia abajo, encontraremos que la memoria flotará lentamente hacia arriba.

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以上才会有这个属性改写!现在稳定版本还是会存在本文的隐藏性能开销,请注意噢!不过相信看完这篇文章,性能优化啥的,不存在了对不对!

结语

Compose是个大话题,希望开发者都能够用上并深入下去,因为声明式ui会越来越流行,Compose相对于传统view体系也有大幅度的性能提升与架构提升!最后记得点赞关注呀!往期也很精彩!

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

Supongo que te gusta

Origin juejin.im/post/7117176526893744142
Recomendado
Clasificación