Jetpack Compose의 상태 스냅샷 시스템에 대해 계속해서 자세히 알아보세요.

Jetpack Compose에는 상태를 표현하고 상태 변경 사항을 전파하여 최고의 반응형 경험을 제공하는 특별한 방법인 상태 스냅샷 시스템이 있습니다. 이 반응형 모델은 구성 요소가 입력에 따라 자동으로 재구성되고 필요한 경우에만 Android 보기 시스템 문서에서 이러한 변경 사항을 수동으로 알리는 데 필요한 모든 상용구를 피하므로 코드를 더욱 강력하고 깔끔하게 만듭니다.

스냅샷 상태란 무엇입니까?

스냅샷 상태는 변경 사항을 기록하고 관찰할 수 있는 격리된 상태를 나타냅니다. mutableStateOf, mutableStateListOf, mutableStateMapOf,derivedStateOf,ProduceState,collectAsState 등과 같은 함수를 호출할 때 얻는 상태는 스냅샷 상태입니다. 이러한 모든 함수는 개발자가 종종 스냅샷 상태라고 부르는 특정 유형의 상태를 반환합니다.

" 스냅샷 상태 "라는 용어는 Jetpack Compose 런타임에서 정의한 상태 스냅샷 시스템의 일부이기 때문에 이름이 붙여졌습니다. 이 시스템은 상태 변경과 변경 전파를 모델링하고 조정합니다. 분리된 방식으로 작성되었으므로 이론적으로 관찰 가능한 상태에 의존하려는 다른 라이브러리에서 사용할 수 있습니다.

이전에 변경 전파에 관해 배운 것 중 하나는 모든 컴포저블 선언과 ​​표현식이 Jetpack Compose 컴파일러에 의해 래핑되어 본문 내의 모든 스냅샷 상태 읽기를 자동으로 추적한다는 것입니다. 이것이 스냅샷 상태가 (자동으로) 관찰되는 방식입니다. 목표는 컴포저블에서 읽은 상태가 변경될 때마다 런타임에서 RecomposeScope를 무효화하여 다음 재구성 시 다시 실행되도록 하는 것입니다.

이는 Compose에서 제공하는 인프라 코드이므로 클라이언트 코드베이스에 존재할 필요가 없습니다. Compose UI와 같은 런타임 클라이언트는 무효화 및 상태 전파 방법이나 재구성을 트리거하는 방법을 전혀 알 필요가 없지만 이 상태와 함께 작동하는 구성 요소인 구성 가능한 함수를 제공하는 데만 집중하면 됩니다.

그러나 스냅샷 상태는 단순히 재구성을 트리거하기 위해 변경 사항을 자동으로 알리는 문제가 아닙니다. "스냅샷"이라는 단어를 사용하는 매우 중요한 이유 중 하나는 상태 격리입니다. 이는 동시성 컨텍스트에서 적용하는 격리 수준을 나타냅니다.

서로 다른 스레드 간에 변경 가능한 상태를 처리한다고 상상해 보세요. 이것은 쉽게 엉망이 될 수 있습니다. 동시에 다른 스레드에서 읽거나 수정할 수 있으므로 상태의 무결성을 보장하려면 엄격한 조정 및 동기화가 필요합니다. 이는 충돌, 감지하기 어려운 버그 및 경쟁 조건의 가능성을 열어줍니다.

전통적으로 프로그래밍 언어는 이 문제에 다양한 방식으로 접근해 왔으며 그 중 하나가 불변성입니다. 불변 데이터는 생성된 후에는 수정되지 않으므로 동시 시나리오로부터 절대적으로 안전합니다. 또 다른 효과적인 접근 방식은 행위자 시스템이 될 수 있습니다. 시스템은 스레드 간 상태 격리에 중점을 둡니다. 행위자는 자신의 상태 사본을 유지하고 메시지를 통해 통신/조정합니다. 이 상태가 변경 가능한 경우 전역 프로그램 상태를 일관되게 만들기 위해 일부 조정이 필요합니다. Compose 스냅샷 시스템은 행위자 시스템을 기반으로 하지 않지만 실제로는 해당 접근 방식에 더 가깝습니다.

Jetpack Compose는 변경 가능한 상태를 사용하므로 구성 가능한 함수가 상태 업데이트에 자동으로 응답할 수 있습니다. 불변 상태만 사용하는 라이브러리는 의미가 없습니다. 이는 구성이 여러 스레드에서 구현될 수 있으므로 동시 시나리오에서 상태 공유 문제를 해결해야 함을 의미합니다. 이 문제에 대한 Compose의 솔루션은 상태 격리 및 후속 변경 전파를 기반으로 하는 상태 스냅샷 시스템으로, 변경 가능한 상태를 여러 스레드에서 안전하게 사용할 수 있습니다.

스냅샷 상태 시스템은 안전한 방식으로 스레드 전체에서 상태를 조정해야 하기 때문에 동시성 제어 시스템을 사용하여 모델링됩니다. 동시 환경에서 변경 가능한 상태를 공유하는 것은 쉽지 않으며 이는 라이브러리의 실제 사용 사례와 관련이 없는 일반적인 문제입니다.

Jetpack Compose의 상태는 모든 스냅샷 상태 객체가 구현하는 인터페이스입니다. 다음은 State 인터페이스의 코드 형식입니다.

Jetpack Compose는 (설계상의 이유로) 안정적인 구현만 제공하고 사용하기 때문에 이 프로토콜은 @Stable로 표시됩니다. 간단히 말해서 이 인터페이스의 모든 구현은 다음을 보장해야 함을 의미합니다.

  • 동일한 두 State 인스턴스에서 equals 메서드를 호출하면 항상 동일한 결과가 반환됩니다.

  • 유형의 노출된 속성 값이 변경되면 컴포지션에 알림이 전송됩니다.

  • 모든 공공 재산 가치 유형도 안정적입니다.

다음으로, 먼저 동시성 제어 시스템에 대한 지식을 이해하세요. 이는 Jetpack Compose 상태 스냅샷 시스템이 이 모델을 채택하는 이유를 더 쉽게 이해하는 데 도움이 됩니다.

동시성 제어 시스템

상태 스냅샷 시스템은 동시성 제어 시스템으로 구현되므로 먼저 해당 개념을 소개하겠습니다.

컴퓨터 과학에서 "동시성 제어"는 동시 작업의 올바른 결과를 보장하는 방법, 즉 조정 및 동기화를 의미합니다. 동시성 제어는 전체 시스템의 정확성을 보장하는 일련의 규칙으로 구성됩니다. 그러나 조정에는 항상 대가가 따릅니다. 조정은 종종 성능에 영향을 미치므로 핵심 과제는 성능을 크게 저하시키지 않으면서 최대한 효율적인 방법을 고안하는 것입니다.

동시성 제어의 예로는 데이터베이스 관리 시스템(DBMS)의 트랜잭션 시스템이 있습니다. 이 맥락에서 동시성 제어는 동시 환경에서 실행되는 모든 데이터베이스 트랜잭션이 데이터베이스의 데이터 무결성을 침해하지 않고 안전한 방식으로 수행되도록 보장합니다. 목표는 정확성을 유지하는 것입니다. 여기서 "안전함"에는 트랜잭션이 원자성이며, 안전하게 실행 취소할 수 있고, 커밋된 트랜잭션의 효과가 절대 손실되지 않으며, 중단된 트랜잭션의 효과가 데이터베이스에 남지 않는다는 보장이 포함됩니다. 이것은 복잡한 문제입니다.

동시성 제어는 DBMS뿐만 아니라 트랜잭션 메모리 구현과 같은 프로그래밍 언어에서도 자주 발생합니다. 트랜잭션 메모리는 일련의 로드 및 저장 작업이 원자적으로 수행되도록 하여 동시 프로그래밍을 단순화하려고 시도합니다. 실제로 Compose 상태 스냅샷 시스템에서는 상태 변경이 한 스냅샷에서 다른 스냅샷으로 전파될 때 상태 쓰기가 단일 원자 작업으로 적용됩니다. 이와 같은 그룹화 작업은 병렬 시스템/프로세스에서 공유 데이터의 동시 읽기 및 쓰기 간의 조정을 단순화합니다. 이를 기반으로 원자성 변경을 쉽게 중단, 실행 취소 또는 재생할 수 있습니다. 즉, 모든 버전의 프로그램 상태를 재생성할 수 있는 재현 가능한 변경 기록이 있어야 합니다.

