본문 바로가기

안드로이드/Compose

Compose 동작 원리

Compose 구조

 

1. Compose Compiler

  • Compose Runtime이 수행하기 적합한 형태의 코드로 변환
  • 순수한 Kotlin으로 작성된 Kotlin 컴파일러 플러그인
  • Compose 컴파일러 > 런타임 과정에서 FIR(Frontend Intermediate Representation)을 통해 개발자가 작성한 코드를 정적 분석 및 수정 가능

2. Compose Runtime

  • Compose Compiler에서 넘어온 코드를 바탕으로 UI에서 활용할 수 있도록 관리
  • Gap Buffer 자료구조에서 파생 된 Slot Table에서 상태를 저장 및 관리
  • Composable 함수에 대한 실질적인 생명주기 관리 및 UI에 대한 정보를 담고 있는 메모리 표현

3. Compose UI

  • Compose Runtime의 데이터를 활용하여 컴포넌트를 화면에 랜더링하는 역할
  • 컴포저블 함수 또는 컴포넌트를 이용하여 레이아웃 노드 생성
  • 노드를 바탕으로 컴포즈 레이아웃 트리 구축
  • 런타임에 의해 소비

Composable Function의 랜더링 과정

1. Composition

  • 설명 생성 
  • Slot Table에 기재
  • UI 트리 생성

2. Layout

  • 컴포저블 트리 내에서 각 컴포저블 노드의 위치 등이 결정
    • Measure
    • Place
  • 각 노드는 각 하위요소를 측정, 자신의 크기 결정, 배치

3. Drawing


Composition 관련 내부 코드 분석

1. @Composable

@Composable 어노테이션은 다음과 같이 구성되어 있다.

@MustBeDocumented
@Retention(AnnotationRetention.BINARY)
@Target(
    // function declarations
    // @Composable fun Foo() { ... }
    // lambda expressions
    // val foo = @Composable { ... }
    AnnotationTarget.FUNCTION,

    // type declarations
    // var foo: @Composable () -> Unit = { ... }
    // parameter types
    // foo: @Composable () -> Unit
    AnnotationTarget.TYPE,

    // composable types inside of type signatures
    // foo: (@Composable () -> Unit) -> Unit
    AnnotationTarget.TYPE_PARAMETER,

    // composable property getters and setters
    // val foo: Int @Composable get() { ... }
    // var bar: Int
    //   @Composable get() { ... }
    AnnotationTarget.PROPERTY_GETTER
)
annotation class Composable

이와 같은 @Composable 어노테이션을 함수 또는 람다에 걸어주게 되면, Compose에게 애플리케이션 데이터에서 트리 또는 레이어로의 변환을 나타낸다는 것을 알려주게 된다.

 

또한 함수 또는 람다식의 타입이 변하게 되는데, 이로 인해 Composable 함수는 Composable 함수 내부에서만 호출할 수 있다.

2. Composition

Composition은 Interface로

interface Composition {
    val hasInvalidations: Boolean
    val isDisposed: Boolean
    fun dispose()
    fun setContent(content: @Composable () -> Unit)
}

setContent와 같은 UI를 구성하는 최상위 API에서 리턴된다.

setContent & CompositionContext

setContent에는 CompositionContext 를 인자로 갖고 있고, 이를 Parent에게 전달하게 된다.

CompositionContext: 두 개의 Composition을 논리적으로 연결하는데 사용되는 추상 클래스

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

    if (existingComposeView != null) with(existingComposeView) {
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(parent)
        setContent(content)
        // Set the view tree owners before setting the content view so that the inflation process
        // and attach listeners will see them already present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

만일 CompositionContext가 null이라면 View가 연결 된 창에서 자동으로 설정해준다.

 

또한 content를 등록(setContent(content))하면서 ParentCompositionContext를 전달하며

fun setContent(content: @Composable () -> Unit) {
    shouldCreateCompositionOnAttachedToWindow = true
    this.content.value = content
    if (isAttachedToWindow) {
        createComposition()
    }
}

fun createComposition() {
    check(parentContext != null || isAttachedToWindow) {
        "createComposition requires either a parent reference or the View to be attached" +
            "to a window. Attach the View or call setParentCompositionReference."
    }
    ensureCompositionCreated()
}
    
@Suppress("DEPRECATION") // Still using ViewGroup.setContent for now
private fun ensureCompositionCreated() {
    if (composition == null) {
        try {
            creatingComposition = true
            composition = setContent(resolveParentCompositionContext()) {
                Content()
            }
        } finally {
            creatingComposition = false
        }
    }
}
    
private fun resolveParentCompositionContext() = parentContext
        ?: findViewTreeCompositionContext()?.cacheIfAlive()
        ?: cachedViewTreeCompositionContext?.get()?.takeIf { it.isAlive }
        ?: windowRecomposer.cacheIfAlive()

위와 같은 과정을 통해 CompositionContext가 하위 Composable 함수들에게 전달되고, 이 때문에 Composition 간의 데이터 전달 등의 로직들이 동작할 수 있게 된다.

3. Composer와 ComposeNode

각 Composable 함수 내에는 Layout 함수가 구현되어 있다.

inline fun Layout(
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val compositeKeyHash = currentCompositeKeyHash
    val materialized = currentComposer.materialize(modifier)
    val localMap = currentComposer.currentCompositionLocalMap
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, SetMeasurePolicy)
            set(localMap, SetResolvedCompositionLocals)
            set(materialized, SetModifier)
            @OptIn(ExperimentalComposeUiApi::class)
            set(compositeKeyHash, SetCompositeKeyHash)
        },
    )
}

