View

300x250

이번에 면접을 진행하면서 플러터에서 화면 렌더링의 과정을 이해하고 있는지에 대한 질문을 받았다. 플러터에서 실행하려는 코드가 엔진을 통해 네이티브로 전달되는 과정은 이전에 애플 로그인을 붙이는 과정에서 고생하면서 학습을 한 상태였는데, 화면을 그리는 과정에 대해서는 잘 알지 못했기에 그냥 ‘엔진 통해서 그려주고, 내부적으로 위젯트리, 상태트리 만들어서 렌더링한다‘ 정도로 답변을 얼버무렸었다.

면접 질문들을 복기하면서 내가 답변 못한 부분들을 채우고 있었는데, 플러터 화면 렌더링 매커니즘에 대해서 아래 글에서 그 내용을 잘 정리해뒀기에, 이를 공부하면서 나름대로 다시 정리를 한 번 해보려고 한다!

https://www.alibabacloud.com/blog/exploration-of-the-flutter-rendering-mechanism-from-architecture-to-source-code_597285

https://medium.com/@khkong/flutter-렌더링-메커니즘-88011e1041cd

재미있는건, 여기 아티클에서도 명확하게 정리되어있는 문서가 없어서, 여러 영상 자료들의 내용을 기반으로 엮어서 만들었다고 한다. 오호라…


렌더링 아키텍처

Flutter 에서의 렌더링 아키텍처는 UI 스레드와 GPU 스레드로 구성되어있다.

아마도 UI 스레드는 CPU가 처리하는 영역이 될 것 같다. HCI 강의에서 들었던 것처럼, 병렬화가 불가능한 영역을 UI 스레드가 담당하고, 그 외에 병렬화를 적용할 수 있는 Composition과 Rasterization 을 GPU로 넘겨 처리하는 것으로 생각된다.

https://www.alibabacloud.com/blog/exploration-of-the-flutter-rendering-mechanism-from-architecture-to-source-code_597285

  • UI 스레드
    • 수행하는 작업
      1. Animation - Timer의 Ticker를 기반으로 UI를 변경 : 사전에 정의된대로 다음 state로 상태값들을 변경해준다는 의미로 생각됨
      2. Build - 화면에 위젯을 그리는 코드를 실행 : 이 시점에 Widget Tree, Element Tree, Render Tree를 생성
      3. Layout - 화면상에 컴포넌트들의 크기와 위치를 결정하고, Render Tree에 저장
      4. Paint - 컴포넌트들의 데이터를 실제 화면에 그려지는 위젯의 형태로 구현 : 이 시점에 Render Tree를 기반으로 Layer Tree를 만들어주기
      5. Submit - Graphic Pipeline으로 Layer Tree를 전달
    • Widget Tree, Element Tree, Render Tree를 생성하고, 이를 기반으로 Layer Tree를 생성하여 GPU 스레드에게 이를 전달하는 절차를 수행
  • GPU 스레드
    • Layer Tree에 있는 최적화가 완료된 여러 화면 정보들을 오버레이 처리하고 레스터화하여, 그래픽 엔진을 통해 GL, Vulkan 등의 GPU API를 사용하여 디스플레이에 화면 렌더링을 수행

면접 답변으로 만들어보자면,

플러터는 크게 CPU 스레드와 GPU 스레드로 나뉘어서 렌더링을 처리한다.

User Input이나 Event의 발생에 따라서 State가 변경되면,
CPU 스레드에서 WidgetTree를 업데이트하고 새로운 상태를 반영한 RenderTree로 구성한 뒤, 이를 실제로 렌더링할 방법에 대한 정보를 담은 LayerTree로 변환하여 담는다.
GPU 스레드에서는 LayerTree의 정보에 Composition과 레스터화를 수행한 뒤 프레임을 GPU API로 전달한다.
vsync signal에 따라 전달한 프레임을 OS가 디스플레이에 보여준다.

사실 이 정도만 알아도 면접에서 답변하는건 문제없을 것 같다. 만, 이왕 공부하는거 조금 더 자세히 찾아보자.

렌더링 프로세스

구조에 대해 파악했으니, 화면을 렌더링하기 위한 절차를 알아보자.

플러터에서는 주기적으로 렌더링을 위한 신호를 보내는 vsync 신호에 의해 랜더링이 동작한다.

vsync이란?
vertical synchronization 의 약어로, “수직 동기화” 라는 말로 나에게는 익숙하다. 이게 무엇이냐.

