Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Сборка кадра в 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'),
),
],
);
}
}
Что происходит:
setState()помечает виджет как "грязный" (dirty)- Flutter вызывает
build()метод - Создаётся новое дерево виджетов
- Старое дерево сравнивается с новым (reconciliation)
- Только изменённые части отмечаются для пересчёта
Время: 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 в ширину
Что происходит:
- Для каждого виджета вызывается его layout logic
- Вычисляется размер (size) на основе constraints
- Вычисляется позиция (offset)
- Все размеры сохраняются в 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;
}
Что происходит:
- Для каждого RenderObject вызывается
paint()метод - Отрисовывается содержимое (текст, картинки, фигуры)
- Всё отрисовывается в display list — промежуточное представление
- Display list передаётся Skia
Время: 5-15 мс (самая тяжёлая часть)
7. Этап 5: COMPOSITE (GPU и слои)
Scalar (система слоёв) и GPU объединяют всё вместе и отправляют на экран.
Display List -> Skia (C++) -> OpenGL/Vulkan -> GPU -> Экран
Что происходит:
- Display list преобразуется в GPU команды
- Все слои (Opacity, Transform и т.д.) применяются
- Результат отправляется на GPU
- GPU рисует на буфере (double buffering)
- Готовый буфер показывается на экране
Время: 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 — это сложный, многоэтапный процесс:
- VSYNC — сигнал от GPU
- BUILD — перестройка дерева виджетов (Dart)
- LAYOUT — расчёт размеров
- PAINT — рисование на display list
- COMPOSITE — отправка на GPU
Понимание этих этапов критично для оптимизации производительности приложения. Используй DevTools, профилируй, и оптимизируй узкие места!