동시성 제어 시스템에는 다양한 클래스가 있습니다.

  • Optimistic : 읽기 또는 쓰기 작업을 차단하지 않으며 이러한 작업의 안전성에 대해 낙관적입니다. 커밋이 필수 규칙을 위반하는 경우 위반을 방지하기 위해 트랜잭션을 중단합니다. 중단된 트랜잭션은 즉시 다시 실행되므로 오버헤드가 발생합니다. 이 전략은 중단된 트랜잭션의 평균 수가 너무 높지 않을 때 좋은 선택이 될 수 있습니다.

  • 비관적(Pessimistic) : 작업이 규칙을 위반하는 경우 위반 가능성이 사라질 때까지 해당 트랜잭션의 작업을 차단합니다.

  • 반낙관적 : 이는 다른 두 솔루션의 하이브리드 솔루션입니다. 어떤 경우에는 작업만 차단하고 다른 경우에는 낙관적으로 생각합니다(그런 다음 커밋 시 중단).

각 범주의 성능은 평균 트랜잭션 완료율(처리량), 필요한 병렬 처리 수준 및 교착 상태 가능성과 같은 기타 요소에 따라 달라집니다. 비낙관적

MVCC(다중 버전 동시성 제어)

Jetpack Compose의 전역 상태는 컴포지션과 스레드 간에 공유됩니다. 복합 함수는 병렬로 실행될 수 있어야 하며(병렬 ​​재구성은 언제든지 가능함) 병렬로 실행될 경우 스냅샷 상태를 동시에 읽거나 수정할 수 있으므로 상태 격리가 필요합니다.

동시성 제어의 주요 속성 중 하나는 실제로 격리입니다. 이 기능은 데이터에 대한 동시 액세스의 경우 정확성을 보장합니다. 격리를 달성하는 가장 쉬운 방법은 작성기가 완료될 때까지 모든 판독기를 차단하는 것이지만 이는 성능에 큰 영향을 미칠 수 있습니다. MVCC(다중 버전 동시성 제어)가 더 나은 성능을 발휘할 수 있습니다.

격리를 달성하기 위해 MVCC는 데이터의 여러 복사본(스냅샷)을 유지하므로 각 스레드는 특정 순간에 격리된 상태 스냅샷으로 작업할 수 있습니다. 우리는 이를 상태의 다양한 버전("다중 버전")으로 이해할 수 있습니다. 스레드에 의한 수정 사항은 모든 로컬 변경 사항이 만들어지고 전파될 때까지 다른 스레드에 표시되지 않습니다.

동시성 제어 시스템에서는 이 기술을 "스냅샷 격리"라고 하며 각 "트랜잭션"에 표시되는 버전을 결정하는 데 사용되는 격리 수준으로 정의됩니다.

MVCC는 또한 불변성을 활용하므로 데이터가 기록될 때마다 원본을 수정하는 대신 데이터의 새 복사본이 생성됩니다. 이로 인해 객체의 모든 변경 기록과 마찬가지로 동일한 데이터의 여러 버전이 메모리에 저장됩니다. Compose에서는 이를 ' 상태 레코드 '라고 합니다.

MVCC의 또 다른 특징은 특정 시점에 일관된 뷰를 생성한다는 것입니다 . 이는 일반적으로 백업 파일의 속성이며 특정 백업의 모든 개체에 대한 참조가 일관되게 유지됨을 의미합니다. MVCC에서는 일반적으로 트랜잭션 ID로 이를 보장하므로 모든 읽기 작업은 해당 ID를 참조하여 사용할 상태 버전을 결정할 수 있습니다. 이것이 실제로 Jetpack Compose에서 작동하는 방식입니다. 각 스냅샷에는 고유한 ID가 할당됩니다. 스냅샷 ID는 단조롭게 증가하는 값이므로 자연스럽게 정렬됩니다. 스냅샷은 ID로 구별되므로 읽기와 쓰기는 잠금 없이 격리됩니다.

스냅 사진

스냅샷은 언제든지 생성될 수 있습니다. 이는 특정 순간(스냅샷이 생성된 시점)의 프로그램(모든 스냅샷 상태 개체)의 현재 상태를 반영합니다. 여러 개의 스냅샷을 생성할 수 있으며 각 스냅샷은 프로그램 상태에 대한 독립적인 복사본을 갖습니다. 즉, 해당 시점의 모든 현재 스냅샷 상태 개체의 상태 복사본(State 인터페이스를 구현하는 개체)의 복사본입니다.

이 접근 방식을 사용하면 한 스냅샷의 상태 개체를 업데이트해도 다른 스냅샷의 동일한 상태 개체 복사본에 영향을 주지 않으므로 상태 수정이 안전해집니다. 스냅샷은 서로 격리되어 있습니다. 여러 스레드가 있는 동시 시나리오에서 각 스레드는 서로 다른 스냅샷을 가리키므로 상태의 서로 다른 복사본을 가리킵니다.

Jetpack Compose 런타임은 프로그램의 현재 상태를 시뮬레이션하기 위해 Snapshot 클래스를 제공합니다. 모든 코드는 스냅샷을 얻으려면 정적 메소드인 val snapshot = Snapshot.takeSnapshot()만 호출하면 됩니다. 그러면 모든 상태 개체의 현재 값에 대한 스냅샷이 찍히고 이 값은 snapshot.dispose() 메서드가 호출될 때까지 보존됩니다. 이에 따라 스냅샷의 수명이 결정됩니다.

스냅샷에는 수명주기가 있습니다. 스냅샷 사용을 완료할 때마다 이를 삭제해야 합니다. snapshot.dispose()를 호출하지 않으면 해당 스냅샷 및 보유 상태와 관련된 모든 리소스가 유출됩니다. 스냅샷은 생성 상태와 릴리스 상태 사이에서 활성 상태로 간주됩니다.

스냅샷이 생성되면 해당 스냅샷의 모든 상태를 동일한 상태의 다른 기본 버전과 쉽게 구별할 수 있도록 ID가 부여됩니다. 이를 통해 프로그램 상태의 버전 관리, 즉 버전에 따라 프로그램 상태를 일관되게 유지할 수 있습니다(다중 버전 동시성 제어).

스냅샷은 코드를 통해 가장 잘 이해됩니다. Zach Klipp의 매우 유익하고 자세한 게시물에서 직접 코드 조각을 가져와 설명하겠습니다.

Dog 클래스의 이름은 mutableStateOf("")를 구현한 것입니다.

여기에서 일반적으로 "스냅샷 입력"이라고 불리는 snapshot.enter 함수는 스냅샷의 컨텍스트에서 람다 표현식을 실행하므로 스냅샷은 모든 상태에 대한 정보의 소스가 됩니다. 람다 표현식에서 읽은 모든 상태는 그 가치를 스냅샷으로 찍으세요. 이 메커니즘을 사용하면 Compose 및 기타 클라이언트 라이브러리가 지정된 스냅샷의 컨텍스트에서 상태 처리 논리를 실행할 수 있습니다. 이 프로세스는 Enter 호출이 반환될 때까지 로컬 스레드에서 발생합니다. 다른 스레드는 어떤 식으로든 영향을 받지 않습니다.

위의 예에서 업데이트된 개의 이름이 "Fido"인 것을 볼 수 있지만 스냅샷의 컨텍스트(enter 호출)에서 이를 읽으면 스냅샷이 생성되었을 때의 "Spot"을 반환합니다. 소유된 가치를 창출했습니다.

물론 스냅샷을 사용한 후에는 snapshot.dispose()를 호출하여 상태를 해제해야 한다는 점을 기억해야 합니다. 전체 코드는 다음과 같습니다.

class Dog {
    var name: MutableState<String> = mutableStateOf("")
}

fun main() {
    val dog = Dog()
    dog.name.value = "Spot"
    val snapshot = Snapshot.takeSnapshot()
    dog.name.value = "Fido"

    println(dog.name.value) // ---> Fido
    snapshot.enter { println(dog.name.value) } // 进入快照 ---> Spot
    println(dog.name.value) // ---> Fido

    // When finished with the snapshot, it must always be disposed. 
    snapshot.dispose()
}

Enter 함수 내에서는 스냅샷 유형(읽기 전용 또는 변경 가능)에 따라 상태를 읽고 쓸 수 있습니다.

Snapshot.takeSnapshot()을 통해 생성된 스냅샷은 읽기 전용입니다. 포함된 모든 상태는 수정할 수 없습니다. 스냅샷의 상태 객체에 쓰려고 하면 예외가 발생합니다.

그러나 모든 작업이 읽기 상태인 것은 아니며 업데이트(쓰기)해야 할 수도 있습니다. Compose는 보유 상태를 수정할 수 있는 Snapshot 계약인 MutableSnapshot의 특정 구현을 제공합니다. 그 외에도 사용 가능한 다른 구현이 있습니다. 다음은 다양한 유형의 스냅샷 구현을 모두 나열한 것입니다.

