Slide 1

Slide 1 text

Compose 성능 최적화를 위한 Stability 마스터하기 엄재웅 / GetStream

Slide 2

Slide 2 text

skydoves @github_skydoves Lead Android Developer Advocate @ Stream Jaewoong Eum

Slide 3

Slide 3 text

Open-Source Projects 10,000,000+ library downloads every year 1,500,000,000+ end-user devices

Slide 4

Slide 4 text

Jetpack Compose

Slide 5

Slide 5 text

Jetpack Compose

Slide 6

Slide 6 text

Compose Structure Code Transformation In-memory Representation Rendering Layout Trees

Slide 7

Slide 7 text

Compose Structure Compose Compiler ● Jetpack Compose 내에서 중추적인 역할을 하는 핵심 구성 요소 ● 순수한 Kotlin으로 작성된 Kotlin 컴파일러 플러그인 ● KAPT나 KSP와 같은 annotation processor와는 달리, Compose 컴파일러는 FIR (Frontend Intermediate Representation)을 통하여 개발자가 작성한 코드를 정적 분석 및 변형

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Compose Structure Compose Runtime ● Compose 모델 및 상태 관리의 초석 역할 ● Gap Buffer라는 데이터 구조에서 파생된 Slot Table을 사용하여 상태 저장 등 메모리 운용 ● Composable 함수에 대한 실질적인 생명주기 관리 및 UI에 대한 정보를 담고 있는 메모리 표현

Slide 10

Slide 10 text

Compose Structure Compose UI ● 개발자가 Composable 함수를 통해 레이아웃을 생성할 수 있도록 여러 UI 라이브러리를 통해 컴포넌트 제공 ● 각 컴포넌트는 LayoutNode를 생성하고, 결과적으로 Compose 레이아웃 트리 구성을 용이하게 함 ● Compose Runtime에 의하여 소비되는 것이 기본 원칙

Slide 11

Slide 11 text

80+ Compose Compiler 120+ Compose UI & Runtime versions have been released. Jetpack Compose continues to evolve with steady performance enhancements. Compose Histories

Slide 12

Slide 12 text

Compose UI Compose Compiler & Runtime ● Smart Recomposition ● Strong Skipping Mode ● Stability Configuration File Compose Histories

Slide 13

Slide 13 text

Compose Phases

Slide 14

Slide 14 text

Compose Phases

Slide 15

Slide 15 text

Compose Phases

Slide 16

Slide 16 text

Compose Phases Recomposition

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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, )

Slide 25

Slide 25 text

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, ) Stable Unstable

Slide 26

Slide 26 text

Understanding Stability Smart Recomposition #Run1 #Run2 recomposition 생략 (Smart Recomposition) ❌ stable한 데이터 타입의 equals()가 true

Slide 27

Slide 27 text

Understanding Stability Smart Recomposition recomposition 생략 (Smart Recomposition) ❌ Recomposition Recomposition Recomposition Skip Recomposition stable한 데이터 타입의 equals()가 true ❌

Slide 28

Slide 28 text

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(동등성) 확인을 수행

Slide 29

Slide 29 text

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 런타임에게 해당 정보를 전달

Slide 30

Slide 30 text

Inferring Composable Functions Restartable #Run1 #Run2 입력값이 달라지면, Composable 함수를 새로운 입력값으로 재실행 대부분의 Composable 함수는 Restartable(재실행 가능)하고 멱등성의 성질을 가짐

Slide 31

Slide 31 text

Inferring Composable Functions Skippable ❌ #Run1 #Run2 Composable 함수가 stable한 매개변수만으로 구성된 경우, recomposition을 생략할 수 있는 Skippable로 분류 stable한 데이터 타입의 equals()가 true recomposition 생략 (Smart Recomposition)

Slide 32

Slide 32 text

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.($composer, 0b0110).circle stable textSize: StyleSize? = @static StyleSize.XL stable textStyle: TextStyle? = @dynamic VideoTheme.($composer, 0b0110).titleM stable contentScale: ContentScale? = @static Companion.Crop stable contentDescription: String? = @static null

Slide 33

Slide 33 text

Inferring Composable Functions Compose Compiler Metrics subprojects { tasks.withType().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") }

Slide 34

Slide 34 text

Stability Annotations Compose Runtime @Immutable @Stable Compose 런타임은 @Immutable과 @Stable 이라는 두 가지 stability 어노테이션을 제공하여 특정 클래스나 인터페이스가 stable한 것으로 간주될 수 있도록 할 수 있음

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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, )

Slide 38

Slide 38 text

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, ) @Immutable public data class User( public val id: String, public val nickname: String, public val profileImages: List, )

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Stability Annotations @Stable @Stable interface State { val value: T } @Stable interface MutableState : State { override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit }

Slide 41

Slide 41 text

Stability Annotations @Immutable vs. Stable @Immutable public data class User( public val id: String, public val nickname: String, public val profileImages: List, ) @Immutable @Stable @Stable interface UiState> { val value: T? val exception: Throwable? val hasSuccess: Boolean get() = exception == null }

Slide 42

Slide 42 text

Stability Annotations @NonRestartableComposable @Composable @NonRestartableComposable fun LaunchedEffect( key1: Any?, block: suspend CoroutineScope.() → Unit ) { .. } @NonRestartableComposable ● Composable 함수에 사용할 경우 Restartable 속성을 부여하지 않고 recomposition을 생략 ● Recomposition의 영향을 받지 않아야 하는 경우 사용하기에 적합 ● e.g, Composable 함수 내 상태 관리 및 사이드 이펙트 처리 및 표준 함수만을 호출하는 사례

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Stabilize Composable Functions Immutable Collections internal var mutableUserList: MutableList = mutableListOf() public val userList: List = mutableUserList kotlin.collections.List를 아래의 immutable collection으로 대체: - kotlinx.collections.immutable (ImmutableList and ImmutableSet) - guava's immutable collections @Composable fun Profile(images: List) { .. } @Composable fun Profile(images: ImmutableList) { .. } Unstable Stable, Skippable

Slide 45

Slide 45 text

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, .. ) }

Slide 46

Slide 46 text

Stabilize Composable Functions Wrapper Class @Immutable data class ImmutableUserList( val user: List, val expired: java.time.LocalDateTime, ) @Composable fun UserAvatars( stable modifier: Modifier, stable userList: ImmutableUserList, ) @Composable fun UserAvatars( stable modifier: Modifier, unstable user: List, unstable expired: java.time.LocalDateTime, ) Non-Skippable Skippable

Slide 47

Slide 47 text

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.*

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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") }

Slide 50

Slide 50 text

Stability In Multi-Module Stability Configuration File

Slide 51

Slide 51 text

Blog Post Jetpack Compose 성능 최적화를 위한 Stability 이해하기 velog.io/@skydoves/compose-stability

Slide 52

Slide 52 text

Jetpack Compose Internals

Slide 53

Slide 53 text

Thank you. https://github.com/skydoves [email protected] https://twitter.com/github_skydoves https://medium.com/@skydoves