如何在 Jetpack Compose 中调试重组

原作者:twitter.com/vinaygaba
译:FunnySaltyFish


自从 Jetpack Compose 的第一个稳定版本上线以来,已经过去了好几个月 (译注:本文写于2022年4月)。多家公司已经使用了 Compose 来参与构建他们的 Android 应用程序,成千上万的 Android 工程师每天都在使用 Jetpack Compose 。

虽然已经有大量的文档可以帮助开发人员接受这种新的编程模式,但仍有这么个概念让许多人摸不着头脑。它就是Recomposition,Compose 赖以运作的基础。

重组是在输入更改时再次调用可组合函数的过程。当函数的输入发生更改时,它便会发生。当 Compose 基于新输入进行重组时,它仅调用可能已更改的函数或 lambda,并跳过其余部分。通过跳过所有未更改参数的函数或 lambda,Compose 可以有效地进行重组。

如果您不熟悉此主题,我将在本文中详细介绍 Recomposition。对于大多数用例,除非传入的参数变了,否则我们不希望重新调用可组合函数(此处从简表示)。Compose 编译器在这方面也非常聪明,当它有足够的可用信息时(例如,所有原始值类型的参数在设计上都是Stable的),它会尽最大努力来做些对使用者无感的优化;当信息没那么多时,Compose 允许您通过使用 @Stable@Immutable 注解提供元数据,以帮助 Compose 编译器正确做出决定。

从理论上讲,这一切都是有道理的,但是,如果开发人员有办法了解他们的可组合函数是如何重组的,那将大有裨益。这类功能目前呼声很高,不过要使Android Studio 快捷地为您提供此信息,还有一吨的工作要做。如果你像我一样迫不及待,你可能也想知道在能正式上手工具前,要想在 Jetpack Compose 中调试重组,咱可以做些什么。毕竟嘛,重组在性能上起着重要作用——不必要的重组可能会导致 UI 卡顿。

打日志

调试重组的最简单方法是使用良好的 log 语句来查看正在调用哪些可组合函数以及调用它们的频率。这感觉上很直白,但注意这个坑 : 我们希望仅在发生重组时才触发这些日志语句。这听起来像是 SideEffect 的用武之地。SideEffect 是一个可组合的函数,每当成功的 Composition/ Recomposition 后便会被重新调用。Sean McQuillan 编写了如下代码片段,您可以使用它来调试您的重组。这只是一个框架,您可以根据需要进行调整。

class Ref(var value: Int)

// 注意,此处的 inline 会使下列函数实际上直接内联到调用处
// 以确保 logging 仅在原始调用位置被调用
@Composable
inline fun LogCompositions(tag: String, msg: String) {
    if (BuildConfig.DEBUG) {
        val ref = remember { Ref(0) }
        SideEffect { ref.value++ }
        Log.d(tag, "Compositions: $msg ${ref.value}")
    }
}

实战如下:

@Composable
fun MyComponent() {
    val counter by remember { mutableStateOf(0) }

    LogCompositions(TAG, "MyComposable function")

    CustomText(
        text = "Counter: $counter",
        modifier = Modifier
            .clickable {
                counter++
            },
    )
}

@Composable
fun CustomText(
    text: String,
    modifier: Modifier = Modifier,
) {
    LogCompositions(TAG, "CustomText function")

    Text(
        text = text,
        modifier = modifier.padding(32.dp),
        style = TextStyle(
            fontSize = 20.sp,
            textDecoration = TextDecoration.Underline,
            fontFamily = FontFamily.Monospace
        )
    )
}

在运行此示例时,我们注意到每次计数器的值更改时,两者都会重组。MyComponent``CustomText

在运行时对重组可视化

Google Play 团队是Google首批利用 Jetpack Compose 的内部团队之一。他们与 Compose 团队密切合作,甚至编写了一份case study,描述了他们迁移到 Compose 的经验。该帖子的宝藏之一是他们开发的可视化重组Modifier。您可以在此处找到修饰符的代码。为了方便,我在下面添加了该代码段;但不要夸我啊,它是由Google Play团队开发的。

/*
 *  Copyright 2022 Google Inc.
 *
 * Licensed under the Apache License,Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,software distributed under the
 * License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND,either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.example.android.compose.recomposehighlighter

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import kotlin.math.min
import kotlinx.coroutines.delay

/**
 * A [Modifier] that draws a border around elements that are recomposing. The border increases in
 * size and interpolates from red to green as more recompositions occur before a timeout.
 */
@Stable
fun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier)

// Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations
// Modifier.composed will still remember unique data per call site.
private val recomposeModifier =
    Modifier.composed(inspectorInfo = debugInspectorInfo { name = "recomposeHighlighter" }) {
        // The total number of compositions that have occurred. We're not using a State<> here be
        // able to read/write the value without invalidating (which would cause infinite
        // recomposition).
        val totalCompositions = remember { arrayOf(0L) }
        totalCompositions[0]++

        // The value of totalCompositions at the last timeout.
        val totalCompositionsAtLastTimeout = remember { mutableStateOf(0L) }

        // Start the timeout,and reset everytime there's a recomposition. (Using totalCompositions
        // as the key is really just to cause the timer to restart every composition).
        LaunchedEffect(totalCompositions[0]) {
            delay(3000)
            totalCompositionsAtLastTimeout.value = totalCompositions[0]
        }

        Modifier.drawWithCache {
            onDrawWithContent {
                // Draw actual content.
                drawContent()

                // Below is to draw the highlight,if necessary. A lot of the logic is copied from
                // Modifier.border
                val numCompositionsSinceTimeout =
                    totalCompositions[0] - totalCompositionsAtLastTimeout.value

                val hasValidBorderParams = size.minDimension > 0f
                if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) {
                    return@onDrawWithContent
                }

                val (color, strokeWidthPx) =
                    when (numCompositionsSinceTimeout) {
                        // We need at least one composition to draw,so draw the smallest border
                        // color in blue.
                        1L -> Color.Blue to 1f
                        // 2 compositions is _probably_ okay.
                        2L -> Color.Green to 2.dp.toPx()
                        // 3 or more compositions before timeout may indicate an issue. lerp the
                        // color from yellow to red,and continually increase the border size.
                        else -> {
                            lerp(
                                Color.Yellow.copy(alpha = 0.8f),
                                Color.Red.copy(alpha = 0.5f),
                                min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f)
                            ) to numCompositionsSinceTimeout.toInt().dp.toPx()
                        }
                    }

                val halfStroke = strokeWidthPx / 2
                val topLeft = Offset(halfStroke, halfStroke)
                val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)

                val fillArea = (strokeWidthPx * 2) > size.minDimension
                val rectTopLeft = if (fillArea) Offset.Zero else topLeft
                val size = if (fillArea) size else borderSize
                val style = if (fillArea) Fill else Stroke(strokeWidthPx)

                drawRect(
                    brush = SolidColor(color),
                    topLeft = rectTopLeft,
                    size = size,
                    style = style
                )
            }
        }
    }

使用此修饰符实际上是直白明了 —— 只需将 recomposeHighlighter 修饰符加到要跟踪其重组的可组合项的修饰符链上即可。修饰符在其附加到的可组合体周围绘制一个框,并使用颜色和边框宽度来表示可组合中发生的重组量。

边框颜色 重组次数
1
绿 2
黄色到红色 3+

让我们来看看它在实际使用时的样子。我们的示例有一个简单的可组合函数,该函数具有一个按钮,该按钮在单击计数器时递增计数器。我们在两个地方使用recomposeHighlighter 修饰符 -——MyButtonComponent本身和``MyTextComponent`,它是按钮的内容。

@Composable
fun MyButtomComponent(
    modifier: Modifier = Modifier.recomposeHighlighter()
) {
    var counter by remember { mutableStateOf(0) }

    OutlinedButton(
        onClick = { counter++ },
        modifier = modifier,
    ) {
        MyTextComponent(
            text = "Counter: $counter",
            modifier = Modifier.clickable {
                counter++
            },
        )
    }
}

@Composable
fun MyTextComponent(
    text: String,
) {
    Text(
        text = text,
        modifier = modifier
            .padding(32.dp)
            .recomposeHighlighter(),
    )
}

在运行此示例时,我们注意到按钮和按钮内的文本最初都有一个蓝色的边界框。这很合理,因为这是第一次重组,它对应于我们使用recomposeHighlighter()修饰符的两个地方。当我们单击按钮时,我们注意到边界框仅围绕按钮内的文本,而不是按钮本身。这是因为 Compose 在重组方面很聪明,它不需要重组整个按钮 —— 只需重组计数器值更改时依赖的那个 Composable 即可。

recomposeHighlighter实战

使用此修饰符,我们能够可视化可组合函数中如何发生重组。这是一个非常强大的工具,我能想象出基于此拓展的巨大潜力。

Compose编译器指标

前两种调试重组的方法非常有用,并且依赖于观察和可视化。但是,如果我们有一些更确凿的证据来证明Compose编译器如何解释我们的代码,那不是相当nice?这些感觉起来就像魔法一样,毕竟我们经常不知道编译器是否按照我们想要的方式在解释。

事实证明,Compose 编译器确实有一种机制,能给出关于此信息的详细报告。我上个月发现了它,这让我大吃一惊

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具