일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- AWS
- boj
- livedata
- Gradle
- ProGuard
- 백준
- TEST
- 프로그래머스
- relay
- 쿠링
- 코드포스
- GitHub
- MiTweet
- 암호학
- Python
- Coroutine
- Codeforces
- architecture
- androidStudio
- android
- activity
- Hilt
- 코루틴
- MyVoca
- Kotlin
- textfield
- Rxjava
- Coroutines
- Compose
- pandas
- Today
- Total
이동식 저장소
Compose Strong Skip 본문
기존에는 composable의 모든 매개변수가 ``@Stable``이어야만 skippable이었다. 그러나 이제는 unstable한 매개변수를 갖는 composable도 skippable이 될 수 있다.
Compose의 성능을 대폭 개선시킬 strong skip에 대해 알아보자.
Strong skip 규칙
이제 모든 restartable composable은 skippable이 될 수 있다. 구체적으로는, 각 매개변수를 다음과 같이 비교한다.
- Unstable 매개변수는 object equality 연산자인 ``===``로 비교한 결과가 ``true``일 때 (새로 추가된 규칙)
- Stable 매개변수는 ``Object.equals()``로 비교한 결과가 ``true``일 때 (기존과 같음)
모든 매개변수의 비교 결과가 ``true``일 때 recomposition을 skip할 수 있다.
다음 예시를 살펴보자.
@Composable
fun ArticleList(
articles: List<Article>, // List = Unstable, Article = Stable
modifier: Modifier = Modifier // Stable
) {
// …
}
@Composable
fun CollectionScreen(viewModel: CollectionViewModel = viewModel()) {
var favorite by remember { mutableStateOf(false) }
Column {
FavoriteButton(isFavorite = favorite, onToggle = { favorite = !favorite })
ArticleList(viewModel.articles)
}
}
``favorite`` 값이 바뀔 때, 기존 규칙대로라면 ``ArticleList``도 무조건 recompose된다. 그러나 strong skip 모드에서는 ``viewModel.articles``에 새로운 객체가 주어지지 않는 한 ``ArticleList``는 recompose되지 않는다.
Lambda 자동 memoization
현재 default 동작은, 내부에서 stable 값만을 참조하는 람다만 자동으로 remember된다. 추가로 ``@Composable`` 람다도 항상 remember된다.
Strong skip 모드에서는, unstable 값을 참조하는 람다식도 memoization될 수 있다(remember가 아님에 주의). 즉, 모든 lambda 매개변수가 자동으로 remember된다는 것. 따라서 strong skip이 활성화됐을 때, 아래 코드는
@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
val lambda = {
use(unstableObject)
use(stableObject)
}
}
내부적으로는 다음 코드와 비슷하게 컴파일된다.
@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
val lambda = remember(unstableObject, stableObject) {
{
use(unstableObject)
use(stableObject)
}
}
}
``remember`` 함수의 ``key`` 매개변수는 composable에서 unstable/stable 객체를 비교할 때와 동일한 방법으로 비교된다. 주의할 점은, 실제 ``remember`` 함수에서는 모든 매개변수를 ``Object.equals()``로 비교한다는 점.
Deep dive
Compose에서 모든 람다는 stable하다. 다음 코드를 보자.
@Composable
fun NumberComposable(
current: Long,
onValueChanged: (Long) -> Unit
) { }
``Long``은 stable하고, ``(Long) -> Unit`` 람다 역시 stable하므로 ``NumberComposable``은 skippable이다. 실제로 컴파일러를 돌려보면 다음과 같이 skippable하다는 결과가 나온다.
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun NumberComposable(
stable current: Long
stable onValueChanged: Function1<Long, Unit>
)
이제 ``NumberComposable``을 다음과 같이 사용해 보자.
@Composable
fun MyScreen(viewModel: MyViewModel) {
val number by viewModel.number.collectAsState()
var text by remember { mutableStateOf("") }
NumberComposable(
current = number,
onValueChange = { viewModel.numberChanged(it) }
)
TextField(text, onValueChanged = { text = it })
}
사용자가 ``TextField``에 뭔가를 입력할 때, ``NumberComposable``은 skip된다고 예상할 수 있다. 그러나 Layout Inspector로 보면, 실제로는 skip되지 않는다!
그 이유는, 컴파일러 보고서가 잘못되었기 때문이 아니며, Layout Inspector가 잘못되었기 때문도 아니다. 바로 ``MyViewModel``이 unstable하기 때문이다.
참고로, 모든 ``ViewModel``이 unstable하지는 않으며, 구현에 따라 stable할 수도 있다. 그러나 ``MyViewModel``은 unstable하다고 가정하자.
람다는 Kotlin 컴파일러에서 단순히 객체로 취급된다. ``MyScreen``이 recompose될 때, ``NumberComposable``의 ``onValueChange`` 람다도 새로운 객체로 재할당된다. 그런데 람다는 reference equality를 사용하기 때문에, 주소값이 다른 새 객체가 주어졌으므로 ``NumberComposable``을 skip할 수 없는 것.
즉, lambda의 내용물이 동일함에도 불구하고 새로운 람다 객체가 할당되었기 때문이다. 그렇다면, 왜 새로운 람다 객체가 할당되었는가?
Google은 stable 값만을 capture하는 람다는 자동으로 remember되도록 구현하였다. 만약 다음과 같이 람다 안에서 stable한 ``stableViewModel`` 인스턴스를 참조했다면,
@Composable
fun NumberComposable(
current = number,
onValueChange = { stableViewModel.numberChanged(it) }
) { }
Compose 컴파일러에 의해 다음과 같이 rewrite된다.
@Composable
fun NumberComposable(
current = number,
onValueChange = remember(stableViewModel) { { stableViewModel.numberChanged(it) } }
) { }
람다가 remember되므로 recomposition이 발생해도 기존 객체가 그대로 사용된다. 따라서 ``NumberComposable`` 역시 정상적으로 skip될 것이다.
변화
그러나, Strong skip을 통해 Google은 모든 lambda가 자동으로 memoization되게 하였다. Unstable 값을 하나라도 capture하는 람다는 ``Object.equals()`` 기반 메모이제이션을, stable 값만을 capture하는 람다는 기존 remember rewrite 방식으로 기억된다.
메모리 사용량은 조금 늘겠지만, 람다를 포함하는 composable이 더 많이 skip되므로 런타임 성능은 개선될 것이다.
Strong skipping 켜기 (experimental?)
컴파일러의 ``experimentalStrongSkipping`` 값을 ``true``로 지정하면 된다. Root-level ``build.gradle``에 다음 코드를 추가하면 모든 모듈에서 strong skip을 켤 수 있다.
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>() {
compilerOptions.freeCompilerArgs.addAll(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true",
)
}
이거 진짜 안전한가?
기존 모드에서 collection 객체가 unstable인 이유는 mutable collection 때문이었다. 예컨대 ``List``가 ``MutableList``일 수도 있기 때문.
예를 들어, 기존 모드에서 아래의 ``MyList``는 unskippable이었다.
@Composable
fun MyToggle(enabled: Boolean) {}
@Composable
fun MyList(list: List<String>) {}
@Composable
fun MyScreen() {
var list by remember { mutableStateOf(mutableListOf("Foo")) }
var toggle by remember { mutableStateOf(false) }
MyToggle(toggle)
MyList(list)
Button(
onClick = {
list.add("Bar")
toggle = !toggle
}
) { Text("Toggle") }
}
그러나 strong skip 모드에서는 ``MyList``가 skippable하다고 간주된다. Google에서는 이것을 accidental side effect라고 언급하고 있지만, 은근히 자주 일어나는 실수라서.. 흠...
결론
Strong skip을 활성화하면 대부분의 경우 성능을 크게 향상시킬 수 있을 것이다. 그러나 ``MutableList`` 같은 일부(하지만 중요한) 케이스에서는 버그가 발생할 수 있으므로, 개발자가 알아서 조심하는 수밖에 없다.
Compose 개발자라면 skippability 정도는 알아두자.
Stable
Compose 컴파일러 1.5.13 버전에서 Strong skip 모드가 stable로 전환됐다. 곧 default 설정으로 바뀔 예정.
참고자료
Strong skip이 아직 테스트 중일 때 작성된 글이다. Strong skip 개발 과정의 비하인드 스토리도 담겨 있으니, 일독을 권한다.
Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
'Primary > Compose' 카테고리의 다른 글
Compose에서 view 사용하기 (0) | 2024.05.28 |
---|---|
Compose 성능을 개선하기 위한 Best Practices (0) | 2024.05.13 |
Modifier로 블러 효과 구현하기 (0) | 2024.05.01 |
Compose TextField를 커스터마이징해 보자 (0) | 2024.04.30 |
Icon vs. Image (0) | 2024.04.07 |