-
Compose - Strong skipping modeAndroid 2024. 4. 26. 20:28
이 글은 아래 포스팅을 기반으로 필요한 내용만 골라 작성하였습니다.
자세한 내용은 아래 포스팅을 참고해 주세요.
Jetpack Compose: Strong Skipping Mode Explained
Strong skipping mode changes the rules for what composables can skip recomposition and should greatly reduce recomposition.
medium.com
‘Strong skipping mode’는 ‘Compose Compiler 1.5.4’ 이상에서 실험적으로 추가된 기능 중 하나이며, Composable이 ReComposition을 건너뛸 수 있는 조건을 변경함으로써 unstable 파라미터를 가진 Composable의 ReComposition을 대폭 감소시키고, unstable capture를 포함하는 람다에 대해서는 자동으로 remember 처리를 적용하여 ReComposition을 크게 줄여줍니다.
Composable Skippability
restartable composable은 stable 파라미터의 존재 여부와 관계없이 건너뛰어질 수 있습니다. 그러나 non-restartable composable은 건너뛸 수 없습니다. ReComposition 동안 Composable의 Skippability 여부를 결정하기 위해서 다음과 같이 비교됩니다.
- unstable 파라미터는 이전 값과 인스턴스 동등성(===)을 통해 비교
- stable 파라미터는 `Object.equals()`를 통해 비교
모든 파라미터가 위 요구 사항을 충족하면, Composable은 ReComposition 중에 건너뛸 수 있게 됩니다.
@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) } }
위 코드를 예시로, Strong skipping이 활성화되어 있지 않다면 `FavoriteButton`이 토글 될 때 `ArticleList`은 unstable 파라미터를 가지고 있어서 ReComposition 됩니다. 그러나 Strong skipping이 활성화되면, `articles`의 인스턴스가 변경되지 않았기 때문에 `ArticleList`은 ReComposition을 건너뛸 수 있습니다.
Lambda memoization
Strong skipping mode는 람다의 memoization 기능을 더 활성화하여, Composable 내에 작성된 모든 람다를 자동으로 메모리에 저장(remember 처리)하도록 합니다. 이는 stable 값을 capture 하는 람다뿐만 아니라, unstable 값을 capture하는 람다에도 적용이 됩니다.
Capture가 없는 람다도 memoization 되지만, 이는 Compose 컴파일러가 아닌 Kotlin 컴파일러에 의해 람다의 정적 인스턴스가 생성되면서 수행됩니다.
// Strong skipping disabled @Composable fun MyComposable(unstableObject: Unstable, stableObject: Stable) { val lambda = { use(unstableObject) use(stableObject) } } // Strong skipping enabled @Composable fun MyComposable(unstableObject: Unstable, stableObject: Stable) { val lambda = remember(unstableObject, stableObject) { { use(unstableObject) use(stableObject) } } }
Key는 Composable 함수와 동일한 비교 규칙을 따릅니다.
unstable key는 '인스턴스 동등성'을 사용하여 비교되고, stable key는 '객체 동등성'을 사용하여 비교됩니다.
이는 일반적인 `remember` 호출과 약간 다른데, 일반적인 경우 모든 key는 '객체 동등성'을 사용하여 비교됩니다.
이 최적화를 수행하면 ReComposition 중에 건너뛰어지는 Composable의 수가 크게 증가하는데, 람다 memoization 없이는 람다 파라미터를 사용하는 모든 Composable이 ReComposition 중에 새로운 람다가 할당될 가능성이 높으므로, 이전 Composition과 동일한 파라미터를 가지지 않게 됩니다.
Unstable lambdas
Compose에 대한 일반적인 오해 중 하나는 unstable 람다가 ReComposition을 유발한다는 것입니다.
그러나, Compose의 모든 람다는 stable 하므로 unstable 람다라는 개념은 다소 잘못된 명칭입니다.
Skippability 문제를 개선하려면 이러한 사항을 고려해야 합니다.
Compose 컴파일러는 Composable의 파라미터 안정성을 검사하여 컴파일 시 Skippability를 결정합니다.
일부 Composable은 제네릭 타입 등의 이유로 런타임까지 안정성을 알 수 없지만, 대부분은 컴파일 시에 Skippability가 결정됩니다.
@Composable fun NumberComposable( current: Long, onValueChanged: (Long) -> Unit ) { ... } // Compiler Rerort restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun NumberComposable( stable current: Long stable onValueChanged: Function1<Long, Unit> ) // Usage @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`의 입력이 변하지 않았으므로 ReComposition을 건너뛸 것으로 기대합니다. 실제로 컴파일러 보고서에서도 이를 건너뛸 수 있다고 나타납니다.
그러나, 실제로는 `TextField`의 값이 변경될 때 `MyScreen`이 ReComposition 되어 좋지 않은 사용자 경험을 줍니다.
이 문제의 원인은 람다가 단순한 객체로 내부적으로 취급된다는 것입니다. `MyScreen`이 ReComposition 될 때, `onValueChanged` 람다는 재할당됩니다. `NumberComposable`의 ReComposition 여부를 평가할 때, Compose 런타임은 전달된 각 인수를 이전 값과 비교합니다. `current` 값은 동일하지만, `onValueChanged`가 재할당되었기 때문에 변경된 것으로 간주되고, 람다는 참조 동등성(객체 주소가 다름)만을 사용하여 동등성을 검사하기 때문에, 입력이 변경되었다고 판단하고 ReComposition을 합니다.즉, 람다가 unstable 한 것이 아니라, 람다 객체의 변경으로 인해 ReComposition이 발생합니다.
따라서, stable capture가 있으면 람다를 자동으로 기억하는 기능이 구현됐습니다.
예를 들어, `NumberComposable`의 람다에서 stable한 `MyViewModel`을 사용했다면, 해당 Composable은 개발자의 예상대로 작동했을 것입니다.@Composable fun NumberComposable( current = number, onValueChange = { stableViewModel.numberChanged(it) } ) { } // Compiler transform code @Composable fun NumberComposable( current = number, onValueChange = remember(stableViewModel) { { stableViewModel.numberChanged(it) } } ) { }
`remember` 호출로 인해 람다가 ReComposition 중에 재할당되지 않으므로, `NumberComposable`의 입력이 동일하게 유지되어 Composable이 스킵될 수 있습니다.
Strong skipping mode는 unstable capture를 포함한 람다까지 확장하여 Composable 내의 모든 람다를 memoization 합니다.
이는 런타임 성능을 향상하는 것을 목표로 하면서 메모리 사용과의 트레이드오프입니다.'Android' 카테고리의 다른 글
MediaSessionService - Foreground 음악 재생 (0) 2024.05.23 Compose와 Hilt 의존성 주입 (0) 2024.05.08 Android Compose - Composable Component API Guideline (1) 2024.02.29 Android Compose - API GuideLine (0) 2024.02.28 Android Standby Bucket (0) 2022.07.01