Flutter를 빠르게 배우고 사용하기 24개의 강의--22 자체 렌더링 모드: Flutter의 렌더링 원리를 통해 성능 최적화 전략에 대해 자세히 알아보세요.

자체 렌더링 모드에서는 Flutter 세 개의 트리가 핵심 지식 포인트입니다. 이 강의에서는 Flutter의 자체 렌더링 모드에서 세 개의 나무에 대해 배운 다음 세 개의 나무를 그리는 과정을 통해 Flutter가 성능을 최적화하는 방법과 Flutter 앱의 성능을 향상시키는 방법을 배웁니다.

나무 세 그루

Flutter에는 Widget, Element, RenderObject라는 세 가지 트리가 있습니다.

  • 위젯은 기본 UI 렌더링 구성 정보를 주로 포함하는 UI 인터페이스를 설명하는 데 사용됩니다.

  • Element는 Widget과 RenderObject 사이의 프런트 엔드의 가상 Dom과 유사합니다.

  • RenderObject는 실제로 렌더링되어야 하는 트리이며, 렌더링 엔진은 RenderObject를 기반으로 인터페이스를 렌더링합니다.

Flutter에서 일련의 처리를 수행한 후 그림 1과 같이 구성 정보가 생성됩니다(디버그 모드를 사용하여 이 렌더링 트리의 구조 정보를 얻을 수 있음).

그리기 0.png

그림 1 렌더링 트리 구조

그림 1에서는 세 가지 속성이 더욱 중요합니다.

  • _widget은 우리가 위젯 트리라고 부르는 것입니다.

  • _chilid는 우리가 Element 트리라고 부르는 것입니다.

  • 그리고 _renderObject는 RenderObject 트리입니다.

위의 렌더링 트리 구조는 다음과 같이 우리가 보는 위젯에 대한 매우 간단한 구성입니다.

void main() {
  runApp(MaterialApp(
    title: 'Navigation Basics',
    home: FirstRoute(),
  ));
}
class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('flutter test'),
    );
  }
}

위의 코드는 간단한 페이지 구성 요소를 설명하지만 이 간단한 페이지 구성 요소 뒤에는 매우 복잡한 트리 구조가 있습니다. 그림 2와 같이 렌더링된 요소 트리가 어떻게 보이는지 살펴보세요.

그림1.png

그림 2 요소 트리 구조

매우 간단한 위젯의 경우 Flutter에서 실제로 생성된 요소 트리 구조 다이어그램이 너무 복잡하다는 사실을 발견한 적이 있습니까? 트리 맨 아래에 우리가 사용하는 FirstRoute->Center->Text->RichText 구성 요소(그림 2의 빨간색 부분)가 있다는 것을 알고 계셨습니까? 세 나무의 구조를 이해한 후, 세 나무가 어떻게 변형되는지 살펴보자.

세 나무 사이의 대응

Flutter에서는 Widget과 Element 트리 사이에 일대일 대응이 있지만 RenderObject와는 일대일 대응이 없습니다. 일부 위젯은 렌더링할 필요가 없기 때문입니다. 예를 들어 위 테스트 코드의 FirstRoute는 렌더링할 필요가 없는 위젯입니다. 결국 RenderObjectWidget과 관련된 Widget만 RenderObject로 변환되며, 이 유형만 렌더링하면 됩니다. 표 1에 표시된 세 가지 트리 부분 유형 간의 해당 관계를 볼 수 있습니다.

그림 2.png

표 1 위젯, 요소 및 RenderObject 간의 대응

다음으로, 세 가지가 어떻게 변화하는지 살펴보겠습니다.

Threetrees 변환 프로세스

Flutter 작업의 핵심 로직 중 일부는 이 세 트리의 변환을 처리하는 것입니다. 모든 인터페이스 상호 작용 및 이벤트 처리는 궁극적으로 이 세 트리의 작업 결과에 반영됩니다. 일반적인 상황에서는 이것이 Flutter 프로젝트를 실행하는 방법입니다.

void main() {
  runApp(MaterialApp(
    title: 'Navigation Basics',
    home: FirstRoute(),
  ));
}

MaterialApp은 우리가 설명한 위젯이고 Flutter는 ScheduleAttachRootWidget, attachmentRootWidget 및 attachmentToRenderTree를 통해 RenderObjectToWidgetElement의 마운트 메서드를 호출합니다. 이 프로세스에는 상당히 많은 소스 코드 기능이 포함될 것입니다. 여기서는 소개할 몇 가지 중요한 기능을 선택합니다.

