작년 11월이 마지막 글인 걸 보고,,, 반성의 remember 내부 동작을 시작해보겠습니다.
서론
먼저, 컴포즈를 사용하다보면 반드시 만나게 되는 remember라는 키워드가 있습니다.
이 remember의 사용 이유와 내부 동작을 알아보도록 하겠습니다.
mutableStateOf() 랑 같이 포스트 해보려고 했는데, mutableStateOf가 생각보다 어렵네요,,,
MutableState는 다음 포스트에서 정리 해보도록 하겠습니다.
remember
먼저 remember는 언제 사용할까요? remember는 리컴포지션 상황에서 값을 보존하기 위해 사용합니다.
공식 문서에 나온 글인데요, remember는 메모리에 객체를 저장하며, remember에 의해 계산된 값은 초기 컴포지션에 저장하고, 리컴포지션중에 값을 반환합니다.
그래서 아시다 싶이 버튼을 눌러 count를 높이는 counter 예제를 보면, count에 대한 상태를 remember 안에 mutableStateOf와 함께 저장합니다.
다만, 이 포스트에서 알아볼 것은 리컴포지션에서 내부적으로 어떻게 값을 유지하는지 입니다. (다음 포스트에서는 MutableState가 어떻게 observe하여 리컴포지션을 알아차리는지에 대해서 알아보겠습니다.)
한번 보겠습니다!
remember의 내부를 파고 들어보기 - cache
먼저 remember 함수는 여러개가 있지만, 내부 구현은 or연산만 추가돼어있고 내부 구현은 모두 비슷합니다.
먼저 주석을 읽어보면 "calculation을 통해 생산된 값을 기억하고, calculation은 컴포지션 중에만 계산된다(저장된다라고 보면 될듯함).
또한 리컴포지션은 항상 컴포지션으로부터 생성된 값을 반환한다. "고 합니다.
예를 들어 아래와 같이 remember를 통해 calculation 람다 안에 mutableStateOf("")의 값을 전달하면, 컴포지션 때 전달 하고, 리컴포지션간에 내부적으로 해당값을 가지고 있다가 리턴해줍니다.
val text = remember { mutableStateOf("케이크") }
또한 다른 remember들 또한, 전달 받은 키가 변경되었을 때를 알기 위해 currentComposer.changed 메소드를 통해 해당 값이 이전과 변경됐는지 확인하고, key 파라미터가 늘어날 수록 or 연산을 이용하여 하나라도 변경시에는 true를 전달합니다.
즉, 여러개의 함수들은 모두 같은 cache라는 함수를 사용하며 cache 함수는 아래와 같이 invalid한지, 그리고 전달받은 calculation을 들고 내부로 이동합니다.
첫 번째 관문입니다. 먼저, 해당 기능은 Compose compiler의 plugin api로 직접 부르지 말라고 합니다.
대충 눈대중으로 함수를 보자면, rememberedValue()를 통해 해당 값이 invalid하거나, rememberedValue가 Composer.Empty면 전달받은 람다의 값을 updateRememberedValue를 통해 내부적으로 업데이트하고, 그대로 반환해줍니다. 또한 해당 값중 하나라도 true라면 값을 업데이트하고, 둘다 false인 경우 그대로 it(rememberedValue())으로 리턴을 해줍니다.
즉, 해당 함수의 invalid를 통해, key가 업데이트가 되어서 invalid 함수가 false를 뱉는다면 그 즉시 값을 업데이트 해주고, 첫 실행을 통해 remember된 value가 없거나, 기본함수를 통해 invalid가 false로 전달된다면 그 즉시 값을 업데이트 해줍니다.
쉽죠? 다만 아쉬운 점은 해당 내부 동작, 즉 rememberdValue나 updateRememberedValue함수의 내부는 보기가 힘들다는 점입니다.
함수의 정의로 이동해보면 아래와 같이 나옵니다.
위 함수들은 구현체가 실제로 보이진 않고, ComposeCompiler용 api라고 어노테이션이 되어 있습니다. 그래서, 저는 해당 파일을 decompile 해봤고, 이를 통해 내부 동작을 추측해보려고 합니다. (근데 보다보니,, 구현체가 파일 위에 있었다;; 그래도 그냥 기록용으로 디컴파일을 분석해본 결과도 남겨본다)
rememberedValue()
먼저 rememberedValue부터 보겠습니다. 해당 함수는 아래와 같이 nextSlotForCache() 라는 메소드를 호출하여 리턴해줍니다.
nextSlotForCache를 가보겠습니다.
해당 file이 readOnly이기 때문에 아쉽지만 길게 캡쳐했습니다 ...
해당 함수를 해석해보자면, var10000이라는 변수에, getInserting이 true면 Composer의 Empty를 대입, 아닌 경우 여러 연산을 통해 계산된 값을 대입하여 반환합니다.
읽기 어려우시죠? 근데 사실 해당 함수는 디컴파일 하지 않아도 볼 수 있습니다. (이때 알아차렸다... 위에 보기 쉽게 구현체가 있다는걸)
해당 메소드를 해석해보자면, inserting 중이라면 Composer.Empty를, 아닌 경우 또 if/ if-else문을 통해 값을 리턴해줍니다. (ReusableRememberObserver, RememberObserverHolder의 경우는 저도 아직 공부 이전이라 좀더 찾아보겠습니다,, 다만 위의 예제는 RememberObserverHolder 타입, 즉 else if에서 값을 반환합니다. 아마도, mutableStateOf로 감싸서, 내부의 값을 꺼내기 위해 it.wrapped를 사용한 것 같습니다.)
먼저 볼것은, inserting이란 값인데요,
해당 값은 위에서 쓰인 것처럼 boolean을 가지고 있는데, 해당 값은 composition이 트리에 삽입될 노드를 예약하고 있다면 true입니다. 또한 첫 컴포지션에서는 true이고, 리컴포지션 도중에도 true라고 합니다.
상황을 가정해보겠습니다. 우리가 작성한 코드("val text = remember {mutableStateOf("케이크")} )가 제일 처음 컴포지션을 일으킬 때, 위의 설명과 같이 true가 set 됩니다. (다만 아래 상황은 key를 주지 않아 cache 메소드의 invalid가 false로 들어가서 시작한다는 점을 알립니다. )
1. 즉, 해당 값이 첫 컴포지션을 통해 true를 주면, nextSlotForCache() 함수는 Composer.Empty를 리턴합니다.
2. 그렇게 되면, rememberdValue()는 Compoer.Empty이기 때문에 값을 업데이트 하기 시작합니다.
3. value는 우리가 위에서 전달해준 mutableStateOf("케이크") 가 될 것입니다.
4. 처음 값 초기화가 아니라면 else 문의 it(즉 rememberedValue()) 를 반환합니다.
이를 통해 처음 값이 어떻게 업데이트 되는지 알게됐습니다.
이어서 다시 업데이트 하는 부분으로 돌아가 이번에는 updateRememberedValue에 대해서 보겠습니다.
updateRememberedValue()
해당 함수 또한 내부적으로 updateCachecValue() 메소드를 호출합니다.
이전과 동일하게, 해당 함수 또한 decompile하지 않아도 볼 수 있습니다.
저장할 값을 만들어서, 옵저버 처리를 해주고(changeListWriter.remember를 통해 내부적으로 rememberingManager를 통해 remembering을 수행해줍니다. 이 부분은 다음 포스트에서 알아보겠습니다.), updateValue를 통해 값을 저장합니다. 아까 말했듯 첫 실행때는 inserting이 true 입니다.
또한, 아래 updateValue 메소드는 첫 업데이트 말고도 (inserting이 false) 내부적으로 슬롯 인덱스와 값을 통해 업데이트 해주는 로직도 존재합니다 (여러군데서 사용되네요)
update 내부에서 값을 set 해주고, 기존의 값 또는 Composer.Empty를 반환해줍니다.
그리고 slot에 값을 저장하고
이후에 value를 리턴해주면 초기화가 끝나고, 이후에는 값이 변하지 않으면 else문을, 변한다면 위의 로직을 다시 수행합니다.
그렇다면, 처음으로 돌아가, 위의 사진에서 cache는 currentComposer에서 동작하는데요, currentComposer은 Composer 인터페이스 타입을 타입으로 가지고 있습니다.
그래서, currentComposer은 아마 ComposerImpl을 가지고 있게 될 것 같습니다.
그래서, 각각의 ComposerImpl은 slotTable을 가지고, 각각 내부에서 slot을 가지고 있습니다.
즉 remember을 사용하게 되면 각각의 Composer 내부에서 Impl된 공간에 slots을 가지고 있고, 각각의 slot에서 remember한 값을 가지고 있다고 보시면 됩니다. (참고로 컴파일 되면 @Composable이 붙은 함수는 파라미터에 Composer가 추가됩니다)
결론
컴포저블에서 remember는 캐싱 로직을 통해 수행되며, 내부적으로 slots이라는 Array안의 slot에 값을 저장한다. (아래는 케이크라는 값으로 테스트해본 결과! )
틀린점이 있다면 댓글로 달아주시면 반영하겠습니다.
감사합니다.
참고한 자료 : https://developer.android.com/develop/ui/compose/state?hl=ko
-----
오타 수정 : 09-18
rememberedValue()를 통해 해당 값이 invalid하거나, rememberedValue가 Composer.Empty면 전달받은 람다의 값을 updateRememberedValue를 통해 내부적으로 업데이트하고, 그대로 반환해줍니다. 또한 해당 값중 하나라도 false라면, 업데이트하지 않고 그대로 it으로 리턴을 해줍니다.
-> rememberedValue()를 통해 해당 값이 invalid하거나, rememberedValue가 Composer.Empty면 전달받은 람다의 값을 updateRememberedValue를 통해 내부적으로 업데이트하고, 그대로 반환해줍니다. 또한 해당 값중 하나라도 true라면 값을 업데이트하고, 둘다 false인 경우 그대로 it(rememberedValue())으로 리턴을 해줍니다.
추가 수정 : 09-23
저장할 값을 만들어서, 옵저버 처리를 해주고(updateValue 이전의 값은 잘 모르지만,,, 옵저빙하는 코드들을 추가해주는 것 같습니다 이부분은 뇌피셜이므로 아닐 수도 있습니다 !!!!), updateValue를 통해 값을 저장합니다.
->
저장할 값을 만들어서, 옵저버 처리를 해주고(changeListWriter.remember를 통해 내부적으로 rememberingManager를 통해 remembering을 수행해줍니다. 이 부분은 다음 포스트에서 알아보겠습니다.), updateValue를 통해 값을 저장합니다.
'Android' 카테고리의 다른 글
Lifecycle.State와 repeatOnLifecycle에 대해서 (0) | 2023.09.14 |
---|---|
Android DeepLink 공백 (0) | 2022.12.10 |
Android github actions CI 적용기 (1) | 2022.12.06 |
javadoc을 이용해서 협업을 더 쉽게 해보자 (2) | 2022.11.07 |
Android API 33대응! onBackPressed()는 deprecated 된다 (1) | 2022.10.23 |