다양한 유형의 스냅샷을 간략하게 설명하겠습니다.

  • ReadonlySnapshot : 스냅샷에 보관된 상태 개체는 읽기 전용이며 읽기만 가능하고 수정할 수는 없습니다.

  • MutableSnapshot : 스냅샷에 보관된 상태 개체를 읽고 수정할 수 있습니다.

  • NestedReadonlySnapshot 및 NestedMutableSnapshot : 스냅샷이 트리를 형성할 수 있으므로 Child의 읽기 전용 및 변경 가능한 스냅샷용입니다. 스냅샷에는 중첩된 스냅샷이 여러 개 포함될 수 있습니다. 이에 대해서는 나중에 자세히 설명하겠습니다.

  • GlobalSnapshot : 전역 공유 프로그램 상태의 변경 가능한 스냅샷을 보유합니다. 이는 실제로 모든 스냅샷의 루트 스냅샷입니다.

  • TransparentObserverMutableSnapshot : 이것은 특별한 경우입니다. 상태 격리를 적용하지 않으며 상태 개체를 읽고 쓸 때 읽기 및 쓰기 관찰자에게 알리기 위해서만 존재합니다. 여기에 있는 모든 상태 레코드는 자동으로 유효하지 않은 것으로 표시되므로 다른 스냅샷에서 보거나 읽을 수 없습니다. 이 스냅샷 유형의 ID는 항상 상위 항목의 ID이므로 이에 대해 생성된 모든 레코드는 실제로 상위 항목과 연결됩니다. 여기서 수행되는 모든 작업은 마치 상위 스냅샷에서 수행되는 것처럼 수행된다는 점에서 "투명"합니다.

스냅샷 트리

위에서 설명한 것처럼 스냅샷은 트리를 형성합니다. 따라서 다양한 스냅샷 유형 중에서 NestedReadonlySnapshot 및 NestedMutableSnapshot을 찾을 수 있습니다. 모든 스냅샷에는 중첩된 스냅샷이 개수 제한 없이 포함될 수 있습니다. 트리의 루트는 전역 상태를 보유하는 GlobalSnapshot입니다.

중첩된 스냅샷은 독립적으로 삭제/해제할 수 있는 독립적인 스냅샷 복사본과 같습니다. 이를 통해 상위 스냅샷을 활성 상태로 유지하면서 삭제/해제할 수 있습니다. Compose에서 하위 컴포지션을 사용할 때 자주 나타납니다.

간략한 요약입니다. 앞서 하위 컴포지션은 독립적인 무효화를 지원하는 것이 유일한 목적인 상위 컴포지션 내에서 생성된 인라인 컴포지션이라고 언급했습니다. 컴포지션과 하위 컴포지션도 트리를 형성합니다.

지연된 목록이나 BoxWithConstraints를 생성할 때 중첩된 스냅샷의 하위 구성이 생성됩니다. SubcomposeLayout 또는 VectorPainter에서도 하위 구성을 찾을 수 있습니다.

하위 구성을 만들어야 하는 경우 상태를 저장하고 격리하기 위해 중첩된 스냅샷이 생성됩니다. 따라서 상위 구성과 상위 스냅샷은 활성 상태로 유지하면서 하위 구성이 사라질 때 중첩된 스냅샷을 삭제할 수 있습니다. 중첩된 스냅샷에 대한 모든 변경 사항은 상위 스냅샷에 전파됩니다.

