← Назад к вопросам

Как происходит сборка кадра?

1.7 Middle🔥 201 комментариев
#Архитектура Flutter

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Сборка кадра в Flutter: Как отрисовывается экран

Это один из самых важных механизмов для понимания производительности Flutter приложений. Каждый кадр (frame) на экране — результат точно скоординированной работы нескольких систем. Объясню этот процесс подробно.

1. Что такое кадр?

Кадр — это одна отрисованная картинка на экране. На современных устройствах экран обновляется 60 раз в секунду (60 FPS = 60 кадров), или 120 FPS на некоторых устройствах.

Бюджет времени:

  • При 60 FPS: каждый кадр должен рисоваться за 16.67 мс
  • При 120 FPS: каждый кадр должен рисоваться за 8.33 мс

Если кадр занимает больше времени, он пропускается и видна рассинхронизация (jank).

2. Жизненный цикл кадра

Flutter проходит 5 основных этапов при сборке каждого кадра:

┌─────────────────┐
│  1. VSYNC сигнал│ (Vertical Sync - сигнал от GPU)
└────────┬────────┘
         ↓
┌─────────────────────────┐
│ 2. BUILD (Dart слой)    │ Перестраивание дерева виджетов
└────────┬────────────────┘
         ↓
┌─────────────────────────┐
│ 3. LAYOUT (Расчёт)      │ Вычисление размеров и позиций
└────────┬────────────────┘
         ↓
┌─────────────────────────┐
│ 4. PAINT (Рисование)    │ Отрисовка визуальных элементов
└────────┬────────────────┘
         ↓
┌─────────────────────────┐
│ 5. Compositor (GPU)     │ Слияние слоёв и отправка на экран
└─────────────────────────┘

3. Этап 1: VSYNC сигнал

Всё начинается с сигнала от GPU (Vertical Sync). Он говорит: "экран готов показать новый кадр".

// Ты не можешь явно получить VSYNC, но он запускает весь процесс
// Это происходит 60 раз в секунду автоматически

Время: < 1 мс

4. Этап 2: BUILD (Дартовый слой)

Этап, где вызываются методы build() виджетов и перестраивается дерево виджетов.

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    // ЭТО ВЫЗЫВАЕТСЯ во время BUILD этапа
    // Может вызваться часто если setState вызывается часто
    return Column(
      children: [
        Text('$counter'),
        ElevatedButton(
          onPressed: () {
            setState(() {
              counter++; // Запускает перестройку
            });
          },
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Что происходит:

  1. setState() помечает виджет как "грязный" (dirty)
  2. Flutter вызывает build() метод
  3. Создаётся новое дерево виджетов
  4. Старое дерево сравнивается с новым (reconciliation)
  5. Только изменённые части отмечаются для пересчёта

Время: 0-10 мс (зависит от сложности дерева)

❌ Анти-паттерн:

// НЕ ДЕЛАЙ ТАК - это медленно
class BadWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 10000,
      itemBuilder: (context, index) {
        // ⚠️ Создаёшь новый объект каждый кадр
        final complexObject = ExpensiveWidget();
        return complexObject;
      },
    );
  }
}

// ✅ ЛУЧШЕ:
class GoodWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 10000,
      itemBuilder: (context, index) => ExpensiveWidget(key: ValueKey(index)),
    );
  }
}

5. Этап 3: LAYOUT (Расчёт размеров)

Вычисляются размеры и позиции всех виджетов на основе constraints (как мы обсуждали ранее).

// Вспомним: constraints проходят от родителя к child
Scaffold(300x500)
  └─ Column -> передаёт constraints детям
      └─ Text -> понимает, что может быть до 300 в ширину
      └─ Button -> также до 300 в ширину

Что происходит:

  1. Для каждого виджета вызывается его layout logic
  2. Вычисляется размер (size) на основе constraints
  3. Вычисляется позиция (offset)
  4. Все размеры сохраняются в RenderObject

Время: 0-5 мс

6. Этап 4: PAINT (Рисование)

Нарисовка всех визуальных элементов с использованием Skia графической библиотеки.

