英文:
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<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
)
}
}
}
...
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<Any>
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 timestate
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 thestate
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论