$30 off During Our Annual Pro Sale. View Details »

[KR] 2024 DroidKnights: Compose 성능 최적화를 위한 Sta...

[KR] 2024 DroidKnights: Compose 성능 최적화를 위한 Stability 마스터하기

[KR] 2024 DroidKnights: Mastering Stability to Optimize Compose Performance.

Jaewoong

June 10, 2024
Tweet

More Decks by Jaewoong

Other Decks in Programming

Transcript

  1. Compose Structure Compose Compiler • Jetpack Compose 내에서 중추적인 역할을

    하는 핵심 구성 요소 • 순수한 Kotlin으로 작성된 Kotlin 컴파일러 플러그인 • KAPT나 KSP와 같은 annotation processor와는 달리, Compose 컴파일러는 FIR (Frontend Intermediate Representation)을 통하여 개발자가 작성한 코드를 정적 분석 및 변형
  2. Compose Structure Compose Compiler • 최근 4월에 JetBrains의 Kotlin repository로

    코드 베이스 이동 • Kotlin 2.0 부터는 Compose 컴파일러와 Kotlin의 버전 통합 [versions] kotlin = "2.0.0" [plugins] org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 🔗 bit.ly/4bCzCVr
  3. Compose Structure Compose Runtime • Compose 모델 및 상태 관리의

    초석 역할 • Gap Buffer라는 데이터 구조에서 파생된 Slot Table을 사용하여 상태 저장 등 메모리 운용 • Composable 함수에 대한 실질적인 생명주기 관리 및 UI에 대한 정보를 담고 있는 메모리 표현
  4. Compose Structure Compose UI • 개발자가 Composable 함수를 통해 레이아웃을

    생성할 수 있도록 여러 UI 라이브러리를 통해 컴포넌트 제공 • 각 컴포넌트는 LayoutNode를 생성하고, 결과적으로 Compose 레이아웃 트리 구성을 용이하게 함 • Compose Runtime에 의하여 소비되는 것이 기본 원칙
  5. 80+ Compose Compiler 120+ Compose UI & Runtime versions have

    been released. Jetpack Compose continues to evolve with steady performance enhancements. Compose Histories
  6. Compose UI Compose Compiler & Runtime • Smart Recomposition •

    Strong Skipping Mode • Stability Configuration File Compose Histories
  7. ❏ Composable 함수는 표준 함수와는 달리 recomposition을 통해 재실행이 가능함

    ❏ Recomposition은 메모리 내 표현을 항상 최신 상태로 유지하기 위해 입력값의 변경이나 관찰 중인 State에 변경이 발생할 때 일어남 ❏ Compose 컴파일러는 상태를 관찰하는 모든 Composable 함수를 찾아서 Compose 런타임에게 정보를 전달함 Understanding Stability Restartable
  8. 1. 관찰 중인 State의 변경 Compose 런타임은 상태 변경을 관찰하고

    recomposition을 트리거하는 State라는 효율적인 메커니즘을 제공 var text by remember { mutableIntStateOf(0) } Text( modifier = Modifier.clickable { text++ }, text = "Clicked $text times" ) Understanding Stability Recomposition
  9. 1. 관찰 중인 State의 변경 Compose 런타임은 상태 변경을 관찰하고

    recomposition을 트리거하는 State라는 효율적인 메커니즘을 제공 2. Composable 함수의 매개변수 변경 Compose 런타임은 equals 함수를 사용하여 stable한 매개변수에 대해 입력값의 변경 사항을 감지함. 만약 equals 함수가 false를 반환하면 런타임은 이를 입력 데이터의 변경으로 해석 @Composable fun UserProfile(name: String, image: String) { .. } Understanding Stability Recomposition
  10. @Composable fun Profile(user: User, posts: List<Post>) { // composable code

    } compile @Composable fun Profile( stable user: User, unstable posts: List<Post>, ) Understanding Stability Stable vs. Unstable ❏ Compose 컴파일러는 Composable 함수에 사용된 매개변수에게 stable 혹은 unstable 타입을 부여 ❏ Composable 함수에 unstable한 매개변수가 하나 이상 포함되어 있으면 recomposition이 항상 발생 ❏ Composable 함수가 모두 stable한 매개변수로 이루어져 있다면 recomposition을 건너뛰고 불필요한 작업 생략
  11. Understanding Stability Stable vs. Unstable Stable로 간주되는 유형 ❏ 원시

    타입 (Primitive types) ❏ (Int) -> String 와 같은 람다식으로 표현되는 함수의 유형 (외부값을 캡처하는 경우는 값이 stable인 경우만) ❏ (data) class의 public 프로퍼티가 모두 불변이거나 stable한 경우 ❏ (data) class에 @Stable 및 @Immutable 어노테이션을 사용하여 명시적으로 stable 하다고 표기된 경우
  12. Understanding Stability Stable vs. Unstable Unstable로 간주되는 유형 ❏ 컴파일

    타임에 구현체를 예측할 수 없는 Any 유형과 같은 추상 클래스와 List, Map 등을 포함한 모든 인터페이스 ❏ (data) class의 public 프로퍼티 중 최소 하나 이상이 가변적이거나 unstable한 경우
  13. Understanding Stability Stable vs. Unstable Stable data class User( val

    id: Int, val name: String, ) Unstable data class User( val id: Int, var name: String, )
  14. Understanding Stability Stable vs. Unstable data class User( val id:

    Int, val name: String, ) data class User( val id: Int, var name: String, ) data class User( val id: Int, val images: List<String>, ) Stable Unstable
  15. Understanding Stability Smart Recomposition recomposition 생략 (Smart Recomposition) ❌ Recomposition

    Recomposition Recomposition Skip Recomposition stable한 데이터 타입의 equals()가 true ❌
  16. Understanding Stability Smart Recomposition 1. Stability에 기반한 의사결정 - 매개변수가

    stable하고 해당 값이 변경되지 않은 경우 (equals() returns true), Compose는 관련 UI 컴포넌트에 대한 recomposition 작업을 생략 - 매개변수가 unstable하거나 stable하지만 값이 변경 된 경우 (equals() returns false), Compose 런타임은 recomposition을 수행하고 UI 레이아웃을 invalidate하여 다시 랜더링 2. Equality 체크 새로운 stable한 유형의 매개변수가 Composable 함수로 전달되면, equals() 메서드를 사용하여 이전의 매개변수 값과 equality(동등성) 확인을 수행
  17. Inferring Composable Functions • Restartable • Skippable • Movable •

    .. @Composable fun NamePlate(name: String, lastname: String) { Column(modifier = Modifier.padding(16.dp)) { Text(text = name) Text(text = lastname) } } Compile Compose 컴파일러는 컴파일 타임에 Restartable, Skippable, Movable과 같은 Composable 함수의 특성을 추론하고 Compose 런타임에게 해당 정보를 전달
  18. Inferring Composable Functions Restartable #Run1 #Run2 입력값이 달라지면, Composable 함수를

    새로운 입력값으로 재실행 대부분의 Composable 함수는 Restartable(재실행 가능)하고 멱등성의 성질을 가짐
  19. Inferring Composable Functions Skippable ❌ #Run1 #Run2 Composable 함수가 stable한

    매개변수만으로 구성된 경우, recomposition을 생략할 수 있는 Skippable로 분류 stable한 데이터 타입의 equals()가 true recomposition 생략 (Smart Recomposition)
  20. Inferring Composable Functions Compose Compiler Metrics stable class StreamShapes {

    stable val circle: Shape stable val square: Shape stable val button: Shape stable val input: Shape stable val dialog: Shape stable val sheet: Shape stable val indicator: Shape stable val container: Shape } restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun Avatar( stable modifier: Modifier? = @static Companion stable imageUrl: String? = @static null stable initials: String? = @static null stable shape: Shape? = @dynamic VideoTheme.<get-shapes>($composer, 0b0110).circle stable textSize: StyleSize? = @static StyleSize.XL stable textStyle: TextStyle? = @dynamic VideoTheme.<get-typography>($composer, 0b0110).titleM stable contentScale: ContentScale? = @static Companion.Crop stable contentDescription: String? = @static null
  21. Inferring Composable Functions Compose Compiler Metrics subprojects { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().all {

    kotlinOptions.freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + project.buildDir.absolutePath + "/compose_metrics" ) kotlinOptions.freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + project.buildDir.absolutePath + "/compose_metrics" ) } } 💻 bit.ly/3V2Q1wB Kotlin 1.9.x Kotlin 2.0.0 + Compose Plugin composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_compiler") }
  22. Stability Annotations Compose Runtime @Immutable @Stable Compose 런타임은 @Immutable과 @Stable

    이라는 두 가지 stability 어노테이션을 제공하여 특정 클래스나 인터페이스가 stable한 것으로 간주될 수 있도록 할 수 있음
  23. Stability Annotations @Immutable @Immutable 어노테이션은 Compose 컴파일러에 대한 강력한 약속이며,

    class의 모든 public 프로퍼티와 필드가 초기화 된 이후에 절대 변경되지 않도록(불변) 보장 @Immutable 어노테이션 사용에 대한 두 가지 규칙: 1. 모든 public 프로퍼티에 대하여 val keyword를 사용하여 불변임을 확인 2. 커스텀 setter 사용을 피하고, 모든 public 프로퍼티에 가변성이 없는지 확인
  24. Stability Annotations @Immutable public data class User( public val id:

    String, public val nickname: String, public val profileImage: String, ) Stable
  25. Stability Annotations @Immutable public data class User( public val id:

    String, public val nickname: String, public val profileImage: String, ) Stable Unstable public data class User( public val id: String, public val nickname: String, public val images: List<String>, )
  26. Stability Annotations @Immutable public data class User( public val id:

    String, public val nickname: String, public val profileImage: String, ) Stable Unstable public data class User( public val id: String, public val nickname: String, public val profileImages: List<String>, ) @Immutable public data class User( public val id: String, public val nickname: String, public val profileImages: List<String>, )
  27. Stability Annotations @Stable ❏ @Stable 어노테이션 또한 Compose 컴파일러에 대한

    강력한 약속이지만, @Immutable 보다는 조금 느슨한 약속을 의미 ❏ 문맥적으로 "Stable(안정적)"이라는 용어는 함수가 동일한 입력값에 대해 일관되게 동일한 결과를 반환하여, 잠재적인 변경 가능성에도 불구하고 예측 가능한 동작을 보장한다는 것을 의미 ❏ @Stable 어노테이션은 public 프로퍼티가 불변인 클래스 혹은 인터페이스에 가장 적합하지만, 클래스 자체나 인터페이스의 구현체가 안정적이지 않을 수 있는 경우에 사용됨
  28. Stability Annotations @Stable @Stable interface State<out T> { val value:

    T } @Stable interface MutableState<T> : State<T> { override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit }
  29. Stability Annotations @Immutable vs. Stable @Immutable public data class User(

    public val id: String, public val nickname: String, public val profileImages: List<String>, ) @Immutable @Stable @Stable interface UiState<T : Result<T>> { val value: T? val exception: Throwable? val hasSuccess: Boolean get() = exception == null }
  30. Stability Annotations @NonRestartableComposable @Composable @NonRestartableComposable fun LaunchedEffect( key1: Any?, block:

    suspend CoroutineScope.() → Unit ) { .. } @NonRestartableComposable • Composable 함수에 사용할 경우 Restartable 속성을 부여하지 않고 recomposition을 생략 • Recomposition의 영향을 받지 않아야 하는 경우 사용하기에 적합 • e.g, Composable 함수 내 상태 관리 및 사이드 이펙트 처리 및 표준 함수만을 호출하는 사례
  31. Stabilize Composable Functions Immutable Collections internal var mutableUserList: MutableList<User> =

    mutableListOf() public val userList: List<User> = mutableUserList @Composable fun Profile(images: List<String>) { .. } Unstable
  32. Stabilize Composable Functions Immutable Collections internal var mutableUserList: MutableList<User> =

    mutableListOf() public val userList: List<User> = mutableUserList kotlin.collections.List를 아래의 immutable collection으로 대체: - kotlinx.collections.immutable (ImmutableList and ImmutableSet) - guava's immutable collections @Composable fun Profile(images: List<String>) { .. } @Composable fun Profile(images: ImmutableList<String>) { .. } Unstable Stable, Skippable
  33. Stabilize Composable Functions Immutable Collections Compose Compiler: KnownStableConstructs.kt object KnownStableConstructs

    { val stableTypes = mapOf( // Guava "com.google.common.collect.ImmutableList" to 0b1, "com.google.common.collect.ImmutableSet" to 0b1, .. // Kotlinx immutable "kotlinx.collections.immutable.ImmutableCollection" to 0b1, "kotlinx.collections.immutable.ImmutableList" to 0b1, .. ) }
  34. Stabilize Composable Functions Wrapper Class @Immutable data class ImmutableUserList( val

    user: List<User>, val expired: java.time.LocalDateTime, ) @Composable fun UserAvatars( stable modifier: Modifier, stable userList: ImmutableUserList, ) @Composable fun UserAvatars( stable modifier: Modifier, unstable user: List<User>, unstable expired: java.time.LocalDateTime, ) Non-Skippable Skippable
  35. Stabilize Composable Functions Stability Configuration File kotlinOptions { freeCompilerArgs +=

    listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" + "${project.absolutePath}/compose_compiler_config.conf" ) } compose_compiler_config.conf // Consider LocalDateTime stable java.time.LocalDateTime // Consider kotlin collections stable kotlin.collections.* // Consider my datalayer and all submodules stable com.datalayer.** // Consider my generic type stable based off it's first type parameter only com.example.GenericClass<*,_> // Consider our data models stable since we always use immutable classes com.google.samples.apps.nowinandroid.core.model.data.*
  36. Stability In Multi-Module :app :core:model :core:network :core:database :feature:camera :feature:feeds :feature:chat

    :feature:user public data class User( public val id: String, public val nickname: String, ) @Composable fun Profile( unstable user: User, ) Non-Skippable
  37. Stability In Multi-Module Compose Stable Marker Compose Stable Marker (GitHub)

    • Compose Runtime에서 제공하는 stability marker 어노테이션(@Stable, @Immutable) 제공 • Compose에 의존성을 두지 않는 순수한 Kotlin 모듈 등에서 stability 어노테이션을 사용해야하는 경우 유용 • KMP 지원 github.com/skydoves/compose-stable-marker @Immutable public data class User( public val id: String, public val nickname: String, ) @Composable fun Profile( stable user: User, ) Skippable dependencies { compileOnly("com.github.skydoves:compose-stable-marker:1.0.4") }