모든 스냅샷 유형은 중첩된 스냅샷을 만들어 상위 스냅샷에 연결하는 기능을 제공합니다(예: Snapshot#takeNestedSnapshot() 또는 MutableSnapshot#takeNestedMutableSnapshot()).

하위 읽기 전용 스냅샷은 모든 스냅샷 유형에서 생성될 수 있습니다. 변경 가능한 스냅샷은 변경 가능한 다른 스냅샷(또는 변경 가능한 스냅샷으로 간주될 수도 있는 전역 스냅샷)에서만 생성할 수 있습니다.

스냅샷 및 스레드

스냅샷을 스레드 범위와 독립적인 구조로 생각하는 것이 중요합니다. 스레드는 실제로 현재 스냅샷을 가질 수 있지만 스냅샷이 반드시 스레드에 바인딩되는 것은 아닙니다. 스레드는 마음대로 스냅샷에 들어가고 나갈 수 있으며, 다른 스레드는 하위 스냅샷에 들어갈 수 있습니다. 실제로 스냅샷의 의도된 사용 사례 중 하나는 병렬 작업입니다. 여러 하위 스레드가 생성될 수 있으며 각각 자체 스냅샷이 있습니다.

변경 가능한 스냅샷을 정의한 후에는 일관성을 유지하기 위해 하위 스냅샷이 상위 스냅샷에 변경 사항을 알리는 방법도 알아봅니다. 모든 스레드의 변경 사항은 서로 격리되며, 서로 다른 스레드에서 충돌하는 업데이트가 감지되고 처리됩니다. 중첩된 스냅샷을 사용하면 이러한 작업 분해가 재귀적으로 수행될 수 있습니다. 이러한 모든 기능은 잠재적으로 병렬 콤보와 같은 기능을 잠금 해제합니다.

현재 스레드의 스냅샷은 Snapshot.current를 통해 얻을 수 있습니다. 현재 스레드의 스냅샷이 있으면 반환하고, 그렇지 않으면 전역 스냅샷을 반환합니다(전역 상태 저장).

Compose 런타임에는 작성된 상태를 관찰할 때 재구성을 트리거하는 기능이 있습니다. 이 메커니즘이 앞서 설명한 상태 스냅샷 시스템과 어떻게 인터페이스하는지 살펴보는 것이 도움이 될 것입니다. 먼저 읽기를 관찰하는 방법을 배우는 것부터 시작해 보겠습니다.

관찰 읽기 및 쓰기

Compose 런타임에는 관찰된 상태가 기록될 때 재구성을 트리거하는 기능이 있습니다.

스냅샷을 찍을 때마다(예: Snapshot.takeSnapshot()) ReadonlySnapshot을 다시 가져옵니다. 이 스냅샷의 상태 개체는 수정할 수 없고 읽기만 가능하므로 스냅샷의 모든 상태는 삭제될 때까지 보존됩니다. takeSnapshot 함수의 람다를 사용하면 Enter 호출의 스냅샷에서 상태 객체를 읽을 때마다 알림을 받는 readObserver(선택적 매개변수) 관찰자를 전달할 수 있습니다.

readObserver: fun snapshotFlow(block: () -> T): Flow를 사용하는 예로 snapshotFlow 함수를 사용할 수 있습니다. 이 함수는 State 객체를 Flow로 변환합니다. Flow가 수집되면 해당 블록을 실행하고 읽은 State 개체의 결과를 내보냅니다. State 개체 중 하나가 수정되면 Flow는 새 값을 수집기에 내보냅니다. 이 동작을 달성하려면 이러한 상태 객체가 변경될 때 블록이 다시 실행되도록 모든 상태 읽기를 기록해야 합니다. 이러한 읽기를 추적하기 위해 읽기 전용 스냅샷을 만들고 읽기 관찰자를 전달하여 이를 Set에 저장합니다.

// SnapshotFlow.kt
fun <T> snapshotFlow(block: () -> T): Flow<T> = flow { 
    val readSet = mutableSetOf<Any>()
    val readObserver: (Any) -> Unit = { readSet.add(it) }
    // ...
    Snapshot.takeSnapshot(readObserver) 
    // ...
    // Do something with the Set
}

읽기 전용 스냅샷은 일부 상태를 읽을 때 readObserver에 알릴 뿐만 아니라 부모의 readObserver에도 알립니다. 중첩된 스냅샷에 대한 읽기는 모든 상위 및 관찰자에게 표시되어야 하므로 스냅샷 트리의 모든 관찰자에게 알림이 전달됩니다.

이제 쓰기 작업을 관찰해 보겠습니다.

쓰기 관찰도 가능하므로 변경 가능한 스냅샷을 생성할 때만 writeObserver(상태 업데이트)를 전달할 수 있습니다. 변경 가능한 스냅샷은 보유한 상태를 수정할 수 있는 스냅샷입니다. Snapshot.takeMutableSnapshot()을 호출하여 변경 가능한 스냅샷을 만들 수 있습니다. 여기서는 읽기 또는 쓰기에 대한 알림을 받기 위해 선택적 읽기 및 쓰기 감시자를 전달할 수 있습니다.

읽기 및 쓰기 관찰의 좋은 예는 Recomposer입니다. 이는 컴포지션의 모든 읽기 및 쓰기를 추적하여 필요할 때 자동으로 재구성을 트리거할 수 있습니다.

합성 함수는 초기 컴포지션(Composition)을 생성할 때와 재구성될 때마다 호출됩니다. 이 논리는 상태를 읽고 쓸 수 있도록 허용하는 MutableSnapshot을 사용하며 Enter로 호출된 블록의 모든 읽기 또는 쓰기는 컴포지션에 통보됩니다. (즉, 변경 가능한 상태의 읽기 및 쓰기를 구성별로 추적할 수 있습니다)

여기서 매개변수로 전달된 블록 코드 블록은 실제로 구성되거나 재구성된 코드를 실행하고 있으므로 트리에 있는 모든 구성 가능한 기능을 실행하여 변경 목록을 계산합니다. 그리고 이러한 작업은 Enter 함수 내에서 발생하므로 모든 읽기 또는 쓰기 작업이 자동으로 추적됩니다.

스냅샷 상태 쓰기가 구성으로 추적될 때마다 정확히 동일한 스냅샷 상태를 읽는 해당 RecomposeScope가 무효화되고 재구성을 트리거합니다.

구성이 끝나면 applyAndCheck(snapshot) 호출이 구성 중에 발생한 모든 변경 사항을 다른 스냅샷과 전역 상태에 전파합니다.

코드에서 관찰자는 다음과 같습니다. 이는 간단한 함수입니다.

private fun readObserverOf(composition: ControlledComposition): (Any) -> Unit {
    return { value -> composition.recordReadOf(value) }
}

private fun writeObserverOf(composition: ControlledComposition, modifiedValues: IdentityArraySet<Any>?): (Any) -> Unit {
    return { value ->
        composition.recordWriteOf(value)
        modifiedValues?.add(value)
    }
}

현재 스레드에서 읽기 및 쓰기를 관찰하는 데 사용할 수 있는 몇 가지 유용한 함수가 있습니다. Snapshot.observe(readObserver, writeObserver, block) 함수입니다. 예를 들어,derivedStateOf 함수는 이를 사용하여 제공된 블록의 모든 객체 읽기에 응답합니다.

Snapshot.observe()는 TransparentObserverMutableSnapshot이 사용되는 유일한 장소입니다. 이 유형의 상위(루트) 스냅샷을 생성하는 유일한 목적은 앞에서 설명한 대로 관찰자에게 읽기를 알리는 것입니다. Comose 팀은 일부 특별한 경우에 스냅샷에 콜백 목록이 생성되는 것을 방지하기 위해 이 유형을 추가했습니다.

가변스냅샷

MutableSnapshot은 변경 가능한 스냅샷 상태를 처리할 때 사용되는 스냅샷 유형으로, 재구성을 자동으로 트리거하기 위해 쓰기를 추적해야 합니다.

변경 가능한 스냅샷에서 모든 상태 객체는 스냅샷 내에서 로컬로 상태 객체를 수정하지 않는 한 스냅샷을 생성했을 때와 동일한 값을 갖습니다. MutableSnapshot의 모든 변경 사항은 다른 스냅샷의 변경 사항과 격리됩니다. 변경 사항은 트리 아래쪽에서 위쪽으로 전파됩니다. 하위 중첩 변경 가능 스냅샷은 변경 사항을 먼저 적용한 다음 상위 스냅샷이나 전역 스냅샷(트리의 루트인 경우)에 전파해야 합니다. 이는 NestedMutableSnapshot#apply를 호출하여 수행됩니다. (또는 중첩되지 않은 경우 MutableSnapshot#apply)

다음 구절은 Jetpack Compose 런타임의 kdoc에서 직접 가져온 것입니다.

컴포지션은 변경 가능한 스냅샷을 사용하여 컴포저블 함수의 변경 사항을 전역 상태에서 일시적으로 격리하고 나중에 컴포지션이 적용될 때 전역 상태에 적용되도록 합니다. MutableSnapshot.apply가 이 스냅샷을 적용하지 못하면 구성 중에 계산된 스냅샷과 변경 사항이 삭제되고 새 구성이 다시 계산되도록 예약됩니다.

(번역: 컴포지션은 변경 가능한 스냅샷을 사용하므로 컴포저블 함수의 변경 사항은 일정 기간 동안 전역 상태에서 격리되었다가 나중에 컴포지션이 적용될 때 전역 상태에 적용됩니다. MutableSnapshot.apply가 이 스냅샷을 적용하지 못하면 스냅샷은 구성 중에 계산된 변경 사항은 삭제되고 새 구성이 다시 계산될 예정입니다.)

따라서 구성이 적용되면(요약하자면 구성의 마지막 단계로 Applier를 통해 변경 사항을 적용함) 변경 가능한 스냅샷의 모든 변경 사항이 적용되고 상위 스냅샷 또는 최종 전역 스냅샷에 통보됩니다. 변경사항을 적용하는 중에 오류가 발생하면 새 조합이 예약됩니다.

변경 가능한 스냅샷에도 수명이 있습니다. 항상 Apply 및 Dispose를 호출하여 끝납니다. 이는 상태 수정 사항을 다른 스냅샷에 전파하고 누출을 방지하는 데 필요합니다.

적용을 통해 전파된 변경 사항은 원자적으로 적용됩니다. 즉, 전역 상태 또는 상위 스냅샷(중첩된 경우)은 이러한 모든 변경 사항을 단일 원자 변경으로 처리합니다. 이렇게 하면 상태 변경 기록이 정리되어 식별, 재현, 중단 또는 되돌리기가 더 쉬워집니다. 이것이 앞서 동시성 제어 시스템 섹션에서 설명한 트랜잭션 메모리의 역할입니다.

변경 가능한 스냅샷을 삭제했지만 적용하지 않은 경우 모든 미해결 상태 변경 사항이 삭제됩니다.

다음은 클라이언트 코드에서 적용을 사용하는 방법을 보여주는 실제 예입니다.

enter 호출 내부에서 인쇄하면 값이 "Another street"이므로 수정 사항이 표시됩니다. 이는 스냅샷 컨텍스트에서 실행 중이기 때문입니다. 그러나 enter 호출 직후 (외부)를 인쇄하면 값이 원래 값으로 되돌아간 것 같습니다. 이는 MutableSnapshot의 변경 사항이 다른 스냅샷과 격리되기 때문입니다. Apply가 호출된 후 변경 사항이 전파되고 마침내 수정된 값으로 거리 이름이 다시 인쇄되는 것을 볼 수 있습니다.

Enter 호출 내에서 수행된 상태 업데이트만 추적되고 전파됩니다.

구문의 또 다른 단순화된 버전이 있습니다: Snapshot.withMutableSnapshot . 이는 적용이 마지막으로 호출되도록 암시적으로 보장합니다.

Apply가 마지막에 호출되는 방식은 Composer가 변경 목록을 기록하고 적용하는 방법을 상기시켜 줍니다. 이번에도 같은 개념입니다. 트리의 변경 사항 목록을 이해해야 할 때마다 해당 변경 사항을 기록/지연하여 올바른 순서로 적용(트리거)하고 그 순간 일관성을 적용할 수 있어야 합니다. 이는 프로그램이 모든 변경 사항을 인식하는 유일한 시간입니다. 즉, 전역적인 관점을 갖게 되는 순간입니다.

최종 수정 변경 사항을 관찰하기 위해 애플리케이션 관찰자를 등록하는 것도 가능합니다. 이는 Snapshot.registerApplyObserver를 호출하여 달성할 수 있습니다.

글로벌스냅샷과 중첩된 스냅샷

GlobalSnapshot은 전역 상태를 유지하는 변경 가능한 스냅샷입니다. 위에서 설명한 상향식 순서로 다른 스냅샷에서 업데이트를 가져옵니다.

GlobalSnapshot은 중첩될 수 없습니다. GlobalSnapshot은 하나만 있기 때문에 실제로는 모든 스냅샷의 최종 루트입니다. 현재 전역(공유) 상태를 유지합니다. 따라서 글로벌 스냅샷을 적용할 수 없습니다(적용 호출이 없음).

글로벌 스냅샷에 변경 사항을 적용하려면 "고급"이어야 합니다. 이는 이전 전역 스냅샷을 지우고 이전 전역 스냅샷의 유효한 상태를 모두 받아들이는 새 스냅샷을 생성하는 Snapshot.advanceGlobalSnapshot()을 호출하여 수행됩니다. 이 경우 메커니즘이 다른 경우에도 변경사항이 효과적으로 "적용"되므로 Apply 관찰자에게도 알림이 전달됩니다. 또한 dispose()를 호출하는 것도 불가능합니다. 글로벌 스냅샷 삭제는 "고급" 방식으로도 수행할 수 있습니다.

Jetpack Compose에서는 스냅샷 시스템을 초기화하는 동안 전역 스냅샷이 생성됩니다. JVM에서 이는 SnapshotKt.class가 Java 또는 Android 런타임에 의해 초기화될 때 발생합니다.

그 후 Composer가 생성될 때 전역 스냅샷 관리자가 시작되고 각 구성(초기 구성 및 추가 재구성 모두)은 자체 중첩된 변경 가능한 스냅샷을 생성하고 이를 트리에 추가하므로 다음과 결합된 모든 상태를 저장할 수 있습니다. 격리. 또한 컴포지션은 이 기회를 통해 읽기 및 쓰기 관찰자를 등록하여 컴포지션에 대한 읽기 및 쓰기를 추적합니다. 앞서 소개한 구성 기능을 기억하세요.

마지막으로 모든 하위 컴포지션은 자체 중첩된 스냅샷을 만들고 이를 트리에 추가하여 상위 요소를 활성 상태로 유지하면서 무효화를 지원할 수 있습니다. 이를 통해 스냅샷 트리의 완전한 청사진이 제공됩니다.

또 다른 흥미로운 세부정보는 Composer가 생성될 때 Composition이 생성될 때 GlobalSnapshotManager.ensureStarted()가 호출된다는 것입니다. 이는 전역 상태에 대한 모든 쓰기를 관찰하기 시작하고 AndroidUiDispatcher.Main 컨텍스트에서 스냅샷 앱 알림을 주기적으로 전달하는 플랫폼(Compose UI)과의 통합의 일부입니다.

StateObject 및 StateRecord

다중 버전 동시성 제어는 상태가 기록될 때마다 새 버전이 생성되도록 보장합니다(기록 중 복사). Jetpack 복합 상태 스냅샷 시스템은 이를 따르므로 동일한 스냅샷 상태 개체의 여러 버전을 저장하게 될 수 있습니다.

이 디자인은 성능에 세 가지 중요한 의미를 갖습니다.

  • 첫째, 스냅샷 생성 비용은 O(N)(여기서 N은 상태 개체 수)이 아니라 O(1) 복잡도입니다.

  • 둘째, 스냅샷 커밋 비용은 O(N) 복잡성입니다. 여기서 N은 스냅샷에 있는 변경된 객체의 수입니다.

  • 셋째, 스냅샷 자체는 스냅샷 데이터 목록(수정된 개체의 임시 목록만)을 보유하지 않으므로 스냅샷 시스템에 알리지 않고 상태 개체를 자유롭게 GC(가비지 수집)할 수 있습니다.

내부적으로 스냅샷 상태 객체는 StateObject로 모델링되며, 다중 버전에서는 객체의 각 버전에 대한 저장 형식이 StateRecord입니다. 각 레코드는 단일 버전의 상태에 대한 데이터를 보유합니다. 각 스냅샷에 표시되는 버전(레코드)은 스냅샷을 생성할 당시 사용 가능한 최신 유효한 버전에 해당합니다. (가장 높은 스냅샷 ID를 가진 유효한 스냅샷)

그러나 주 기록을 효과적으로 만드는 방법은 무엇입니까?

"효과적"은 특정 스냅샷에서만 의미가 있습니다. 레코드는 해당 레코드를 생성한 스냅샷 ID와 연결됩니다. 스냅샷의 상태 레코드는 다음 조건이 충족될 때 유효합니다. 상태 레코드의 ID가 스냅샷 ID보다 작거나 같거나(즉, 현재 또는 이전 스냅샷에서 생성된) 스냅샷에 속하지 않는 경우 유효하지 않은 스냅샷 세트도 유효하지 않은 것으로 명시적으로 표시되지도 않습니다. 이전 스냅샷의 유효한 레코드는 자동으로 새 스냅샷에 복사됩니다.

이는 다음과 같은 질문을 던집니다. 언급된 유효하지 않은 세트의 레코드 부분을 만들거나 명시적으로 유효하지 않은 것으로 표시하는 것은 무엇입니까?

  • 현재 스냅샷 이후에 생성된 레코드는 이 스냅샷 이후에 생성된 스냅샷에 대해 생성되었기 때문에 유효하지 않은 것으로 간주됩니다.

  • 현재 스냅샷이 생성될 때 해당 스냅샷에 대해 생성된 레코드가 켜져 있으면 해당 레코드도 무효화 세트에 추가되므로 해당 레코드도 유효하지 않은 것으로 간주됩니다.

  • 적용되기 전에 폐기된 스냅샷에서 생성된 레코드는 유효하지 않은 것으로 명시적으로 표시됩니다.

잘못된 레코드는 스냅샷에 표시되지 않으므로 읽을 수 없습니다. 구성 가능한 함수에서 스냅샷 상태를 읽을 때 레코드는 고려되지 않고 대신 최신 유효한 상태가 반환됩니다.

상태 개체로 돌아갑니다. 다음은 상태 스냅샷 시스템에서 모델링되는 방법에 대한 간단한 예입니다.

어떤 방법으로든 생성된 변경 가능한 스냅샷 상태 개체는 이 인터페이스를 구현합니다. 예를 들어 mutableStateOf, mutableStateListOf 또는 파생된StateOf 런타임 함수 등이 반환한 상태입니다.

mutableStateOf(value) 함수를 살펴보겠습니다.

이 호출은 본질적으로 관찰 가능한 변경 가능 상태인 SnapshotMutableState의 인스턴스를 반환합니다. 즉, 업데이트할 수 있고 관찰자에게 상태를 자동으로 알리는 상태입니다. 이 클래스는 StateObject이므로 다양한 버전의 상태(이 경우 값)를 저장하는 레코드의 연결된 목록을 유지 관리합니다. 상태를 읽을 때마다 레코드 목록을 탐색하여 가장 최근의 유효한 레코드를 찾아서 반환합니다.

StateObject의 정의를 다시 살펴보면 연결된 레코드 목록의 첫 번째 요소에 대한 포인터가 있고 각 레코드가 다음 항목을 가리키는 것을 볼 수 있습니다. 또한 목록에 새 레코드를 추가할 수도 있습니다(새로운 firstStateRecord로 만들기).

StateObject 정의의 또 다른 기능은 mergeRecords입니다. 앞서 시스템이 가능한 경우 충돌을 자동으로 병합할 수 있다고 언급했습니다. 이것이 바로 이 기능이 하는 일입니다. 병합 전략은 간단하며 나중에 자세히 설명합니다.

StateRecord를 조금 이해해 봅시다

여기서는 각 레코드가 스냅샷 ID와 연결되어 있음을 볼 수 있습니다. 이 ID는 레코드를 생성한 스냅샷에 속한 ID입니다. 이는 위의 요구 사항에 따라 특정 스냅샷에 대해 레코드가 유효한지 여부를 결정합니다.

객체를 읽을 때마다 주어진 스냅샷 상태(StateObject)에 대한 StateRecords 목록을 순회하면서 유효한 최신 레코드(가장 높은 스냅샷 ID를 가진)를 찾습니다. 마찬가지로 각 스냅샷 상태 객체의 유효한 최신 상태는 스냅샷이 생성될 때 캡처되며 이는 새 스냅샷의 전체 수명 동안 사용되는 상태가 됩니다(변경 가능한 스냅샷이고 상태가 로컬에서 수정되지 않는 한).

StateRecord에는 다른 StateRecord 개체에서 이를 할당하고 생성하는 할당 기능도 있습니다.

StateRecord는 계약(인터페이스)이기도 합니다. 각 기존 StateObject 유형은 레코드가 각 유형(각 사용 사례)마다 다른 각 StateObject 유형에 대한 정보를 저장하기 때문에 서로 다른 구현을 정의합니다.

mutableStateOf의 예를 따르면 StateObject인 SnapshotMutableState를 반환한다는 것을 알 수 있습니다. 이는 매우 구체적인 유형인 StateStateRecord의 연결된 레코드 목록을 유지 관리합니다. 이 레코드는 T 유형의 값을 둘러싼 래퍼일 뿐입니다. 왜냐하면 이 경우에는 이것이 각 레코드에 저장해야 하는 모든 정보이기 때문입니다.

또 다른 좋은 예는 mutableStateListOf일 수 있습니다. StateObject의 또 다른 구현인 SnapshotStateList를 생성합니다. 상태는 관찰 가능한 변경 가능 목록을 시뮬레이션하므로(Kotlin 컬렉션의 MutableList 계약 구현) 해당 레코드는 자체적으로 정의된 StateListStateRecord 유형을 갖게 됩니다. 이 레코드는 상태 목록 버전을 보유하기 위해 PertantList(Kotlin Immutable Collections 참조)를 사용합니다.

읽기 및 쓰기 상태

즉, 상태 레코드를 읽고 씁니다. "개체를 읽을 때 지정된 스냅샷 상태(StateObject)에 대한 StateRecords 목록을 순회하여 가장 최근에 유효한 레코드(가장 높은 스냅샷 ID 포함)를 찾습니다." 이것이 코드에서 어떻게 구현되는지 살펴보겠습니다.

이는 compose.material 라이브러리의 TextField Composable 구성요소입니다. 텍스트 값을 유지하기 위한 변경 가능한 상태를 기억하므로 값이 업데이트될 때마다 컴포저블이 화면에 새 문자를 표시하도록 재구성됩니다.

지금은 기억하라는 부르심을 고려하지 않겠습니다. 왜냐하면 그것이 여기서 논의의 초점이 아니기 때문입니다. 여기서는 mutableStateOf 함수를 사용하여 스냅샷 상태를 생성합니다.

그러면 T 값과 SnapshotMutationPolicy를 매개변수로 가져오는 SnapshotMutationState 상태 객체가 생성됩니다. (메모리에 저장된) 값을 래핑하고 업데이트해야 할 때 충돌 전략을 사용하여 전달된 새 값이 현재 값과 다른지 확인합니다. 다음은 이 클래스의 value 속성에 대한 정의입니다.

getter를 사용하여 TextField 구성 가능한 내부 값(예: textFieldValueState.value)에 액세스할 때마다 다음 상태 레코드(연결된 목록의 첫 번째 레코드) 옆에 있는 참조를 사용하여 읽기 가능한 메서드를 호출하여 반복을 시작합니다. 읽기 가능 함수는 현재(최신) 유효한 읽기 가능 상태를 찾기 위해 반복하여 등록된 읽기 관찰자에게 알립니다. 각각의 새로운 반복 항목에 대해 이전 섹션에서 정의된 유효한 조건과 비교하여 확인됩니다. 현재 스냅샷은 현재 스레드의 스냅샷이 되며, 현재 스레드가 스냅샷과 연결되지 않은 경우 전역 스냅샷이 됩니다.

/**
 * Return the current readable state record for the current snapshot. 
 * It is assumed that [this] is the first record of [state]
 */
fun <T : StateRecord> T.readable(state: StateObject): T {
    val snapshot = Snapshot.current
    snapshot.readObserver?.invoke(state)
    return readable(this, snapshot.id, snapshot.invalid) ?: sync { 
        val syncSnapshot = Snapshot.current
        readable(this, syncSnapshot.id, syncSnapshot.invalid)
    } ?: readError()
}

이것이 mutableStateOf의 스냅샷 상태를 읽는 방법입니다. 상황은 mutableStateListOf에서 반환된 것과 같은 사용 가능한 다른 변경 가능한 스냅샷 상태 구현과 유사합니다.

상태를 업데이트하려면 상태의 setter 메서드를 사용하면 됩니다. 샘플 코드는 다음과 같습니다.

withCurrent 함수는 내부적으로 읽기 가능한 함수를 호출하여 제공된 코드 블록을 실행하고 현재 읽을 수 있는 최신 상태 레코드를 매개변수로 전달합니다.

다음으로 제공된 SnapshotMutationPolicy를 사용하여 새 값이 현재 값과 동일한지 확인합니다. 동일하지 않으면 쓰기 프로세스가 시작됩니다. 이 작업은 덮어쓰기 가능 기능에 의해 수행됩니다.

구현 세부 사항은 향후 변경될 수 있으므로 여기서는 의도적으로 다루지 않겠습니다. 하지만 간단히 설명하자면, 쓰기 가능한 상태 레코드로 블록을 실행하고 현재 유효한 최신 레코드가 될 후보 레코드를 제안합니다. 현재 스냅샷에 유효하면 쓰기에 사용되고, 그렇지 않으면 새 레코드를 생성하여 목록의 선두에 추가하여 새 초기 레코드로 만듭니다. 이 블록은 실제로 이를 수정합니다.

마지막으로 등록된 모든 쓰기 관찰자에게 알립니다.

오래된 기록 삭제 또는 재사용

다중 버전 동시성 제어를 사용하면 동일한 상태의 여러 버전(레코드)을 저장할 수 있지만 이로 인해 더 이상 사용되지 않고 절대 읽을 수 없는 버전을 제거해야 한다는 흥미로운 과제가 발생합니다. 잠시 후에 Compose가 이 문제를 어떻게 해결하는지 설명하겠지만 먼저 '오픈 스냅샷'의 개념을 소개하겠습니다.

새로운 스냅샷은 적극적으로 닫힐 때까지 열려 있는 스냅샷 컬렉션에 추가됩니다. 스냅샷이 열려 있는 동안 해당 상태 레코드는 모두 다른 스냅샷에 대해 유효하지 않은(읽을 수 없는) 것으로 간주됩니다. 스냅샷을 닫는다는 것은 생성된 모든 새 스냅샷에 대해 모든 레코드가 자동으로 유효(읽기)된다는 의미입니다.

  1. 이를 이해한 후 Compose가 오래된 레코드를 어떻게 회수하는지 살펴보겠습니다.

  2. 가장 낮은 오픈 스냅을 추적합니다. Compose는 공개 스냅샷 ID 집합을 추적합니다. 이러한 ID는 단조롭게 증가하고 계속 증가합니다.

레코드가 유효하지만 가장 낮은 열린 스냅샷에 표시되지 않는 경우 다른 스냅샷에서 선택되지 않으므로 안전하게 재사용할 수 있습니다.

덮어쓴 레코드를 재사용하면 변경 가능한 상태 개체에 레코드가 1~2개만 생성되어 성능이 크게 향상되는 경우가 많습니다. 스냅샷이 적용되면 덮어쓴 기록은 다음 스냅샷에서 재사용됩니다. 스냅샷이 적용되기 전에 폐기되면 모든 기록이 유효하지 않은(폐기됨) 것으로 표시되어 즉시 재사용할 수 있습니다.

변경 전파

변경 가능한 스냅샷의 변경 사항이 어떻게 전파되는지 설명하기 전에 스냅샷 "닫기"와 "전진"의 의미를 검토하여 두 용어를 모두 이해하는 것이 도움이 될 수 있습니다.

스냅샷을 닫으면 열려 있는 스냅샷 ID 집합에서 해당 ID가 효과적으로 제거되며, 결과적으로 해당 ID와 연결된 모든 상태 레코드(레코드)가 생성된 새 스냅샷에서 읽을 수 있도록 표시/읽기 가능해집니다. 이렇게 하면 스냅샷을 끄는 것이 상태 변경 사항을 전파하는 효율적인 방법이 됩니다.

스냅샷을 닫을 때 이를 대체할 새 스냅샷을 즉시 생성하고 싶은 경우가 많습니다. 이것을 "진행"이라고 합니다. 새로 생성된 스냅샷은 이전 ID를 증가시켜 생성된 새 ID를 얻습니다. 그런 다음 이 ID는 열린 스냅샷 ID 컬렉션에 추가됩니다.

우리가 배운 대로 글로벌 스냅샷은 적용되지 않고 항상 고급화되어 새로 생성된 글로벌 스냅샷에 모든 변경 사항이 표시됩니다. 중첩된 스냅샷이 변경 사항을 적용하는 동안 변경 가능한 스냅샷도 발전할 수 있습니다.

이제 이를 잘 이해했으므로 변경 가능한 스냅샷의 변경 사항이 어떻게 전파되는지 알아볼 준비가 되었습니다.

변경 가능한 스냅샷에서 snapshot.apply()가 호출되면 해당 범위 내에서 상태 객체에 대한 모든 로컬 변경 사항이 상위 스냅샷(중첩된 변경 가능한 스냅샷의 경우) 또는 전역 상태로 전파됩니다.

Apply 또는 Dispose를 호출하면 스냅샷의 수명 주기가 정의됩니다. 애플리케이션의 변경 가능한 스냅샷은 나중에 해제할 수도 있습니다. 그러나 dispose 후에 Apply를 호출하면 변경 사항이 이미 삭제되었기 때문에 예외가 발생합니다.

위에서 설명한 내용에 따르면 모든 로컬 변경 사항(촬영된 새 스냅샷에 표시)을 전파하려면 활성 스냅샷 세트에서 스냅샷을 삭제하는 것만으로도 충분합니다. 스냅샷이 생성될 때마다 현재 열려 있는 스냅샷의 복사본이 잘못된 스냅샷 세트로 전달됩니다(즉, 아직 적용되지 않은 스냅샷은 새 스냅샷에 표시되지 않아야 합니다). 단순히 공개 스냅샷 세트에서 스냅샷 ID를 제거하는 것만으로도 각각의 새로운 스냅샷이 이 스냅샷 중에 생성된 기록을 유효한 것으로 간주하기에 충분하므로 해당 상태 객체를 읽을 때 반환될 수 있습니다.

하지만 먼저 해결해야 하는 상태 충돌(충돌 쓰기)이 없다고 확신한 후에만 이 작업을 수행해야 합니다.

스냅샷이 적용되면 적용된 스냅샷의 변경 사항이 다른 스냅샷의 변경 사항과 함께 추가됩니다. 상태 개체에는 모든 변경 사항이 집계되는 연결된 레코드 목록이 있습니다. 여러 스냅샷이 동일한 상태 개체에 변경 사항을 적용하려고 시도할 수 있으므로 쓰기 충돌이 발생할 수 있습니다. 변경 가능한 스냅샷이 로컬 변경 사항을 적용(알림/전파)하려고 할 때 잠재적인 쓰기 충돌을 감지하고 가능하면 해당 충돌을 병합하려고 시도합니다.

여기에는 두 가지 시나리오가 있습니다.

  • 보류 중인 로컬 변경 사항이 없습니다.

스냅샷에 보류 중인 로컬 변경 사항이 없는 경우:

  • 변경 가능한 스냅샷은 능동적으로 닫힙니다(열린 스냅샷 ID 집합에서 이를 제거하여 모든 상태 기록을 새 스냅샷에 자동으로 표시/읽을 수 있게 만듭니다).

  • 글로벌 스냅샷은 "고급"입니다(종료와 동일하지만 생성된 새로운 글로벌 스냅샷으로 대체됨).

  • 이 기회를 활용하여 전역 스냅샷의 상태 변경을 확인하면 변경 가능한 스냅샷이 잠재적인 애플리케이션 관찰자에게 이러한 변경 사항을 알릴 수 있습니다.

  • 보류 중인 로컬 변경사항이 있습니다.

보류 중인 변경사항이 있는 경우:

  • 낙관적인 접근 방식을 사용하여 충돌을 감지하고 병합된 레코드 수를 계산합니다(동시성 제어 범주 기억). 충돌은 자동으로 병합을 시도하며, 그렇지 않으면 삭제됩니다.

  • 보류 중인 각 로컬 변경 사항에 대해 현재 값과 다른지 확인합니다. 그렇지 않은 경우 변경 사항이 무시되고 현재 값이 유지됩니다.

  • 실제 변경(다른)인 경우 이미 계산된 낙관적 병합을 확인하여 이전, 현재 또는 적용된 레코드를 유지할지 여부를 결정합니다. 실제로 이 모든 것의 융합을 만들 수 있습니다.

  • 레코드 병합을 수행해야 하는 경우 새 레코드(불변성)를 생성하고 여기에 스냅샷 ID를 할당한 다음(변경 가능한 스냅샷과 연결) 레코드의 연결 목록 앞에 추가하여 효과적으로 레코드가 되도록 만듭니다. 목록의 첫 번째 레코드입니다.

변경 사항을 적용하는 데 실패하는 경우 보류 중인 로컬 변경 사항이 없는 경우와 동일한 흐름으로 돌아갑니다. 이는 변경 가능한 스냅샷을 닫아 레코드가 새 스냅샷에 표시되도록 하고 전역 스냅샷을 진행하여(닫고 새 스냅샷으로 교체) 방금 닫은 변경 가능한 스냅샷의 모든 변경 사항을 포함하고 감지된 적용 관찰자에게 모든 변경 사항을 알립니다. 전역 상태가 변경됩니다.

중첩된 변경 가능한 스냅샷의 프로세스는 변경 사항을 전역 스냅샷이 아닌 상위 스냅샷에 전파하므로 프로세스가 약간 다릅니다. 이러한 이유로 모든 수정된 상태 개체를 상위 개체의 수정된 세트에 추가합니다. 이러한 모든 변경 사항은 상위 스냅샷에서 볼 수 있어야 하므로 중첩된 변경 가능한 스냅샷은 유효하지 않은 스냅샷의 상위 스냅샷 세트에서 자체 ID를 제거합니다.

병합 쓰기 충돌

병합을 수행하기 위해 변경 가능한 스냅샷은 수정된 상태 목록(로컬 변경 사항)을 반복하고 각 변경 사항에 대해 다음을 수행합니다.

  • 상위 스냅샷 또는 전역 상태에서 현재 값(상태 레코드)을 가져옵니다.

  • 변경 사항을 적용하기 전에 이전 값을 가져옵니다.

  • 변경 사항을 적용한 후 개체의 상태를 가져옵니다.

  • 세 가지를 모두 자동으로 병합해 보세요. 이는 제공된 병합 전략에 의존하는 상태 개체에 위임됩니다(위의 StateObject 정의 참조).

사실 런타임에서 사용 가능한 전략 중 어느 것도 올바른 병합을 지원하지 않으므로 업데이트를 범핑하면 런타임 예외가 발생하고 사용자에게 문제를 알립니다. 이러한 상황이 발생하지 않도록 Compose는 고유 키로 상태 객체에 액세스하여 충돌이 불가능함을 보장합니다(구성 가능한 함수에서 기억된 상태 객체에는 일반적으로 고유한 액세스 속성이 있음). mutableStateOf는 등호(==)를 통해 객체의 두 버전을 심층적으로 비교하는 StructuralEqualityPolicy를 사용하여 병합되므로 고유한 객체 키를 포함한 모든 속성이 비교되어 두 객체가 충돌하는 것이 불가능해집니다.

충돌하는 변경사항을 자동으로 병합하는 것은 Compose에서는 아직 사용하지 않지만 다른 라이브러리에서는 사용할 수 있는 잠재적인 최적화입니다.

SnapshotMutationPolicy 인터페이스를 구현하여 사용자 정의 충돌 정책을 제공할 수 있습니다. 참조 전략으로 사용할 수 있는 Compose 문서의 예는 MutableState를 카운터로 사용하는 것입니다. 이 정책은 상태 값을 동일하게 변경하는 것이 변경으로 간주되지 않는다고 가정하므로 counterPolicy를 사용하여 변경 가능한 상태를 변경해도 애플리케이션 충돌이 발생하지 않습니다.

두 값이 동일하면 동일한 것으로 간주되므로 현재 값이 유지됩니다. 새로 적용된 값과 이전 값의 차이를 현재 값에 추가하여 병합을 얻는 방법에 유의하세요. 그러면 현재 값은 항상 저장된 총계를 반영합니다.

이 단락은 공식 문서의 설명입니다: *정책 이름에서 알 수 있듯이 스냅샷에서 소비되거나 생산된 리소스의 양을 추적하는 등 사물을 계산할 때 유용할 수 있습니다. 10개, 스냅샷 B는 20개를 생성하는데, A와 B를 모두 적용한 결과는 30개가 생성되어야 합니다.* (정책 이름에서 알 수 있듯이, 스냅샷 B에서 소비되거나 생산된 것이 무엇인지 추적하는 등의 계산에 유용합니다. 스냅샷 리소스 수 예를 들어 스냅샷 A가 10개의 아티팩트를 생성하고 스냅샷 B가 20개의 아티팩트를 생성했다면 A와 B를 모두 적용한 결과는 30개의 아티팩트가 되어야 합니다.)

카운터 전략을 사용하여 비교되는 단일 변경 가능한 상태와 이를 수정하고 변경 사항을 적용하려고 시도하는 두 개의 스냅샷이 있습니다. 이는 충돌에 대한 완벽한 시나리오이지만, 우리의 대응 전략에 따라 모든 충돌은 완전히 방지됩니다.

이는 충돌을 피하기 위해 사용자 정의 SnapshotMutationPolicy를 제공하여 요점을 파악하는 방법에 대한 간단한 예일 뿐입니다. 충돌이 불가능한 또 다른 구현은 요소를 추가할 수만 있고 제거할 수는 없는 컬렉션일 수 있습니다. 로프와 같은 다른 유용한 유형은 작동 방식과 예상되는 내용에 특정 제한이 있는 경우 충돌하지 않는 데이터 유형으로 유사하게 변환될 수 있습니다.

충돌을 허용하지만 병합 기능을 사용하여 데이터를 병합하여 충돌을 해결하는 사용자 지정 전략을 제공할 수도 있습니다.

요약하다

스냅샷 상태의 개념은 상태 격리와 스냅샷 격리입니다.

MVCC(다중 버전 동시성 제어) 구현을 기반으로 합니다 .

  • 격리된 로컬 복사본을 사용하여 특정 순간에 각 스레드가 작동하도록 데이터의 여러 복사본/스냅샷을 유지합니다.

  • 각 스레드는 서로 다른 스냅샷을 가리키므로 상태의 서로 다른 복사본도 가리킵니다.

  • 데이터가 기록될 때마다 수정되는 원본 데이터가 아닌 새로운 데이터 복사본이 생성됩니다.

  • 동일한 데이터의 여러 버전 또는 기록 레코드는 Compose에서 StateRecord라고 하는 메모리에 저장됩니다.

  • 각 스냅샷에는 ID, 즉 트랜잭션 ID가 할당되며, 스냅샷 ID는 단조롭게 증가합니다.

  • 읽기와 쓰기는 스냅샷 ID로 구분되며 잠금 없이 격리됩니다.

스냅샷 수명주기 :

  • Snapshot.takeSnapshot()이 호출될 때 생성되고 snapshot.dispose()가 호출될 때 소멸됩니다. 스냅샷은 생성 상태와 릴리스 상태 사이에서 활성 상태로 간주됩니다.

  • 스냅샷은 사용하지 않을 때 폐기해야 합니다. 그렇지 않으면 관련 리소스가 유출될 수 있습니다.

snapshot.enter : 종종 "스냅샷 입력"이라고도 하며, 이는 스냅샷의 컨텍스트에서 람다 표현식을 실행합니다. 이 람다에 입력되면 해당 범위 내의 상태 읽기 및 쓰기가 현재 스냅샷을 기반으로 격리됩니다. 이를 통해 스레드는 로컬이 되고 다른 스레드와 격리될 수 있습니다.

일반적인 유형의 스냅샷:

  • ReadonlySnapshot: 읽기 전용 스냅샷

  • MutableSnapshot: 읽기 및 쓰기 가능

  • NestedReadonlySnapshot 및 NestedMutableSnapshot: 중첩된 스냅샷입니다. 스냅샷 트리의 하위 항목에 대한 읽기 전용 및 변경 가능한 스냅샷입니다. 상위 스냅샷을 활성 상태로 유지하면서 독립적으로 삭제/해제할 수 있습니다. 하위 그룹화 시 중첩된 스냅샷이 생성됩니다. SubcomposeLayout에서와 같습니다.

  • GlobalSnapshot: 전역 공유 상태의 변경 가능한 스냅샷입니다. 모든 스냅샷의 루트 스냅샷입니다. 중첩될 수 없으며 전역적으로 하나만 있습니다.

Snapshot Tree : 스냅샷은 트리를 형성할 수 있으며, 트리의 루트는 GlobalSnapshot입니다.

스냅샷과 스레드는 서로 독립적입니다 .

  • 스레드는 현재 스냅샷을 가질 수 있지만 스냅샷이 반드시 스레드에 바인딩될 필요는 없습니다. 스레드는 마음대로 스냅샷에 들어가고 나갈 수 있습니다.

  • Snapshot.current 현재 스레드의 스냅샷을 가져옵니다. 현재 스레드의 스냅샷 또는 전역 스냅샷을 반환합니다.

스냅샷 읽기 및 쓰기 모니터링 :

  • 예를 들어 Snapshot.takeSnapshot(readObserver)는 읽기 전용 스냅샷에 대한 관찰자를 설정할 수 있습니다. 이 관찰자는 snapshot.enter 호출의 스냅샷에서 상태 객체를 읽을 때마다 알림을 받습니다.

  • 변경 가능한 스냅샷은 읽기 및 쓰기 작업을 관찰하도록 readObserver 및 writeObserver를 모두 설정할 수 있습니다.

  • DerivativeStateOf 함수 내부에는 Snapshot.observe(readObserver, writeObserver, block)을 사용하여 현재 스레드에서 읽기 및 쓰기를 관찰하는 것입니다.

Recomposer는 컴포지션의 모든 읽기 및 쓰기 작업을 추적하고 자동으로 재구성을 트리거합니다. 이는 변경 가능한 스냅샷에 읽기-쓰기 관찰자를 등록함으로써 달성됩니다.

  • Snapshot.takeMutableSnapshot(readObserver, writeObserver)은 Recomposer가 초기 구성과 모든 재구성을 수행할 때 호출됩니다.

  • snapshot.enter(block)에서 결합되거나 재조립된 블록 코드를 실행하므로 이를 들을 수 있습니다.

  • 스냅샷 상태 쓰기가 Recomposer에 의해 추적/관찰될 때마다 동일한 스냅샷 상태를 읽는 해당 RecomposeScope가 무효화되고 재구성이 트리거됩니다.

스냅샷 변경 적용 :

  • snapshot.apply()는 스냅샷 변경 사항을 적용할 수 있으며(변경 가능한 스냅샷의 경우) 원자적 작업입니다.

  • Apply()가 호출된 후 스냅샷에 대한 수정 사항이 다른 스냅샷에 전파됩니다. 해당 범위 내의 상태 객체에 대한 모든 로컬 변경 사항은 상위 스냅샷(중첩된 변경 가능한 스냅샷의 경우) 또는 전역 상태로 전파됩니다.

  • Snapshot.withMutableSnapshot{}은 암시적으로 apply()를 호출하는 단순화된 버전입니다.

글로벌 스냅샷 :

  • SnapshotKt.class 클래스가 JVM에 의해 초기화되면 전역 스냅샷이 생성됩니다.

  • 글로벌 스냅샷 관리자는 컴포저가 생성될 때 시작되고, 컴포지션이 생성될 때 GlobalSnapshotManager.ensureStarted()가 호출되어 글로벌 상태에 대한 모든 쓰기를 관찰하기 시작합니다.

  • 각 컴포지션은 자체 중첩된 변경 가능한 스냅샷을 생성하여 스냅샷 트리에 추가하며, 글로벌 스냅샷은 이 트리의 루트이므로 글로벌 스냅샷 관리자는 컴포지션의 모든 상태를 저장하고 격리할 수 있습니다.

스냅샷 상태의 내부 표현 :

  • MVCC는 상태가 기록될 때마다 새 버전이 생성되도록 보장합니다(기록 중 복사).

  • 스냅샷 상태 개체의 내부 구현은 StateObject이며, 다중 버전에서는 이 개체에 대해 저장된 각 버전의 저장 형식이 StateRecord입니다.

  • 각 레코드는 상태 정보의 버전을 저장하고, 각 레코드는 해당 레코드를 생성한 스냅샷 ID와 연결되며, 스냅샷 ID는 증가되므로 레코드의 각 버전은 ID가 증가하는 단일 연결 목록을 형성합니다.

  • 각 스냅샷에 표시되는 버전(레코드)은 스냅샷을 생성할 당시 사용 가능한 최신 유효한 버전에 해당합니다.

  • 최신 유효 버전은 스냅샷 이전에 생성된 레코드를 의미하며, 해당 레코드는 유효하지 않거나 유효하지 않은 목록에 추가되지 않습니다.

상태 읽기 및 쓰기 :

  • 객체를 읽을 때 지정된 스냅샷 상태(StateObject)에 대한 StateRecords 목록을 순회하여 가장 최근에 유효한 레코드(가장 높은 스냅샷 ID 포함)를 찾습니다.

  • 작성시 가장 최근의 유효한 기록을 후보기록으로 사용합니다. 현재 스냅샷에 유효하면 쓰기에 사용되고, 그렇지 않으면 새 레코드를 생성하여 목록의 선두에 추가하여 새 초기 레코드로 만듭니다.

  • 읽기 및 쓰기가 완료되면 등록된 읽기 관찰자 및 쓰기 관찰자에게 알림이 전송됩니다.


모든 사람이 Jetpack Compose 시스템의 지식 포인트를 더 잘 이해할 수 있도록 돕기 위해 "Jetpack 초보자부터 마스터까지"(Compose 포함) 학습 노트 에 대한 보다 완전하고 자세한 기록이 있습니다 ! ! ! Jetpose Compose에 관심 있는 친구들은 다음을 참고하세요...

Jetpack 제품군 버킷(Compose)

제트팩 섹션

  1. 제트팩의 수명주기
  2. 젯팩지뷰모델
  3. Jetpack 즈 데이터 바인딩
  4. 제트팩 탐색
  5. 젯팩지라이브데이터

Compose 파트
1. Jetpack Compose 상세 소개
2. 학습노트 작성
3. Compose 애니메이션 사용법 상세 설명

추천

출처blog.csdn.net/weixin_61845324/article/details/132516023