플러터의 렌더링 원리
(본 글은 공부하며 적은 글로서 100% 맞음을 보장할 수 없습니다. 틀린 곳은 조언해주시면 감사하겠습니다!)
Dart Developers Korea 오픈채팅방 https://open.kakao.com/o/gYyufB6
지난 글에(https://brunch.co.kr/@myner/5) 이어서 Flutter 렌더링 원리를 이어 보도록 하자!
Flutter는 그래서 어떻게 앱을 렌더링 할까?
1. 레이아웃 단계 : 이 단계에서 Flutter는 각 객체의 크기와 화면에 표시되는 위치를 정확하게 결정합니다.
2. 페인팅 단계 : 이 단계에서 Flutter는 각 위젯에 캔버스를 제공하고 자체 위젯을 그립니다.
3. 합성 단계 : 이 단계에서 Flutter는 모든 것을 하나의 장면에 넣고 처리를 위해 GPU로 보냅니다.
4. 래스터라이징 단계 : 이 최종 단계에서 장면은 픽셀 행렬로 화면에 표시됩니다.
이 단계는 Flutter만 그런 것이 아니다. 웹브라우저나 안드로이드 등등 대부분 매우 유사한 단계를 찾을 수 있다. 필지의 경우 Map Rendering Engine을 개발한 경험이 있는데 그것도 이와 비슷하다. 사실 어찌 보면 당연한 단계이다. Flutter의 특별한 점은 렌더링 프로세스가 매우 단순하지만 매우 효율적이라는 것이다.
Layout
Layout의 Size 계산할 때는 아래의 그림과 같은 흐름으로 진행된다.
부모는 자식에게 제약조건을 계속 내려준다. 그런 다음 자식은 새로운 제약 조건을 생성하고이를 자신의 자식에게 넘깁니다. 이런 작업을 자식이 없는 리프 위젯에 도달할 때까지 계속 진행된다. 그런 다음이 위젯은 전달된 제약 조건을 기반으로 레이아웃 세부 정보를 결정한다. 바로 사이즈를 결정하는 것이다. 레이아웃에 필요한 세부 사항을 판별하고 정한 다음 이를 부모에게로 다시 전달한다.
제약조건의 흐름은 위에서 아래로 가고 이 제약조건에 따라 각 자식들은 본인의 사이즈를 확정해서 위로 올려 보낸다. 이렇게 제약조건과 사이즈에 따라 View들의 위치가 결정되는 것이다. 흔히들 말하는 레이 아웃팅 과정이 완료가 되는 것이다.
"Constraint"과 "LayoutDetail"은 무엇일까? 이는 사용 중인 레이아웃 프로토콜에 따라 다르다. Flutter에는 두 가지 기본 레이아웃 프로토콜이 있다. Box Protocol과 Sliver Protocol이다. Box Protocol은 단순한 2D 데카르트 좌표계에서 객체를 표시하는 데 사용되는 반면 Sliver Protocol 스크롤하는 데 반응하는 객체를 표시하는 데 사용된다.
Box Protocol에서, 부모가 자식에게 전달하는 제약 조건은 BoxConstraints이다. 이러한 제한은 각 자식이 허용되는 최대 너비와 최소 너비와 높이를 결정합니다. 예를 들어, 부모는 자식에게 다음 BoxConstraints를 전달할 수 있다.
제약 속에서 어떤 크기를 가져야 하는지 결정하고 그 부모에게 결정을 알린다. 따라서 "Layout Detail"은 자식이 선택하는 크기인 셈이다.
Slivver Protocol은 상황이 좀 더 복잡하다. 부모는 스크롤 오프셋, 겹침 등과 같은 스크롤 정보와 제약 조건을 포함하는 자식에게 SliverConstraints로 전달한다. 자식은 다시 부모에게 SliverGeometry를 보낸다.
Paint
레이아웃이 완료되면 렌더링 객체 트리의 각 노드는 명시적인 크기와 위치를 가지고 이제 Layer에 그리기 시작한다. 일단 부모가 그 자식의 모든 레이아웃 세부 사항을 알고 있으면, 모두를 그릴 수 있다. 이를 위해 Flutter는 PaintingContext를 전달한다. 이 PaintingContext에는 그릴 수 있는 Canvas가 포함되어 있고, 페인팅 콘텍스트를 사용하면 자식을 페인트 하고 새로운 페인팅 레이어를 만들 수 있다. (이후 합성 및 래스터 화를 수행한다.)
드로잉 할 때는 offset에 맞는 레이어를 찾아서 드로잉 한다. 근데 4번째 노드는 노란색 레이어 6번째 노드는 빨간색 레이어에 그려진다. 이로 인해서 2번 노드는 6 노드를 그릴 때 다시하번 그려져야 하고, 논리적 으로는 관계가 없지만 같은 레이어의 6번 노드도 다시 한번 또 그려진다. 왜 다시 그려지냐고?.. 아마 예상하기로 Paint는 마치 예전 프린트 방식과 같기 때문이다. 한 줄 한 줄 인쇄하는 거다. 그래서 이를 피하기 위해 Flutter에선 Repaint Boundary라는 개념을 제공한다.
Repaint boundary를 사용하면 변환이 일어나는 부분과 아닌 부분의 연결을 피하여 위에 말했던가 같은 상황(두 번 그리는 상황)을 만들지 않게 된다. 주로 많이 쓰이는 부분이 ScrollView에서 이다. 스크롤되는 부분을 다시 그릴 때 다른 부분은 다시 그릴 필요가 없기 때문이다.
Render Tree
Flutter를 개발한다면 여러 종류의 위젯에 대해 알 것이다. StatefulWidget, StatelessWidget, InheritedWidget 등이 있다. 그러나 RenderObjectWidget이라는 또 다른 종류의 위젯이 있다. 이 위젯에는 build 메서드가 없으며 RenderObject를 작성하여 렌더 트리에 추가할 수 있는 createRenderObject 메서드가 있다. RenderObject는 렌더링 프로세스에서 가장 중요한 구성 요소이다. 렌더링 트리의 모든 것은 RenderObject입니다. 그리고 각 RenderObject에는 렌더링을 수행하는 데 사용되는 많은 속성과 메서드가 있다.
- 부모로부터 건네받은 제약을 나타내는 Constraint Object
- 부모가 유용한 정보를 저장할 수 있는 parentData Object
- performLayout 메서드.
- Paint 메서드
RenderObject는 추상 클래스이다. 실제 렌더링을 수행하려면 이를 확장해야 한다. 그리고 RenderOject를 확장하는 두 가지 가장 중요한 클래스는 RenderBox와 RenderSliver로 생각된다. 이 두 클래스는 각각 Box Protocol과 Sliver Protocol을 구현하는 모든 렌더 객체의 부모이다. 이 두 클래스는 수십 및 수십 개의 다른 클래스로 확장되고 렌더링 프로세스의 세부 사항을 구현한다.
사실 웹 브라우저의 렌더링에 관심이 많다면 이미 감이 왔을 것이다. 웹 브라우저는 렌더링에 있어서 많은 고민을 해왔고 다른 플랫폼들에서는 이를 많이 차용하고 있다.
웹 브라우저의 렌더링 방식(크로미움 기반)
웹 브라우저의 렌더링 순서이다.
JS라고 된 부분에는 Dom Tree , CSSOM Tree -> RenderTree까지 완성된 것이 포함된다.
이후 Layout을 통해서 해당 RenderObject를 그리기 위한 가장 작은 Box 형태로 위치들을 계산한다.
이것들을 모아 Layer Tree를 만들고 Paint를 실행하고 최종족으로 Compositing을 하여 화면에 출력한다.
어떤가 Flutter랑 차이가 느껴지는가? 필자는 그렇게 많지 않다고 생각한다.
내부적 약간의 다름이 있을 수 있지만 개념은 같다.
추후에 웹 브라우저 렌더링 방식에 대해서도 살펴보는 글을 쓰겠다.
다음 글은 이 RenderObjectWidget을 한번 커스텀해보자.
<참고문헌>
https://flutter.io/docs/resources/technical-overview
https://github.com/flutter/flutter
https://github.com/flutter/engine
https://docs.flutter.io/index.html
https://zhuanlan.zhihu.com/p/36861174
https://tech.meituan.com/waimai_flutter_practice.html
https://www.yuque.com/xytech/flutter/hc0xq7
https://www.yuque.com/xytech/flutter/tge705
https://www.jianshu.com/p/e6cd8584fdbb?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation
https://flutter.io/docs/development/platform-integration/platform-channels