Android UI 변경 메커니즘

woga·2022년 2월 26일
1

Android 공부

목록 보기
18/49
post-thumbnail

메인 스레드에서 UI 변경 메커니즘을 간단히 살펴보자. 언제 UI가 변경되고 변경되지 않는지 메커니즘을 이해한다면 더 정확하게 코드를 작성할 수 있다.

UI를 변경하는 메서드는 TextView에서처럼 주로 setXxx()로 되어 있다. 게다가 커스텀 뷰를 만들면서 이런 setter 메서드를 작성할 때, POJO(plain old java object)와 같이 단순 대입만 하는 실수를 하기도 한다. 값을 지정하기만 하면 되는게 아니다. 다시 그리도록 invalidate() 메서드를 호출해야 한다.

public void setTitle(String title) {
	this.title = title;
    invalidate(); // 1)
 }

다시 그리도록 1)과 같이 invalidate() 메서드를 호출해야만 메인 Looper의 MessageQueue에 들어가서 다음 타이밍에 화면에 그린다. 이 때 파라미터로 전달받은 title을 onDraw()에서 반영하는 식이다.

invalidate() 메서드의 호출 스택 확인

invalidate()부터 시작하는 메서드를 따라가보자.

View.invalidate()

ViewGroup.invalidateChild()
parent = parent.invalidateChildInParent() : ViewGroup.invalidateChildInParent()

parent = parent.invalidateChildInParent() : ViewRootImpl.invalidateChildInParent()
ViewRootImpl.scheduleTraversals() : traversal 작업을 스케줄링
ViewRootImpl.performTraversal() : traversal 작업을 실행 (draw 메세지를 보냄)
  1. View의 invalidate() 메서드는 상위 ViewGroup에 자신의 영역을 다시 그려야한다는 의미로 ViewGroup의 invalidateChild(View child, final Rect dirty)를 호출한다.

  2. invalidateChild() 메서드는 do while문에서 parent에 parent.invalidateChildInParent(location, drity)를 대입하고, parent가 null이 아닌 동안에 계속 호출한다. (부모가 Root가 아닐때까지)

여기서 ViewParent 인터페이스가 등장한다. View에는 getViewGroup() 같은 메서드가 있을 것 같은데 그런 메서드는 없다. 대신 getParent() 메서드가 있고 getParent()는 ViewParent를 리턴한다.

따라서 상위 ViewGroup을 가져오는 방법은 getParent()를 실행하고 번거롭게도 ViewGroup으로 캐스팅하는 것이다.

  1. 만약, 현재 부모가 Root라면, ViewGroup.invalidateChildInParent() 대신 ViewRootImpl.invalidateChildInParent()가 호출된다.

do while 문에서 View/ViewGroup이 다시 그리는 영역을 상위로 전달하다 보면 어쨌든 가장 위까지 전달될 것이다. 가장 상위는 역시 ViewGroup(com.android.internal.policy.PhoneWindow$DecorView)인데 여기서 다시 그리라는 Message를 보낼 수도 있다.

하지만 로직을 분리하기 위해서 가상으로 또 다른 상위를 만들었다. 이것이 ViewRootImpl 클래스이다. ViewGroup과 ViewRootIml은 ViewParent 인터페이스를 구현한 것으로 invalidateChildInParent()는 ViewParent 인터페이스의 메서드이다.

  • 즉, invalidateChild() 메서드는 do while 문에서 최종적으로 ViewRootImpl의 invalidateChildInParent(int[] location, Rect dirty)를 호출한다.
  1. ViewRootImpl에서 invalidateChildInParent()의 주 작업은 scheduleTrraversals() 메서드를 호출하는 것이다. scheduleTraversals() 메서드는 무효화된(invalidated) 영역을 다시 그리기 위한 순회(traversal) 작업을 스케줄링한다.
    스케줄링은 기존에는 메인 Looper의 MessageQueue에 Message를 직접 넣었지만 젤리빈부터는 Choreographer에 다시 위임한다.

ViewRootImpl의 invalidateChildInParent()에서는 맨 먼저 checkThread() 메서드를 호출해서 메인 스레드가 아니면 CalledFromWrongThreadException을 발생시킨다.


invalidateChild(), invalidateChildInParent() Deprecated

  • onDescendantInvalidated()를 사용해야한다.

invalidate() 호출 스택

View.invalidate()

ViewGroup.onDescendantInvalidated()
parent = parent.onDescendantInvalidated() 

//다음 노드가 Root가 아닌 경우 (do-while문)
ViewGroup.onDescendantInvalidated()

//다음 노드가 Root인 경우
ViewRootImpl.onDescendantInvalidated()
ViewRootImpl.scheduleTraversals() : traversal 작업을 스케줄링
ViewRootImpl.performTraversal() : traversal 작업을 실행 (draw 메세지를 보냄)

invalidate()를 여러 번 호출하는 경우

fun onClick(view: View) {
        for (i in 0..4) {
            currentValue.setText("Current Value=$i")
            SystemClock.sleep(1000)
        }
    }

1초마다 TextView의 텍스트를 바꿔주는 코드로 보인다.

그러나 실행해 보면 화면이 1초마다 변경되지 않고 5초 후 마지막에 넣은 Current Value=4만 보이는 것을 확인할 수 있다. 5초 동안 메인 스레드를 잡고 있기 때문에 화면 갱신이 5초 동안 가능하지 않은 것이다.

TextView의 setText() 메서드는 로직이 복잡한데 결국은 다시 그리기 위해서 invalidate() 메서드를 호출해야 한다. 여기서 의문이 하나 생긴다. setText()에서 매번 invalidate()를 실행하면 5초 동안은 그리지 못하지만, invalidate()에서 ViewRootImpl을 거쳐서 scheduleTraversals()를 실행하면서 MessageQueue에 다시 그리기 Message를 매번 쌓을 것 같다.

Q. 그러면 Current Value=0에서 Current Value=4까지 눈에 보이지 않을 정도로 짧은 시간에 출력해서 중간 과정을 사람의 눈으로는 보지 못하는 것은 아닐까?

-> 그렇지는 않다.

View에서는 mPrivateFlags라는 플래그를 사용해서, 메서드 내에서 invalidate()를 여러 번 호출해도 첫 번째 호출만 ViewRootImpl까지 전달된다.

View의 invalidateInternal() 메서드의 시작 부분에서 mPrivateFlags 값으로 체크하는 if 조건문이 그 내용이다.

1) 첫 번째 invalidateInternal() 메서드의 if 문 내에서 플래그 변경
2) 다음 invalidate() 호출에서는 invalidateInternal() 메서드의 if 문에서 필터링되서 ViewRootImpl까지 도달하지 않는다.

즉, 메서드 내에서 여러 번 invalidate()를 호출해도 다시 그리라는 Message는 한 번만 전달되는 것이다.

그렇다고 invalidate() 메서드가 계속 막혀서도 안된다. 한 번 그려지고 invalidate()가 호출되면, 또 그려야 하기 때문이다.

View의 mPrivateFlags는 패키지브라이빗 변수인데 View, ViewGroup, ViewRootImpl 세 군데에서 적절하게 변경해서 이 문제를 해결하고 있다.

Reference

profile
와니와니와니와니 당근당근

0개의 댓글