중요한 기능 설명

기능을 소개하기 전에 그림 3과 같이 전체적인 아키텍처 흐름도를 살펴보겠습니다.

그림3.png

그림 3 Flutter 트리 변환 다이어그램

위의 다이어그램은 비교적 복잡하므로 먼저 간단히 이해하면 되며 나중에 자세히 설명하겠습니다. 먼저 이러한 주요 기능의 기능을 살펴보겠습니다.

  • ScheduleAttachRootWidget은 루트 위젯을 생성하고, 루트 위젯에서 하위 노드까지 Element 요소를 반복적으로 생성하고, 하위 노드가 RenderObjectWidget인 위젯에 대해 RenderObject 트리 노드를 생성하여 뷰의 렌더링 트리를 생성합니다. 태스크는 여기 소스 코드에서 사용되며, 그 목적은 마이크로 태스크 실행에 영향을 주지 않는 것입니다.
void scheduleAttachRootWidget(Widget rootWidget) {
  Timer.run(() {
    attachRootWidget(rootWidget);
  });
}

  • attachmentRootWidget은 ScheduleAttachRootWidget과 동일한 기능을 가지고 있으며, 먼저 루트 노드를 생성한 다음, 루프에 자식 노드를 생성하기 위해 AttachToRenderTree를 호출합니다.
void attachRootWidget(Widget rootWidget) {
  _readyToProduceFrames = true;
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
  ).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
}

  • attachmentToRenderTree, 이 메소드에는 두 가지 핵심 호출이 있습니다. 핵심 코드 부분의 예만 설명하겠습니다. BuildScope가 먼저 실행되고 buildScope에서 두 번째 매개변수(콜백 함수, 즉 element.mount)가 먼저 호출됩니다. , 그리고 mount는 루프에서 하위 노드를 생성하고 생성 프로세스 중에 업데이트해야 하는 데이터를 더티로 표시합니다.
owner.buildScope(element, () {
  element.mount(null, null);
});

  • buildScope, 첫 번째 더티 렌더링이 빈 목록이어서 첫 번째 렌더링에 이 함수의 실행 흐름이 없으면 이 함수의 핵심은 두 번째 렌더링 또는 setState 이후 더티로 표시된 요소가 있는 경우에만 적용됩니다. 이 함수의 목적은 더티 배열을 순환하는 것이기도 합니다. 요소에 하위 요소가 있으면 재귀적으로 하위 요소를 결정하고 하위 요소를 빌드하여 새 요소를 생성하거나 요소를 수정하거나 RenderObject를 생성합니다.

  • updateChild, 이 메소드는 매우 중요합니다. 모든 하위 노드는 이 함수를 통해 처리됩니다. 이 함수에서 Flutter는 Element와 RenderObject 사이의 변환 로직을 처리하고 Element 트리의 중간 상태를 통해 RenderObject 트리에 미치는 영향을 줄입니다. 성능 향상. 이 함수의 구체적인 코드 로직을 분해하여 분석해 보겠습니다. 이 함수의 입력 매개변수에는 요소 하위, 위젯 newWidget 및 동적 newSlot의 세 가지 매개변수가 포함됩니다. child는 현재 노드의 Element 정보이고, newWidget은 위젯 트리의 새 노드, newSlot은 노드의 새 위치입니다. 매개변수를 이해한 후 핵심 로직을 살펴보고 먼저 새로운 Widget 노드가 있는지 확인합니다.

if (newWidget == null) {
  if (child != null)
    deactivateChild(child);
  return null;
}

존재하지 않으면 현재 노드의 Element가 바로 소멸되는데, Widget에 노드가 존재하고 Element에도 해당 노드가 존재하는 경우에는 먼저 두 노드의 첫 번째 줄과 같이 일관성이 있는지 확인합니다. 코드가 일치하지만 위치가 다른 경우에는 위치만 업데이트하세요. 다른 경우에는 하위 노드를 업데이트할 수 있는지 확인하고 업데이트할 수 있으면 업데이트하고, 그렇지 않은 경우 원래 Element 하위 노드를 삭제하고 새 노드를 생성합니다.

if (hasSameSuperclass && child.widget == newWidget) {
  if (child.slot != newSlot)
    updateSlotForChild(child, newSlot);
  newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
  if (child.slot != newSlot)
    updateSlotForChild(child, newSlot);
  child.update(newWidget);
  assert(child.widget == newWidget);
  assert(() {
    child.owner._debugElementWasRebuilt(child);
    return true;
  }());
  newChild = child;
} else {
  deactivateChild(child);
  assert(child._parent == null);
  newChild = inflateWidget(newWidget, newSlot);
}