class CustomPainter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: MyPainter(),
      size: Size(200, 200),
    );
  }
}

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // PAINT этап - вызывается здесь
    canvas.drawRect(
      Rect.fromLTWH(0, 0, 100, 100),
      Paint()..color = Colors.blue,
    );
  }

  @override
  bool shouldRepaint(MyPainter oldDelegate) => false;
}

Что происходит:

  1. Для каждого RenderObject вызывается paint() метод
  2. Отрисовывается содержимое (текст, картинки, фигуры)
  3. Всё отрисовывается в display list — промежуточное представление
  4. Display list передаётся Skia

Время: 5-15 мс (самая тяжёлая часть)

7. Этап 5: COMPOSITE (GPU и слои)

Scalar (система слоёв) и GPU объединяют всё вместе и отправляют на экран.

Display List -> Skia (C++) -> OpenGL/Vulkan -> GPU -> Экран

Что происходит:

  1. Display list преобразуется в GPU команды
  2. Все слои (Opacity, Transform и т.д.) применяются
  3. Результат отправляется на GPU
  4. GPU рисует на буфере (double buffering)
  5. Готовый буфер показывается на экране

Время: 1 мс (в большинстве случаев)

8. Полный цикл

VSYNC (GPU готов)
  ↓ (0 мс)
BUILD (Dart: setState → build) (0-10 мс)
  ↓
LAYOUT (Расчёт размеров) (0-5 мс)
  ↓
PAINT (Рисование display list) (5-15 мс)
  ↓
COMPOSIT (GPU отправка) (1 мс)
  ↓
ЭКРАН показывает кадр (16.67 мс общего времени)

Если что-то заняло > 16.67 мс → кадр пропущен → видна рассинхронизация (jank)

9. Отладка производительности

// Включить отладку производительности
flutter run --profile

// В DevTools смотреть Timeline
// Или через код:
import 'package:flutter/scheduler.dart';

SchedulerBinding.instance.addTimingsCallback((List<Duration> timings) {
  for (Duration timing in timings) {
    print('Frame took: ${timing.inMilliseconds} ms');
  }
});

10. Как оптимизировать?

Оптимизация BUILD этапа:

// Кэширование сложных виджетов
const expensiveWidget = MyComplexWidget();

// Использование const конструкторов
class MyWidget extends StatelessWidget {
  const MyWidget(); // const = не перестраивается
}

Оптимизация LAYOUT этапа:

// Избегай глубокого дерева виджетов
// Избегай вложенных SingleChildScrollView

Оптимизация PAINT этапа:

// Избегай дорогих операций
bool shouldRepaint(MyPainter oldDelegate) => false; // не перерисовывать

// Используй RepaintBoundary для изоляции
RepaintBoundary(
  child: ExpensiveWidget(),
)

Оптимизация COMPOSITE этапа:

// Избегай частых Transform и Opacity изменений
// Используй AnimatedBuilder вместо setState
AnimatedBuilder(
  animation: animationController,
  builder: (context, child) => Transform.rotate(
    angle: animationController.value,
    child: child,
  ),
  child: MyWidget(), // Это не пересчитывается
)

11. Smooth 60 FPS

// Полный цикл должен быть < 16.67 мс
// BUILD: 2 мс
// LAYOUT: 1 мс
// PAINT: 10 мс
// COMPOSITE: 2 мс
// ИТОГО: 15 мс ✅ (в бюджете)

// ПЛОХО:
// BUILD: 12 мс (много setState)
// LAYOUT: 4 мс
// PAINT: 12 мс
// COMPOSITE: 2 мс
// ИТОГО: 30 мс ❌ (2 кадра теряем)

Вывод

Сборка кадра в Flutter — это сложный, многоэтапный процесс:

  1. VSYNC — сигнал от GPU
  2. BUILD — перестройка дерева виджетов (Dart)
  3. LAYOUT — расчёт размеров
  4. PAINT — рисование на display list
  5. COMPOSITE — отправка на GPU

Понимание этих этапов критично для оптимизации производительности приложения. Используй DevTools, профилируй, и оптимизируй узкие места!

Как происходит сборка кадра? | PrepBro