ComposeとRecyclerViewの組み合わせは不快だと聞きましたが?

背景と作者のスモールトーク

作曲も最近人気が出てきており、グーグルが推進するUIフレームワークとして、それを使って改善する必要があります!最新の評価では、LazyRowなどのLazyXXXリストコンポーネントがRecyclerViewのパフォーマンスに徐々に近づいています。しかし、それを心配している学生はまだたくさんいます!元のビュー開発システムを使用していても、すばやく移行して作成することができます。この武器はComposeViewであり、RecyclerViewに基づいてComposeを統合します。このようにして、RecyclerViewのパフォーマンスとComposeのメリットがありますよね?多くの人が私と同じ考えを持っていると思いますが、2つの組み合わせはパフォーマンスのオーバーヘッドを隠しています!(今回使用した作曲バージョンは1.1.1です)

元のビューシステムで作成にアクセスします

純粋な作成プロジェクトでは、元のビューシステムのsetContentViewの代わりにsetContentを使用します。

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の最初の子に配置されます。ここでわかるように、composeは完全ではありません。元のビューシステムは削除されましたが、作曲システムは花や木を動かす方法で移行されました!ComposeViewは、Composeを使用できることを前提としています。したがって、元のビューシステムでは、ComposeViewを使用してビューシステムに「移植」することもできます。例を見てみましょう。

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

このようにして、作成はRecyclerViewに移動します。もちろん、各列は実際にはテキストです。えーと!通常、特別なことではないようですが、このときにプロファイラーを開くと、下にスライドすると、メモリがゆっくりと浮き上がることがわかります。

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体系也有大幅度的性能提升与架构提升!最后记得点赞关注呀!往期也很精彩!

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

おすすめ

転載: juejin.im/post/7117176526893744142