위 코드의 8행은 매우 중요합니다. child.update 함수 로직에서 현재 노드의 유형에 따라 다른 업데이트가 호출됩니다. 그림 3의 업데이트 아래 프로세스를 참조할 수 있습니다. 각 프로세스는 또한 하위 노드를 재귀적으로 실행하고 updateChild로 루프백합니다. 다음 세 가지 핵심 함수, 즉 PerformRebuild, inflateWidget 및 markNeedsBuild가 updateChild 프로세스에 다시 진입합니다. 다음으로 이 세 가지 함수의 구체적인 기능을 살펴보겠습니다.

  • PerformRebuild는 매우 중요한 코드입니다. 이 부분은 우리가 컴포넌트에 작성하는 빌드 로직 함수입니다. StatelessWidget 및 StatefulWidget의 빌드 함수가 여기에서 실행됩니다. 실행이 완료되면 노드의 하위 노드로 사용됩니다. updateChild 재귀 함수를 입력합니다.

  • inflateWidget은 새로운 노드를 생성하며, 생성이 완료되면 현재 Element 유형에 따라 RenderObjectElement 또는 ComponentElement로 판단됩니다. 두 가지 유형의 차이점에 따라 현재 노드에 마운트하기 위해 다른 마운트를 호출하는데, 두 가지 유형의 마운트에서는 하위 노드를 순환하고 updateChild를 호출하여 하위 노드 업데이트 프로세스에 다시 들어갑니다. 여기에는 또 다른 점이 있습니다. RenderObjectElement인 경우 RenderObject가 생성됩니다.

  • markNeedsBuild를 더티로 표시하고, 다음 buildScope 작업을 기다리려면 ScheduleBuildFor를 호출하세요.

위의 내용은 보다 중요한 기능 중 일부이며, 기타 기능에 대해서는 공식 웹사이트 문서를 직접 확인하실 수 있습니다. 다음으로 그림 3의 흐름도와 결합하여 첫 번째 빌드 프로세스와 setState 프로세스라는 두 프로세스를 결합하여 설명하겠습니다.

첫 번째 빌드

처음으로 페이지 컴포넌트를 로드할 때 모든 노드가 존재하지 않기 때문에 이때 대부분의 프로세스는 그림 4와 같이 새 노드를 생성하는 것입니다.

그림4.png

그림 4 첫 번째 빌드 프로세스

runApp에서 RenderObjectToWidgetElement(mount)까지의 로직은 동일하며 _rebuild에서는 updateChild를 호출하여 노드를 업데이트하게 되는데, 해당 노드가 존재하지 않기 때문에 이때 inflateWidget을 호출하여 Element를 생성하게 된다.

Element가 Component인 경우 Component.mount가 호출됩니다. Element는 Component.mount에 생성되어 현재 노드에 마운트됩니다. 두 번째로 _firstBuild가 호출되어 하위 구성 요소를 빌드합니다. 빌드가 완료된 후 빌드된 구성 요소가 사용됩니다. 하위 구성 요소로 Component, updateChild의 하위 구성 요소 업데이트를 입력합니다.

Element가 RenderObjectElement인 경우 RenderObjectElement.mount가 호출되며, RenderObjectElement.mount에서는 RenderObjectElement가 생성되고 createRenderObject가 호출되어 RenderObject가 생성되며, RenderObject와 RenderObjectElement가 Element 트리와 RenderObject 트리에 마운트된다. 각각 현재 노드 마지막으로 동일한 호출이 updateChild로 수행되어 하위 노드를 재귀적으로 생성합니다.

위는 첫 번째 빌드의 로직으로 단독으로 보면 매우 명확하며, 다음으로 setState의 로직을 살펴보겠습니다.

setState

setState를 호출할 때 실제로 컴포넌트의 markNeedsBuild를 호출하며, 이 함수는 컴포넌트를 더티로 설정하고, 다음 buildScope의 로직을 추가하고, 다음 재구축 주기를 기다리기 위해 위에 소개되었습니다. 프로세스는 그림 5에 나와 있습니다. buildScope는 다시 빌드를 호출한 다음 빌드 작업을 시작하여 updateChild 루프 시스템으로 들어갑니다.

그림6.png

그림 5 setState 프로세스

