Jetpack Compose Internals를 참고하여 작성하였습니다
https://kancho.tistory.com/61 ComposeRuntime - (2)에서는 Composition에 대한 내용을 작성
이 글에서는 Recomposer에 대해 알아본다
Recomposer
Recomposer는 ControlledComposition을 제어하고 변경 사항들을 적용하기 위해 필요할 때 Recomposition을 트리거한다
또한 Composition을 시작하거나 Recomposition할 스레드 및 변경 사항을 적용할 때 필요한 스레드를 결정한다
Recomposer를 생성하고 invalidation을 대기 시키는 방법
Recomposer 생성 (Spawning the Recomposer)
클라이언트에서 Compose 진입은 Composition을 생성하고 setContent를 호출하는 것이다
생성 시 해당 컴포지션에 대한 부모를 함께 제공하고 Root 컴포지션의 경우 부모가 Recomposer가 된다
안드로이드에서는
ViewGroup.setContent를 호출해 Compose를 사용
내부적으로 Recomposer 팩토리에게 상위 컨텍스트 생성 작업을 위임한다
WindowRecomposerFactory 인터페이스
현재 연결중인 Window에 대한 Recomposer 생성
@InternalComposeUiApi
fun interface WindowRecomposerFactory {
// rootView에 대한 참조 전달
fun createRecomposer(windowRootView: View): Recomposer
// 라이프사이클을 인식하는 Recomposer를 생성
companion object {
@OptIn(ExperimentalComposeUiApi::class)
val LifecycleAware: WindowRecomposerFactory = WindowRecomposerFactory { rootView ->
rootView.createLifecycleAwareWindowRecomposer()
}
}
}
Compose UI에서 UI를 통해 발생하는 모든 일은 AndroidUiDispatcher를 이용해 조정되고 전달된다
이로 인해 AndroidUiDispatcher는 Choreographer 인스턴스 및 메인 Looper의 핸들러와 연결된다
위 팩토리 함수는 먼저
PausableMonotonicFrameClock을 생성한다 (AndroidUiDispatcher의 모노리틱 클럭 위에 구축된 래퍼)
기본적으로 CoroutineContext.Element를 상속하고 있어서 다른 CoroutineContext와 결합될 수 있다
@ExperimentalComposeUiApi
fun View.createLifecycleAwareWindowRecomposer(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
lifecycle: Lifecycle? = null
): Recomposer {
// BaseContext 생서
// CoroutineContext에 필요한 요소가 없으면 AndroidUiDispatcher.CurrentThread를 기반으로 설정
val baseContext = if (coroutineContext[ContinuationInterceptor] == null ||
coroutineContext[MonotonicFrameClock] == null
) {
AndroidUiDispatcher.CurrentThread + coroutineContext
} else coroutineContext
val pausableClock = baseContext[MonotonicFrameClock]?.let {
PausableMonotonicFrameClock(it).apply { pause() }
}
// 결합된 context
// Recomposer가 내부 Job을 생성하여 Recomposer를 종료할 때 모든 composition 또는
// recomposition에 사용되는 이펙트들이 취소될 수 있도록 한다
// Composition 및 Recomposition 후 변경 사항을 적용하는데 사용되는 Context
val contextWithClockAndMotionScale =
baseContext + (pausableClock ?: EmptyCoroutineContext) + motionDurationScale
val recomposer = Recomposer(contextWithClockAndMotionScale).also {
it.pauseCompositionFrameClock()
}
// 위 결합된 context를 사용해 CoroutineScope 생성
// invalidation을 기다리고 이에 따라 recomposition을 트리거하는 작업을 시작하는데 사용
val runRecomposeScope = CoroutineScope(contextWithClockAndMotionScale)
....
// 관찰자는 viewTreeLifecycle에 연결됨
viewTreeLifecycle.addObserver(
object : LifecycleEventObserver {
override fun onStateChanged(
source: LifecycleOwner,
event: Lifecycle.Event
) {
val self = this
when (event) {
Lifecycle.Event.ON_CREATE -> {
runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
try {
// Recomposition 작업 시작
recomposer.runRecomposeAndApplyChanges()
} finally {
source.lifecycle.removeObserver(self)
}
}
}
Lifecycle.Event.ON_START -> {
pausableClock?.resume()
recomposer.resumeCompositionFrameClock()
}
Lifecycle.Event.ON_STOP -> {
recomposer.pauseCompositionFrameClock()
}
Lifecycle.Event.ON_DESTROY -> {
// Recomposer 종료
recomposer.cancel()
}
Lifecycle.Event.ON_PAUSE -> {
// Nothing
}
Lifecycle.Event.ON_RESUME -> {
// Nothing
}
Lifecycle.Event.ON_ANY -> {
// Nothing
}
}
}
}
)
return recomposer
}
다시 setContent를 통해 Composition이 어떻게 생성되었는지 보면
// parent -> Recomposer or parent composition reference
internal fun AbstractComposeView.setContent(
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
val composeView = ...
return doSetContent(composeView, parent, content)
}
private fun doSetContent(
owner: AndroidComposeView,
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
...
val original = Composition(UiApplier(owner.root), parent)
val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
as? WrappedComposition
?: WrappedComposition(owner, original).also {
owner.view.setTag(R.id.wrapped_composition_tag, it)
}
wrapped.setContent(content)
return wrapped
}
Recomposition 프로세스 (Recomposition process)
recomposer.runRecomposeAndApplyChanges 함수가 호출되면 invalidation을 기다리고 invalidation이 발생하면 자동으로 recomposition 된다
runRecomposeAndApplyChanges 함수를 호출할 때
- 스냅샷 변경 전파 프로세스에 대한 관찰자 등록
- 해당 관찰자가 활성화되어 모든 변경사항을 스냅샷 invalidation 목록에 추가하고 알고 있는 모든 Composer에게 전파
- 전파를 통해 어떤 부분이 recomposition 되어야 하는지 기록
- 관찰자는 상태 변경 시 자동적으로 recomposition을 트리거
- 초기 Invalidation
- 관찰자 등록 후 Recomposer는 모든 Composition에 대해 초기 invalidation 수행
- 처음에는 모든 것이 변경되었다고 가정
- invalidation이 수행된 시간을 기점으로 변경 사항 기록 시작
- 모든 보류 중인 invalidation이 유입될 때까지 작업 지연
- 프레임 관련
- parentFrameClock.withFrameNanos 함수를 호출해 다음 프레임 대기
- 변경 사항을 프레임 단위로 통합
- 추가적인 invalidation 발생 가능 (애니메이션 상태 변경으로 인한 Composable 교체 등)
- 상태 변경 처리
- 보류 중인 모든 스냅샷에 대해 invalidation 수행
- 마지막 recomposition 이후 수정된 모든 상태 값으로 Composer의 모든 변경 사항을 보류 중인 recomposition으로 기록
- Recomposition 실행
- invalidate된 Composition에 대해 Recomposition 수행
- Recomposition은 Composition 상태(슬롯 테이블)와 구체화된 트리(Applier)에 필요한 변경을 다시 계산하는 것
- 이미 계산된 코드와 상태를 최대한 재사용
- 잠재적인 Recomposition 처리
- 첫 recomposition으로 인한 추가적인 변경 사항을 감지하고 추가적인 recomposition 예약 (CompositionLocal)
- 변경 사항 적용
- 변경 사항이 있는 모든 Composition에 대해 composition.applyChanges 함수 호출하고 실제 UI에 적용
- Recomposer 상태 업데이트
Recomposition의 동시성 (Concurrent recomposition)
Recomposer에는 recomposition을 동시에 수행할 수 있는 기능이 있다 (runRecomposeConcurrentlyAndApplyChanges)
runRecomposeConcureentlyAndApplyChanges
자동적으로 recomposition 트리거하는 suspend 함수
외부에서 CoroutineContext를 받아 invalidation된 Recomposition 수행
(recomposition 작업을 여러 코루틴에 분산)
@ExperimentalComposeApi
suspend fun runRecomposeConcurrentlyAndApplyChanges(
recomposeCoroutineContext: CoroutineContext
) = recompositionRunner { parentFrameClock ->
...
val recomposeCoroutineScope = CoroutineScope(
coroutineContext + recomposeCoroutineContext + Job(coroutineContext.job)
)
...
// recomposition이 필요한 동안 계속 작업 수행
while (shouldKeepRecomposing) {
// 작업이 준비될 때까지 대기
awaitWorkAvailable()
// 병렬 Composition 처리
recordComposerModifications { composition ->
synchronized(stateLock) {
concurrentCompositionsOutstanding++
}
// 각 composition마다 별도의 코루틴을 사용
recomposeCoroutineScope.launch(composition.recomposeCoroutineContext) {
val changedComposition = performRecompose(composition, null)
synchronized(stateLock) {
// recomposition 결과를 compositionsAwaitingApply에 추가
changedComposition?.let { compositionsAwaitingApply += it }
concurrentCompositionsOutstanding--
deriveStateLocked()
}?.resume(Unit)
}
}
...
}
recomposeCoroutineScope.coroutineContext.job.cancelAndJoin()
frameLoop.cancelAndJoin()
}
suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock -> ... }
Recomposer의 상태 (Recomposer states)
enum class State {
// Recomposer가 취소되고 정리 완료 (완전 종료)
ShutDown,
// Recomposer가 취소었지만 아직 정리 중
ShuttingDown,
// Recomposer가 composer들의 invalidation을 무시 변경 무시
// 새로 시작하려면 runRecomposeAndApplyChanges 호출
Inactive,
// Recomposer 비활성 상태. 대기 중인 작업이 있지만 아직 활성화되지 않은 상태
InactivePendingWork,
// Recomposer가 composition과 스냅샷 invalidation을 추적 중이지만, 현재 수행할 작업이 없는 상태
// 활성화되어 있지만 현재 처리할 변경사항이 없음
Idle,
// Recomposer가 수행할 보류 중인 작업이 있음을 알림 받은 상태
// 작업을 적극적으로 수행 중이거나 수행할 적절한 기회를 기다리는 중
PendingWork
}
'Compose' 카테고리의 다른 글
[ComposeInternals] 컴포즈 UI (Compose UI) - (2) Measure(측정) (0) | 2025.04.04 |
---|---|
[ComposeInternals] 컴포즈 UI (Compose UI) - (1) (0) | 2025.04.03 |
[ComposeInternals] 컴포즈 런타임 (ComposeRuntime) - (2) Composition (0) | 2025.03.30 |
[ComposeInternals] 컴포즈 런타임 (ComposeRuntime) - (1) Composer (0) | 2025.03.16 |
[ComposeInternals] Composable 함수들 (0) | 2025.03.03 |