안드로이드에서 뷰가 그려지는 과정

Posted by Jungwoon Blog on October 2, 2019

안드로이드에서 뷰가 그려지는 과정

최근에 안드로이드에서 뷰가 그려지는 과정에 대해서 질문을 받았던 적이 있는데 그 부분에 대답을 잘 못해서 공부도 할겸 포스팅을 작성합니다.


개념

안드로이드에서 UI를 렌더링하기 위한 여러가지 개념들이 있는 해당 개념들 보통 살펴보도록 하겠습니다.

크기 순서

Window > Surface > Canvas > View 

Window

위의 그림에서 네모로 표시한 하나하나가 Window 입니다. 위와 같이 하나의 화면 안에는 여러개의 Window를 가질 수 있고 이러한 Window들은 WindowManager가 관리를 합니다.

Window는 뭔가를 그릴 수 있는 창이라고 생각하면 됩니다. 보통 하나의 Surface를 가지고 있으며, ApplicationWindow Manager와 상호작용하여 Window를 만듭니다. Window Manager는 각각의 Window 표면에 Component를 그리기 위해 Surface를 만듭니다.

ApplicationSurface에 원하는 것을 그릴 수 있습니다.

일반적으로 ActivityWindow를 가지게 됩니다.


Surface

위의 그림처럼 각각의 WindowSurface를 가집니다.

Surface는 화면에 합성되는 픽셀을 보유한 객체입니다. 화면에 표시되는 모든 Window는 자신만의 Surface가 포함되어 있으며, Surface Flinger가 여러 소스로부터 그래픽 데이터 버퍼를 받고, 그것들을 합성해서 Display로 보냅니다.

개별 Surface는 **이중 버퍼 렌더링을 위한 1개 이상(보통 2개)의 버퍼를 가집니다.

** 이중 버퍼 렌더링

스크린의 출력될 화면 데이터를 프레임 버퍼(Frame Buffer)라고 하는데, 렌더링을 하면 그 결과가
프레임 버퍼에 저장이 됩니다. 근데 하나의 버퍼만 가진다면, 이미지가 다 그려지지 않아도 화면
주사율때문에 렌더리을 해야할때 다 그려지지 않은 프레임 버퍼가 렌더링이 되어서 그려져야 할 이미지가
나타났다 사라졌다 하면서 깜빡이는 현상(Flicker)가 발생할 수 있습니다.

이를 해결하기 위해서 프레임 버퍼에 바로 렌더링 하지 않고, 다른 버퍼를 만들어서 그 버퍼에 렌더링을
완료하고 다음 프레임 버퍼로 옮기는 방식을 사용하여 위와 같은 현상을 해결할 수 있습니다. 

View

ViewWindow 내부의 대화식 UI 요소입니다. 창에는 단일 뷰 계층 구조가 연결되어 있으며 모든 창의 동작을 제공합니다.

Window가 다시 뭔가를 그려야할때마다 WindowSurface에서 작업이 수행됩니다. 만약 Surface가 잠기면 그리는데 사용할 수 있는 Canvas가 반환이 됩니다. 그럼 Draw Traversal가 계층적으로 각 뷰를 Canvas에 전달하여 UI를 그리기 시작합니다.

완료가 되면 Surface가 잠기고 방금 그린 Buffer가 포그라운드 상태로 바뀌고 Surface Flinger에 의해서 화면에 합성됩니다.


Canvas

실제 UI를 그리기 위한 공간으로 비트맵이 그려지는 공간이라고 생각합니다.


SurfaceView

일반적인 ViewMain Thread에서 캔버스를 그리기 때문에, 그리기를 하는 동안에는 사용자의 입력을 받을 수 없고 그로 인해 반응성이 좋지 못합니다. 그렇다고 그리는 작업을 별도의 작업 스레드에서 처리하고 싶어도 안드로이드 정책 상 Main Thread가 아닌 별도의 Thread에서는 UI 관련 작업을 할 수도 없습니다. 이럴때 사용할 수 있는게 SurfaceView입니다.

SurfaceViewCanvas 아닌 Surface(=가상 메모리 화면)에 그리고 그려진 Surface를 화면에 뿌리기 때문에 게임이나, 카메라 같은 높은 반응성이 필요한 UI 작업이 필요한 경우 사용할 수 있습니다.

구조는 위의 이미지와 같으며, SurfaceHolderSurface에 접근하여 화면을 처리해주는 구조입니다.


View와 ViewGroup

