当前位置:网站首页>Compose与RecyclerView结合效果会是怎样的?
Compose与RecyclerView结合效果会是怎样的?
2022-07-23 00:45:00 【码中之牛】
背景&笔者碎碎谈
最近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,当我们向下滑动的时候,会发现内存会慢慢的往上浮动

滑动嘛!有点内存很正常,毕竟谁不生成对象呢,但是这跟我们平常用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不一致,就回调给监听者,实现更新!这里又是一个大话题了,我们点到为止

跟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的秘密了,那么这个秘密在哪呢
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定义如下

也就是说,如果我们view的祖先存在isPoolingContainer = true的时候,就不会进行dispose啦!所以说,如果我们的自定义view是这种情况,就一定要把isPoolingContainer变成true才不会有隐藏的性能开销噢!当然,RecyclerView也要同步到1.3.0-alpha02以上才会有这个属性改写!现在稳定版本还是会存在本文的隐藏性能开销,请注意噢!不过相信看完这篇文章,性能优化啥的,不存在了对不对!
结语
Compose是个大话题,希望开发者都能够用上并深入下去,因为声明式ui会越来越流行,Compose相对于传统view体系也有大幅度的性能提升与架构提升!
最后分享一份个人整理的 Compose 学习笔记,里面记包含了(基本控件、Composable的理解和使用场景、MutableState的使用和源码浅析、Compose的重组和Remember、Compose的单一信息源和单向数据流原则、状态机制和重组优化、derivedStateOf和remember的使用、CompositionLocal的应用场景、Compose动画之AnimateSpec、Compose动画之DecayAnimation、Compose动画之中止和入场效果等),有需要参考的小伙伴可以点击这里查看获取方式 传送门直达 !!!

边栏推荐
- Wireshark packet capture tool basic use
- Practical exercise | a simple method for MySQL processlist table and Navicat monitor to identify slow queries
- 阿里二面:Mysql的索引为什么用B+树而不是跳表?
- College students downloaded 2578 documents abnormally, and the IP of the University of Social Sciences of China was banned by a database
- 解读随着教育改革的深入steam教育
- What is the experience of writing concurrent tool classes (semaphore, cyclicbarrier, countdownlatch) by yourself in line 30?
- 为什么使用Well-Architected Framework?
- Family fraud is prevalent, and Sogou number builds a security firewall
- Airserver third party projection software v7.3.0 Chinese Version (airplay terminal utility)
- Swin-Transformer-Object-Detection项目安装教程
猜你喜欢

Mathematical modeling interpolation fitting

The most detailed explanation of the output of three numbers from large to small

Camera IQ: 76% of consumers have experienced AR, and 49% are willing to share ar advertisements

IDM下载器免费高质量的Win下载工具无使用限制

SIP账号的作用-告诉你什么是SIP线路

LiveQing直播点播流媒体OBS推流直播如何获得接口校验token视频校验streamToken及配置token有效期

Jmeter---Jmeter安装教程

IDM downloader free high-quality win download tool without restrictions

322. 零钱兑换

差分数组操作的一些性质
随机推荐
There was an accident caused by MySQL misoperation, and "high availability" couldn't stand it
Camera IQ: 76% of consumers have experienced AR, and 49% are willing to share ar advertisements
Construction of mGRE network
Anti attack based on conjugate gradient method
Kali 2022.2 installation
Wireshark packet capture tool basic use
DOM系列之禁止选中文字和禁止右键菜单
How to prevent all kinds of affiliated fraud?
博途PLC信号处理系列之限幅消抖滤波
Flutter linear layout, filling
DALSA智能相机BOA Spot与西门子S7-1200 Profinet通讯
华为应用已经调用了checkAppUpdate接口,为什么应用内不提示版本更新
Idea export jar package to JMeter
SQL Server database design -- select statement 2
【微信小程序】开发入门篇(二)
No requirement document, reject development?
模板学堂丨JumpServer安全运维审计大屏
What is the experience of writing concurrent tool classes (semaphore, cyclicbarrier, countdownlatch) by yourself in line 30?
Detailed explanation of common ADB operation commands of "suggestions collection"
Internet download manager is simply a killer of downloaders