Layout 함수에서 ReusableComposeNode 에 전달 받은 매개 변수들을 전달해주고 있고,

RunsableComposeNode를 보면

@Suppress("NONREADONLY_CALL_IN_READONLY_COMPOSABLE", "UnnecessaryLambdaCreation")
@Composable inline fun <T : Any, reified E : Applier<*>> ReusableComposeNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater<T>.() -> Unit
) {
    if (currentComposer.applier !is E) invalidApplier()
    currentComposer.startReusableNode()
    if (currentComposer.inserting) {
        currentComposer.createNode(factory)
    } else {
        currentComposer.useNode()
    }
    Updater<T>(currentComposer).update()
    currentComposer.endNode()
}

Composer를 통해 Node를 관리하고 있는 것을 볼 수 있다.

이 노드들은 Composer를 통해 Gap Buffer 데이터 구조로 관리되며, Compose에서는 Slot Table이라는 이름으로 사용되고 있고, Composition 데이터를 저장하고 있다.

4. ComponentActivity를 상속 받는 이유

setContent 를 실행하면 제일 먼저 현재 화면의 ViewGrop을 찾는다.

val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

ComposeView는 ViewGroup을 상속 받는 AbstractComposeView 를 상속 받고 있다.

만일 ViewGroup이 없다면 ComposeView를 생성하고, 이 ComposeView는 루트 ViewGroup이 된다.

 

루트 ViewGroup이 되는 이유는 AbstractComposeView 클래스 내의 함수에서

fun setParentCompositionContext(parent: CompositionContext?) {
        parentContext = parent
    }

위와 같이 인자로 넘겨 받은 CompositionContext를 등록하게 끔 되어있는데 setContent의 ComposeView에서는 위의 함수를 사용하지 않기에 루트 ViewGroup이라는 것을 알 수 있다.

 

이렇게 생성한 ComposeView를 다음과 같은 단계를 거쳐 등록을 한다.

ComposeView(this).apply {
    // Set content and parent **before** setContentView
    // to have ComposeView create the composition on attach
    setParentCompositionContext(parent)
    setContent(content)
    // Set the view tree owners before setting the content view so that the inflation process
    // and attach listeners will see them already present
    setOwners()
    setContentView(this, DefaultActivityContentLayoutParams)
}

private fun ComponentActivity.setOwners() {
    val decorView = window.decorView
    if (decorView.findViewTreeLifecycleOwner() == null) {
        decorView.setViewTreeLifecycleOwner(this)
    }
    if (decorView.findViewTreeViewModelStoreOwner() == null) {
        decorView.setViewTreeViewModelStoreOwner(this)
    }
    if (decorView.findViewTreeSavedStateRegistryOwner() == null) {
        decorView.setViewTreeSavedStateRegistryOwner(this)
    }
}

setContentView는 우리가 기존 Activity의 onCreate() 내에서 xml을 등록할 때 자주 보던 함수로, Compose 또한 생성한 ComposeView를 ContentView에 등록해주는 것을 볼 수 있다.

 

이를 통해 우리는 왜 Compose를 사용할 때 ComponentActivity를 상속 받는 지 유추해볼 수 있다.

  1. Compose 또한 ViewGroup을 상속 받아 구현하는 방식으로 ComponentActivity에 구현되어 있는 setContentView에 ComposeView를 등록해야 하기 때문
  2. Compose는 Fragment가 필요 없기 때문에 불필요한 상속을 피하고자 FragmentActivity와 AppCompatActivity를 상속 받지 않는다. (AppCompatActivity를 상속 받아도 Compose를 구현할 수 있다.)

 

참고

https://medium.com/hongbeomi-dev/compose-deep-dive-1-composition-c5df4fda68f8

 

Compose Deep Dive — 1. Composition

Jetpack Compose Layout의 동작 원리를 Deep Dive into Jetpack Compose Layout(Android Dev Summit21) 영상과 함께 살펴봅시다.

medium.com

 

'안드로이드 > Compose' 카테고리의 다른 글

Recomposition  (0) 2025.01.21