안드로이드 뷰 시스템에서의 뷰 무효화(View Invalidation)
안드로이드 뷰 시스템에서의 뷰 무효화(View Invalidation)
뷰 무효화(invalidation)란 특정 View를 다시 그려야 한다고 표시하는 과정을 의미합니다. 안드로이드 뷰 시스템에서 UI 변경 사항을 화면에 반영하기 위한 핵심 메커니즘이며, View가 무효화되면 시스템은 다음 드로잉 사이클(drawing cycle)에서 해당 화면 영역을 갱신해야 한다는 사실을 인지하게 됩니다. 면접에서 커스텀 뷰나 렌더링 파이프라인에 대한 질문이 나올 때, 이 무효화 과정을 정확히 이해하고 있으면 좋은 인상을 줄 수 있습니다. 이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- 무효화(invalidation)의 의미와
View의 다시 그리기가 어떻게 트리거되는지 설명할 수 있습니다. invalidate()와postInvalidate()의 차이점을 기술할 수 있습니다.- 부분 무효화(partial invalidation)를 활용하여 드로잉 성능을 최적화할 수 있습니다.
- 변경 사항의 종류에 따라
invalidate()와requestLayout()을 올바르게 구분하실 수 있습니다. - 불필요한 다시 그리기를 유발하는 흔한 실수를 파악할 수 있습니다.
- 하드웨어 가속(hardware acceleration)이 무효화 경로에 미치는 영향을 이해하실 수 있습니다.
무효화의 동작 원리
View에 대해 invalidate() 또는 postInvalidate() 같은 메서드를 호출하면 무효화 프로세스가 시작됩니다. 시스템은 해당 View를 "dirty" 상태로 표시하며, 이는 다시 그려야 함을 나타냅니다. 다음 프레임이 도달하면 시스템은 무효화된 View를 드로잉 패스(draw pass)에 포함시켜 화면의 시각적 표현을 갱신합니다.
가령, View의 위치, 크기, 외형 등의 속성이 변경되면 무효화를 통해 사용자가 업데이트된 상태를 확인할 수 있게 됩니다. 실제 드로잉은 다음 Choreographer 프레임 콜백에서 수행되므로, invalidate()를 호출하는 시점에 즉시 렌더링이 이루어지는 것은 아닙니다. 이 점은 면접에서도 자주 확인하는 부분이므로 명확히 알아 두시는 것이 좋습니다.
무효화 신호는 해당 뷰로부터 부모 계층(parent hierarchy)을 거쳐 루트인 ViewRootImpl까지 상향 전파됩니다. ViewRootImpl은 시스템 컴포지터(compositor)와 협력하여 다음 VSYNC 프레임에 대한 전체 순회(traversal)를 예약합니다. 이러한 상향 전파 덕분에 렌더링 파이프라인 전체가 최소 하나의 뷰가 다시 그려져야 한다는 사실을 인식하게 됩니다.
무효화를 위한 주요 메서드
다시 그리기를 요청하기 위한 진입점(entry point)은 크게 네 가지가 있습니다.
-
invalidate\(\):View전체를 dirty로 표시하고 다음 레이아웃 패스에서 다시 그리도록 예약합니다. 즉시 다시 그리는 것이 아니라, 다음 프레임을 위해 요청을 큐에 넣는 방식입니다. -
invalidate\(Rect dirty\): dirty 영역을View내 특정 사각형으로 제한하는 오버로드 버전입니다. 전체 뷰 영역이 아닌 변경된 부분만 다시 그리므로 성능 최적화에 도움이 됩니다. -
postInvalidate\(\): 백그라운드 스레드에서 메인 스레드로 무효화 요청을 포스트합니다. UI 스레드에서만 호출해야 하는invalidate()와 달리, 스레드 안전한(thread-safe) 대안입니다. -
postInvalidateOnAnimation\(\):postInvalidate()와 유사하지만, 무효화를 다음 디스플레이 VSYNC 신호에 맞춰 정렬합니다. 프레임 사이에 불필요한 다시 그리기를 예약하는 것을 방지하므로, 애니메이션 기반 무효화에 적합합니다.
invalidate()를 사용한 커스텀 뷰 업데이트
아래는 상태가 변경될 때마다 invalidate()로 다시 그리기를 트리거하는 커스텀 View의 예제입니다.
class CustomView(context: Context) : View(context) {
private var circleRadius = 50f
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(
width / 2f, height / 2f,
circleRadius,
Paint().apply { color = Color.RED }
)
}
fun increaseRadius() {
circleRadius += 20f
invalidate() // 반지름 변경 후 다시 그리기 요청
}
}