안드로이드에서 UI요소들은 크게 ViewViewGroup으로 이루어져 있으며 아래와 같은 특징들을 가지고 있습니다.

  • View는 화면상의 UI를 구성합니다.
  • ViewGroupViewGroupView를 포함합니다.
  • TextView, Button등 기본적인 화면 구성 요소들은 View에 상속되어 구현됩니다.
  • 개별적으로 속성을 가지며 개별 속성은 Parent로부터 상속을 받을 수 있습니다.
  • View는 Window의 Surface를 이용하여 화면을 그리고, 이벤트를 어떻게 처리할지에 대한 구현을 하는 객체입니다.

Layout이 그려지는 과정

안드로이드에서는 액티비티가 Focus를 얻게되면 자신의 Layout을 그리게 됩니다. 이때 그리고자 하는 LayoutRoot Node를 요청하게 되는데, 우리가 setContentView()를 호출할때 Root Node가 정해집니다. Root Node가 결정이 되면 Root Node를 따라 Child Node를 찾아가면서 차례대로 View를 그리게 됩니다.

Layout을 그리는건 아래 과정을 통해 측정순서란 과정을 거치게됩니다.

measure() -> onMeasure() -> layout() -> onLayout()
  • measure(widthMeasureSpec: Int, heightMeasureSpec: Int) : 뷰의 크기를 알아내기 위해 호출되며, 실제 크기 측정을 위해 onMeasure()를 호출합니다.
    • widthMeasureSpec : Parent가 부여한 필요한 가로 공간
    • heightMeasureSpec : Parent가 부여한 필요한 세로 공간
  • onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) : 실제 뷰의 크기를 측정합니다.
    • widthMeasureSpec : Parent가 부여한 필요한 가로 공간
    • heightMeasureSpec : Parent가 부여한 필요한 세로 공간
  • layout(left: Int, top: Int, right: Int, bottom: Int) : 뷰의 위치를 할당하기 위해 호출되며, 실제 할당을 위해 onLayout()을 호출합니다.
    • left : Parent에 대한 왼쪽 포지션
    • top : Parent에 대한 위쪽 포지션
    • right : Parent에 대한 오른쪽 포지션
    • bottom : Parent에 대한 하단 포지션
  • onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) : 실제 뷰의 할당을 합니다.
    • changed : 새로운 사이즈나 위치인지 맞으면 True 아니면 False
    • left : Parent에 대한 왼쪽 포지션
    • top : Parent에 대한 위쪽 포지션
    • right : Parent에 대한 오른쪽 포지션
    • bottom : Parent에 대한 하단 포지션

Custom View LifeCycle

뷰가 생성이 되면 다음 순서대로 메소드들이 호출이 됩니다. View의 LifeCycle는 여러가지 이유로 공식적으로 제공이 되지 않습니다. 아래 이미지는 널리 퍼져 있는 View LifeCycle입니다.

  1. Constructor :
    • CustomView(context: Context) : 코드로 생성하면 호출
    • CustomView(context: Context, attributeSet: AttributeSet) : xml로 생성하면 호출
  2. onAttachedToWindow : Parent View가 addView(view: View)를 호출하면 해당 ViewWindow에 연결됩니다, 이 단계부터 id를 통해 접근할 수 있습니다.
  3. onMeasure : View의 사이즈 측정
    • MeasureSpec : Parent View에서 Child View로 요구조건을 보내기 위해 사용
      • MeasureSpec.EXACTLY : 직접적으로 widthheight에 대해 하드코딩 할때
      • MeasureSpec.AT_MOST : ParentChildMaximum Size를 설정하기 위해 사용합니다.
      • MeasureSpec.UNSPECIFIED : Parent에 의해 사용되며 제한이 없기 때문에 Child View가 원하는 사이즈를 가질 수 있습니다.
  4. onLayout : 개별 Child View들의 사이즈 및 위치 할당
  5. onDraw : View 그리기 시작, CanvasPaint를 사용하여 그리기 시작하며 Canvas모양을 그리고 Paint을 칠합니다. (리소스를 많이 사용하기 때문에 할당된 객체를 재사용할 수 있도록 해야합니다.)

View Update

View의 업데이트는 위에서 보이는거 같이 아래 두 함수를 통해서 이루어집니다.

  • invalidate() : View에 변화가 생겨서 다시 그려야 할때 사용합니다.
  • requestLayout() : View를 처음부터 그려야 할때 사용합니다.

Animation

ViewAnimationframe 단위View에 애니메이션으로 변화가 있을때마다 invalidate()를 호출하여 뷰를 그립니다. 대표적으로 애니메이션을 사용하는 클래스는 ValueAnimator 입니다.