디스플레이 장치에는 주사율이 정해져있다. 만약 주사율이 10Hz라고 한다면, 1초에 화면을 10번 그릴 수 있고, 매 0.1초마다 화면을 새로 그려낼 수 있는 능력을 가지고 있다는 의미이다.

만약 프로그램에서 화면을 전체를 렌더링하는데에 0.15초가 걸린다고 해보자. 그러면 다음 화면을 그려내는 간격인 0.1초 동안에는 67%의 화면만 렌더링해낼 수 있을 것이다.

매 0.1초 마다 칼같이 화면을 새로 그릴 때, 어느 순간에는 67%의 화면만 렌더링 된 상태로 그려지고, 그 다음번에는 34%, 3번째 싱크에서는 다시 100%가 렌더링 된 화면이 그려지는 식으로 화면이 일부만 업데이트 된 채로 그려지게 될 것이며, 이를 tearing (화면 찢어짐) 이라고 부른다.

Tearing 현상을 방지하기 위해서는 화면을 모두 렌더링 한 뒤 한 번에 그리는 방식을 사용할 수 있다. 화면은 매 0.1초마다 새로 그려지지만, 0.15초 동안 화면을 모두 렌더링 한 뒤 그 다음 차례 (0.2초 시점)에 한 번에 화면을 그려내는 방식으로 화면 전체가 새로운 프레임으로 그려지도록 할 수 있다.

다만, 화면을 그려내는 최대 능력이 화면 주사율 이하로 고정이 되고, 프레임을 그려내는 주기와 화면 주사 주기가 맞지 않으면 프레임드랍이 발생할 수 있다.

  1. Flutter 엔진이 실행될 때 vsync 신호에 콜백 등록
    • Android 에서는 OS 내 Chreographer 에 vsync 콜백이 등록이 된다. 이후 매 화면주사율 시점마다 vsync 신호가 켜지면 전달한 콜백이 실행되고, 이에 따라 렌더링된 화면을 OS에 전달해 Tearing 없이 화면을 그려낸다.
    • iOS에서는 OS 내 CADisplayLink 에 vsync 콜백이 등록되고, 동일하게 실행된다.
    • 단순히 화면 렌더링 주기만으로 이를 처리하지 않고 OS의 콜백을 받아오는 이유는 시간을 정확하게 맞춰서 데이터를 전송하려는 것도 있겠지만, 요즘에는 가변 주사율을 사용하는 경우도 많기 때문이지 않을까 생각된다.
  2. vsync 신호에 따른 플러터 엔진 상에서 Dart 코드 실행
    • vsync 신호가 들어오면 플러터의 VsyncWaiter::fireCallback() 이 호출되고, 이에 따라 Animator::BeginFrame() , Window::BeginFrame() 함수들이 호출되면서 UI 스레드의 작업이 시작된다.
  3. UI 스레드의 처리
    • RenderBinding::drawFrame() 함수의 호출로, Widget Tree 상에서 새롭게 그려야하는 위젯들을 새로 그려낸다.
    • Widget Tree의 업데이트에 이어서 Render Tree를 업데이트하며 그려낼 위젯들의 픽셀화(이미지화)를 처리한다.
    • 그 결과를 Layer Tree에 담아 GPU 스레드에게 전달(submit)한다.
  4. GPU 스레드의 처리
    • Layer Tree에 전달된 위젯 픽셀 정보들에 Composition (투명하면 뒤에 보여주기 + 반투명하면 뒷 위젯 비치기 등) 와 Rasterization (여러 겹으로 되어있는 위젯 이미지들 → 하나의 2D 이미지로 납작하게 만들기) 을 거쳐 디스플레이에 그려낼 화면을 만들어낸다.
    • GPU API를 통해 다음 vsync 신호에 화면이 그려지도록 등록한다.

렌더링 아키텍처 파이프라인 단계별 메커니즘

https://www.alibabacloud.com/blog/exploration-of-the-flutter-rendering-mechanism-from-architecture-to-source-code_597285

Animation

Animation 프로세스는 가장 처음에 handleBeginFrame() 메서드에서 transientCallbacks콜백 메서드에서 시작된다.

위젯에서 애니메이션을 가지고 있으면 Ticker에 tick() 함수를 호출하여 다음 프레임 값을 업데이트 하도록 처리한다. 아무래도 미리 정해진 경로대로 애니메이션이 수행되기 때문에, 단순히 tick 함수 하나를 호출하는 것만으로도 다음 state로 값들을 업데이트 할 수 있을 것 같다.