그림 5에서 우리는 Flutter에서 상위 노드가 업데이트되면, 즉 RenderObject 트리가 반드시 생성될 필요는 없지만 setState 호출로 인해 하위 노드의 재귀 루프가 빌드 논리를 결정하게 된다는 것을 이해할 수 있습니다( 하위 노드가 생성되지 않을 수 있기 때문입니다.) 노드가 변경되지 않았으므로 변경 사항은 없습니다.) 그러나 여전히 성능에 어느 정도 영향을 미칩니다.

위는 Threetrees의 변환 과정으로, 핵심이 아닌 프로세스 기능 중 일부를 생략하였습니다. 관심 있는 분은 Flutter 공식 홈페이지 Github 에서 학습 하실 수 있습니다. 전체 프로세스를 마스터한 후, 다음으로 이 변환 프로세스에서 Flutter APP의 성능을 향상시킬 수 있는 핵심 사항을 추출하겠습니다.

성능 향상을 위한 핵심 포인트

그림 3의 전체 프로세스에서 우리는 Widget에서 Element, RenderObject까지 Flutter의 성능 향상의 핵심 포인트이기도 한 updateChild 함수에 특별한 주의를 기울여야 합니다. 위에서 이 함수의 기능을 소개했는데, 중요한 점은 Widget을 Element로 변환한 후 Element를 RenderObject로 변환하는 과정에서 세부적인 판단과 최적화를 하는 것이며, 이러한 세부 처리에는 다음과 같은 5가지 사항이 포함된다.

  • 새 노드가 삭제되면 요소 노드를 직접 삭제하세요.

  • 노드가 존재하고 구성 요소 유형이 동일하며 구성 요소가 동일하면 노드 위치가 업데이트됩니다.

  • 노드가 존재하면 컴포넌트의 종류는 같지만 컴포넌트가 달라서 컴포넌트를 업데이트할 수 있으면 컴포넌트가 업데이트되는데, 현재 컴포넌트가 업데이트되므로 현재 컴포넌트의 하위 노드도 업데이트해야 하므로 update는 하위 노드 목록을 업데이트하기 위해 호출되며, 이 프로세스에서 해당 노드의 RenderObject의 하위 노드도 업데이트됩니다.

  • 노드가 존재하는 경우 컴포넌트의 종류는 같지만 컴포넌트가 다르며, 둘째로 컴포넌트를 업데이트할 수 없어 노드가 생성되며, 생성 과정에서 RenderObject인지 판단하게 된다. RenderObject가 생성되고 하위 노드가 루프에서 판단됩니다.

  • 노드가 존재하지 않는 경우 생성 프로세스는 동일합니다.

이런 방식으로 Widget이 RenderObject에 미치는 영향을 줄일 수 있으며, 생성 및 업데이트가 필요한 노드만 RenderObject 트리에 반영됩니다. 이 트리 노드의 변환 과정에서 우리는 APP의 성능을 향상시키기 위해 다음 네 가지 핵심 포인트를 추출할 수 있습니다.

const

위에서 언급했듯이 상위 구성 요소를 업데이트하면 하위 구성 요소를 다시 빌드해야 합니다. 한 가지 방법은 상태 저장 구성 요소 아래에 있는 하위 구성 요소의 수를 줄이는 것이고, 또 다른 방법은 가능한 한 많은 const 구성 요소를 사용하는 것입니다. 상위 구성 요소가 업데이트되더라도 하위 구성 요소는 업데이트되지 않습니다. 재구축 작업이 다시 수행됩니다. 위의 판단 로직은 다음과 같습니다. 노드가 존재할 때 처리 로직은 컴포넌트 유형이 동일하며 컴포넌트가 동일합니다.

우리 프로젝트의 소스 코드에는 몇 가지 실용적인 최적화 포인트도 있는데, 특히 일반 오류 보고 구성 요소, 일반 로딩 구성 요소 등 오랫동안 수정되지 않은 일부 구성 요소는 물론 변수가 없는 구성 요소에 대해서만 반환될 수 있습니다. , 예를 들어 코드의 다음 부분과 같습니다.

if (error) {
    return CommonError(action: this.setFirstPage);
  }
  if (contentList == null) {
    return const Loading();
  }
}

코드의 두 번째 줄에는 변수 action이 있으므로 const로 설정할 수 없으며, 다음 Loading은 변수를 전달하지 않으므로 const로 설정할 수 있습니다. 다른 코드도 같은 방식으로 수정할 수 있으며, 이는 특히 구성 요소 설계가 불합리한 경우 성능을 향상하는 데 도움이 됩니다.

업데이트할 수 있음

