Jetpack Compose:大量输入后出现极长帧。

huangapple go评论96阅读模式
英文:

Jetpack Compose: extremely long frames after lots of input

问题

Link to my code: https://github.com/tylerwilbanks/word-game-android

在启动后大约10秒钟左右,如果您快速点击键盘按钮,应用程序的性能会突然下降,每帧处理需要5-10秒钟。

问题似乎不是过度重新组合,一个组合突然需要很长时间,而其他组合在之前都很快。

一旦慢帧开始,应用程序将保持在这种状态,直到重新启动。

非常感谢任何见解。

MainActivity:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GuessWordValidator.initValidWords(this)
        viewModel.setupGame()
        setContent {
            WordGameTheme {
                DailyWordScreen(
                    state = viewModel.state.collectAsStateWithLifecycle(),
                    onEvent = viewModel::onEvent
                )
            }
        }
    }
}

ViewModel:

class DailyWordViewModel(application: Application) : AndroidViewModel(application) {

    private val _state = MutableStateFlow(DailyWordState())
    private val context = getApplication<Application>().applicationContext

    val state
        get() = _state.stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5000L),
            DailyWordState()
        )
...
}

ViewModel.onEvent():

fun onEvent(event: DailyWordEvent) {
        when (event) {
            is DailyWordEvent.OnCharacterPress -> {
                val currentGuess = _state.value.currentGuess
                currentGuess?.getLetterForInput?.let { guessLetter ->
                    guessLetter.updateCharacter(event.character)
                    _state.update {
                        it.copy(
                            currentGuess = currentGuess,
                            currentWord = currentGuess.displayWord
                        )
                    }
                }
            }

            DailyWordEvent.OnDeletePress -> {
                val currentGuess = _state.value.currentGuess
                currentGuess?.getLetterToErase?.let { guessLetter ->
                    guessLetter.updateCharacter(' ')
                    _state.update {
                        it.copy(
                            currentGuess = currentGuess,
                            currentWord = currentGuess.displayWord
                        )
                    }
                }
            }
...

分析器显示了非常长的帧,但没有任何指示原因。内存使用保持稳定。

我尝试了直接在viewModel中更新状态变量而不使用协程,但没有效果。我还尝试了明确使用viewModel.launch(Dispatchers.IO) {}来确保没有过多的分配作业导致主线程阻塞。

我唯一能想到可能引起问题的是在可组合函数中使用的.collectAsStateWithLifecycle被所有输入阻塞并卡住了。

我真的不知道如何以其他方式将状态传递到可组合函数中,如果收集StateFlow会导致输入问题的话。

在我的logcat中得到了这个消息:跳过了741帧!应用程序可能在其主线程上做了太多的工作。

编辑1:组合绝对不会被状态收集阻塞。我注意到我甚至不必再狂按键盘,它将在应用程序启动后等待约10秒钟后开始跳帧,然后以正常速度输入几个字母就会跳帧。

编辑2:我在分析器中注意到,每次我输入一个字母时,应用程序的内存消耗都会增加约1.2 MB,永远不会减少。

编辑3:感谢Tenfour04,这个问题已经解决了!导致这个问题的原因是我向可组合函数传递的DailyWordState对象是Mutable的。我将其更改为包含不可变领域对象的列表Immutable。我的DailyWordState仍然被标记为Mutable,因为List<Any>被视为mutable,因为您可以将MutableList转换为List。我通过在我的DailyWordState对象上添加@Immutable注释来解决了这个问题。这现在运行得很好,但现在我的任务是学习如何构建我的数据,而不需要@Immutable注释。

英文:

Link to my code: https://github.com/tylerwilbanks/word-game-android

After around maybe 10 seconds after startup, if you tap keyboard buttons quickly, suddenly the app performance will tank with 5-10 second frame processes.

The issue does not seem to be excessive re-composition, 1 composition will suddenly take a very long time whereas others just before it were very quick.

Once the slow frame begins, the application remains in this state until relaunch.

Any insights would be greatly appreciated.

MainActivity:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GuessWordValidator.initValidWords(this)
        viewModel.setupGame()
        setContent {
            WordGameTheme {
                DailyWordScreen(
                    state = viewModel.state.collectAsStateWithLifecycle(),
                    onEvent = viewModel::onEvent
                )
            }
        }
    }
}

ViewModel:

class DailyWordViewModel(application: Application) : AndroidViewModel(application) {

    private val _state = MutableStateFlow(DailyWordState())
    private val context = getApplication&lt;Application&gt;().applicationContext

    val state
        get() = _state.stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5000L),
            DailyWordState()
        )
...
}

ViewModel.onEvent():