이렇듯 handleBeginFrame에서 호출되는 주요 콜백들이 3가지 있다.

  • transientCalbacks
    • 애니메이션이나 이벤트에 의해 일시적으로 추가/제거되는 콜백 (예를 들어, 애니메이션이 존재할 때에만 추가되고, 없으면 그냥 null 임)
    • vsync가 아닌 Ticker의 영향으로 프레임을 처리
  • persistentCallbacks
    • 매 프레임마다 일관되게 호출되는 렌더링과 관련된 작업을 수행하는 콜백
    • vsync에 의해 직접 영향을 받음
  • postFrameCallbacks
    • UI 스레드의 화면 작업 이후 개발자가 임의로 추가적인 작업을 수행하고자 할 때 실행하는 콜백
    • 다음 프레임을 위한 준비 / 데이터 로드 등의 로직을 처리할 수 있음

 

위 시퀀스 다이어그램을 통해 프로세스를 확인할 수 있다.

  1. HandleBeginFrame() 호출
  2. ㄴ 위젯을 그리기 위한 전처리 수행
  3. transientCallbacks 을 통한 애니메이션에 대한 처리 수행
  4. HandleDrawFrame() 호출
  5. ㄴ 위젯 프레임 그리기 수행
  6. postFrameCallbacks 을 통한 다음 프레임 준비
  7. persistentCallback 을 통한 다음 파이프라인으로 프레임 전달
  8. drawFrame() 함수 호출로 렌더링 작업 수행
  9. buildScope() 함수의 호출로 Build 단계로 넘어감

Build

handleDrawFrame() 함수 호출 시 타고타고 들어가면 시작되는 프로세스이며, BuildOwner::buildScope() 라는 함수를 사용한다. 이 함수가 바로 Widget Tree, Element Tree, RenderObject Tree를 만들어내는 함수이다.

buildScope은 2가지 경우에 호출된다.

  • runApp 함수가 실행될 때
    • WidgetTree를 처음에 만들기 위해 호출된다.
    • 최상단 root 부터 모든 WidgetTree 상의 노드를 만들어낸다.
  • WidgetTree가 업데이트 될 때
    • Dirty Widget (변경이 필요한 위젯) 만 따로 만들어낸다.
    • 대부분은 이 케이스로 호출된다.

Layout

PipelineOwner::flushLayout() 함수를 통해 시작되는 프로세스이며, Widget의 크기와 위치를 결정하고 그 정보를 RenderObject Tree에 저장하여 위젯의 배치를 결정하게 된다.

위젯트리 상에서 부모에서 자식으로 DFS로 내려가면서 부모가 가진 제약사항을 자식 위젯에게 전달하고, 자식은 제약 사항 속에서의 자신의 크기를 계산해 부모에게 다시 전달하여 각 위젯의 배치와 크기를 계산한다. Row나 Column에 여러 크기의 위젯들을 담는 상황을 생각해보면 될 것 같다.

비트 Compositing

PipelineOwner::flushCompositingBits() 함수를 통해 시작되며, Paint 프로세스 이전에 처리가 된다.

여기에서 각 RenderObject를 새로 그려줘야 하는지에 대해 확인한다. 만약 update가 필요하다면 새로 그려주게 된다.

🤔 Build 함수를 실행한다고 해서 모두 다 새로 그려버리지는 않나보다. 견적 보고 새로 그려야 하는 것들만 골라서 처리해주는 방식으로 성능을 올리는 선택을 했다. 오호!

Paint

PipelineOwner::flushPaint() 함수를 통해 시작되며, RenderObject Tree 를 기반으로 Layer Tree를 만들어낸다. UI를 여러 Layer로 나눠서, 어떻게 렌더링을 처리할 지에 대해 속성값들을 넣어주게 되는데, 여기에서 최종적으로 화면에 그려져야 하는 내용들이 담기게 된다.

Submit

Paint의 결과인 Layer Tree 를 GPU 스레드에게 전달한다.

Compositing and Rasterization

어떻게 화면을 렌더링 해야하는지에 대한 정보를 담은 Layer Tree를 기반으로 GPU 스레드에서 composition과 레스터화를 수행한다.

  • Composition : 각 레이어를 오버레이하여 겹쳐보여주기
  • Rasterization : 화면에 대한 정보를 픽셀로 변환

샤라웃

https://medium.com/@khkong/flutter-렌더링-메커니즘-88011e1041cd

https://www.alibabacloud.com/blog/exploration-of-the-flutter-rendering-mechanism-from-architecture-to-source-code_597285

320x100
Share Link
reply
반응형
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30