위의 updateChild 과정에는 실행 함수인 canUpdate가 있는데, 이 역시 성능 향상의 핵심 포인트이며, 특히 여러 요소를 조정해야 하는 경우 구체적인 로직 구현을 살펴볼 수 있습니다.

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}

주요 목적은 런타임 시 클래스가 동일한지 확인하는 동시에 키가 동일한지 확인하는 것이며, 동일할 경우 컴포넌트 Element 위치를 직접 업데이트하여 성능을 향상시킬 수 있으므로 설계 시 기본적으로 비어 있도록 설정될 수 있는 구성 요소의 키 변경을 최소화하도록 노력하세요.

둘째, 구성 요소를 자주 정렬, 삭제 또는 추가해야 하는 경우 구성 요소에 키를 추가하여 성능을 향상시키는 것이 가장 좋습니다. 여기서 주의할 점은 StatefulWidget의 상태가 Element에 저장되어 있기 때문에 동일한 클래스명(runtimeType)을 가진 두 Widget을 구별하려면 서로 다른 키를 가지고 다녀야 하며, 그렇지 않으면 이전 Widget과 새 Widget의 변화를 구분할 수 없기 때문입니다. 특히 목록에서 데이터, 각 목록은 상태 저장 클래스입니다. 목록의 항목 목록을 전환해야 하는 경우 키를 설정해야 하며, 그렇지 않으면 시퀀스 전환이 실패합니다. 이에 대해 자세히 알아보려면 이 영어 기사를 참조하세요 .

팽창위젯

updateChild의 inflateWidget 실행 기능도 핵심 성능 개선 포인트인데, 이 함수는 생성 전 키가 GlobalKey인지 확인하는데, 만약 그렇다면 Element가 존재한다는 의미이므로 이때 바로 활성화할 수 있다. 존재하지 않으면 다시 생성해야 합니다. , 이는 컴포넌트 캐싱과 유사하며 컴포넌트의 빌드 비용만 줄일 수 있습니다. 다음 코드 부분을 살펴보세요.

final Key key = newWidget.key;
if (key is GlobalKey) {
  final Element newChild = _retakeInactiveElement(key, newWidget);
  if (newChild != null) {
    assert(newChild._parent == null);
    assert(() {
      _debugCheckForCycles(newChild);
      return true;
    }());
    newChild._activateWithParent(this, newSlot);
    final Element updatedChild = updateChild(newChild, newWidget, newSlot);
    assert(newChild == updatedChild);
    return updatedChild;
  }
}
final Element newChild = newWidget.createElement();

하지만 이 부분 역시 컴포넌트를 메모리에 캐시해 가비지 콘텐츠가 재활용되지 않기 때문에 메모리 소모가 매우 크기 때문에 GlobalKey 사용 시 주의가 필요하며, 재사용성이 높고 빌드가 복잡한 컴포넌트에 적용하도록 노력해야 합니다. 사업.

setState

그림 3의 setState가 트리거된 후 현재 구성 요소는 재구축 작업을 수행합니다. 현재 구성 요소의 빌드로 인해 현재 구성 요소 아래의 모든 하위 구성 요소의 재구축 동작이 발생하므로 설계 시 Stateful의 Stateless를 최소화하도록 노력하십시오. 구성 요소를 사용하여 불필요한 빌드 논리를 줄입니다. 앞서 언급한 컴포넌트 디자인 포인트 중 일부이기도 합니다.Flutter는 상대적으로 위젯과 요소를 구축하는 속도가 빠르지만 성능을 위해 이 부분에서는 여전히 불필요한 손실을 줄이려고 노력합니다. 둘째, 특히 상태 저장 구성 요소의 경우 Flutter의 빌드가 종종 트리거되므로 빌드에서 비즈니스 로직을 줄이는 데 주의하세요.

요약하다

본 수업은 Flutter의 3-트리 개념부터 3-트리 대응까지 Flutter의 셀프 렌더링에서 3개 트리에 대한 지식을 집중적으로 다루고, 3-트리 변환 프로세스에 중점을 두고, 그 과정에서 성능 최적화를 위한 핵심 사항을 요약합니다. 주의할 점.

이 수업을 마친 후에는 Flutter의 3-트리 개념을 마스터하고 3-트리 변환 과정을 매우 명확하게 이해해야 합니다. 변환 과정에서 성능 최적화에 대한 지식을 학습함으로써 코딩 중에 매우 좋은 코드를 개발하게 됩니다. 프로세스, 코딩 습관.

이 강의의 소스 코드를 보려면 링크를 클릭하세요.

추천

출처blog.csdn.net/g_z_q_/article/details/129719002