fun onEvent(event: DailyWordEvent) {
        when (event) {
            is DailyWordEvent.OnCharacterPress -&gt; {
                val currentGuess = _state.value.currentGuess
                currentGuess?.getLetterForInput?.let { guessLetter -&gt;
                    guessLetter.updateCharacter(event.character)
                    _state.update {
                        it.copy(
                            currentGuess = currentGuess,
                            currentWord = currentGuess.displayWord
                        )
                    }
                }
            }

            DailyWordEvent.OnDeletePress -&gt; {
                val currentGuess = _state.value.currentGuess
                currentGuess?.getLetterToErase?.let { guessLetter -&gt;
                    guessLetter.updateCharacter(&#39; &#39;)
                    _state.update {
                        it.copy(
                            currentGuess = currentGuess,
                            currentWord = currentGuess.displayWord
                        )
                    }
                }
            }
...

The profiler shows the very long frames but no indicator as to why. Memory usage stays level.

I've tried updating the state variable in the viewModel directly without using coroutines, no avail. I also tried explicitly using viewModel.launch(Dispatchers.IO) {} to make sure I'm not holding up the main thread with too many allocated jobs.

The only thing I can think that is causing the issue is .collectAsStateWithLifecycle in the composable is getting backed up with all the inputs and gets stuck.

I really don't know how else to send state down into the composable if collecting the StateFlow causes issues with input.

Getting this in my logcat: Skipped 741 frames! The application may be doing too much work on its main thread.

Edit 1: There is no way that compose is getting bogged down by collecting state. I've noticed that I don't even have to spam the keyboard anymore, it will begin skipping frames after waiting around 10 seconds after application launch, and then typing a few letters at a normal pace.

Edit 2: I've noticed in the profiler that everytime i type a letter, the app's memory consumption goes up appx. 1.2 mb and never goes back down.

Edit 3: Thanks to Tenfour04, this issue has been solved! The reason for this was that my DailyWordState object I was sending down into my composables was Mutable. I changed this to contain a list Immutable domain objects. My DailyWordState was still flagged as Mutable because List&lt;Any&gt; is considered mutable because you can convert a MutableList to a List. I remedied this by adding @Immutable annotation to my DailyWordState object. This works great now, however now my task is to learn how to structure my data without the need for the @Immutable annotation.

答案1

得分: 1

我看到一个潜在的问题:

val state
    get() = _state.stateIn(
        viewModelScope,
        SharingStarted.WhileSubscribed(5000L),
        DailyWordState()
    )

有两件事情不合理:

  • 在自定义 getter 中使用 stateIn,这意味着每次访问 state 时都会创建一个新的 StateFlow,而每个 StateFlow 都订阅了 _state。这会产生许多重复工作的 StateFlow。除了这些不必要的工作之外,由于 state 属性不稳定,你可能会触发某种无限重组循环。

  • 在 StateFlow 上使用 stateIn_state 已经是一个 StateFlow。这只会创建另一个 StateFlow,复制了源 StateFlow 正在做的一切,没有任何理由,并且需要 SharingStarted 和可能与源 StateFlow 相矛盾的默认值,这可能会导致奇怪的错误。

你应该将其更改为:

val state = _state.asStateFlow()

这将 _state 包装在一个只读版本中,由于没有自定义 getter,它只执行一次。这比 stateIn 更轻量级,因为它只是通过集合传递。Compose 应该还会识别到属性是稳定的,因此不会仅仅因为看到属性被访问而不必要地重新组合。


总结以下在下面的评论中找到的内容,模型类和键盘描述的二维列表也(或者可能主要是)引发了问题,因为它们不稳定或不可变。

英文:

I see a potential culprit here:

val state
    get() = _state.stateIn(
        viewModelScope,
        SharingStarted.WhileSubscribed(5000L),
        DailyWordState()
    )

Two things about this that don't make sense:

  • Using stateIn in a custom getter, so you are creating a new StateFlow every time state is accessed, and each of these is subscribed to _state. So this is proliferating many StateFlows that are doing duplicate work. Aside from all the unnecessary work that this is creating, it's possible you're triggering some kind of infinite recomposition loop because the state property is not stable but you're trying to collect it in your composable.

  • Using stateIn on a StateFlow. _state is already a StateFlow. This just creates another StateFlow that duplicates everything the source StateFlow is doing for no reason, and requiring SharingStarted and default values that might contradict what the source has, which could lead to weird bugs.

You should change it to:

val state = _state.asStateFlow()

This wraps the _state in a read-only version, and since there's no custom getter, it does it only once. This is lighter weight than stateIn since it just passes through collection. Compose should recognize also that the property is stable, so it won't recompose unnecessarily just from seeing the property being accessed.


To summarize what was found in the comments below, the model classes and the keyboard description 2D List were also (or maybe primarily) causing the issue because they were not stable or immutable.

huangapple
  • 本文由 发表于 2023年8月10日 10:28:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/76872296.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定