Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как работает рендеринг в Flutter: Практический взгляд
За предыдущий вопрос я объяснил архитектуру. Здесь расскажу о механике: как именно Flutter преобразует виджеты в пиксели, этап за этапом, с акцентом на производительность и практику.
1. Инициализация рендеринга
Всё начинается при запуске приложения.
void main() {
runApp(MyApp()); // Запускается рендеринг
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
// Что происходит внутри:
// 1. Flutter создаёт RenderView (корневой RenderObject)
// 2. Вызывает build() MyApp
// 3. Конвертирует MaterialApp в RenderObject'ы
// 4. Запускает цикл VSYNC
2. VSYNC-управляемый цикл
Все рендеры синхронизированы с экраном через VSYNC.
┌────────────────────┐
│ VSYNC сигнал (60Hz)│ ← от GPU "готов новый кадр"
└─────────┬──────────┘
↓
┌─────────────┐
│ Build Phase │
└─────────────┘
↓
┌─────────────┐
│Layout Phase │
└─────────────┘
↓
┌─────────────┐
│Paint Phase │
└─────────────┘
↓
Показать на экране
Eсли что-то займет > 16.67мс (60FPS), кадр пропустится.
3. Build Phase в деталях
Этап, где создаётся дерево виджетов и RenderObject'ы.
// Flutter вызывает build() каждый раз при изменении
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int counter = 0;
@override
Widget build(BuildContext context) {
// ⚠️ Этот метод вызывается ДЕ ЧАСТО
// При setState, при получении нового Widget parent'а, и т.д.
return Scaffold(
body: Center(
child: Column(
children: [
Text('Count: $counter'), // Создаётся RenderParagraph
ElevatedButton(
onPressed: () => setState(() => counter++),
child: Text('Increment'), // Ещё один RenderParagraph
),
],
),
),
);
}
}
// Build phase создаёт эту иерархию:
// RenderView
// └─ RenderScaffold
// ├─ RenderAppBar
// └─ RenderCenter
// └─ RenderFlex (Column)
// ├─ RenderParagraph (Text)
// └─ RenderConstrainedBox (Button)
// └─ RenderParagraph (Button text)
Оптимизация Build Phase:
// ❌ Создаёшь новые объекты каждый раз
WidgetToReuse buildComplexWidget() {
return WidgetToReuse(
child: ExpensiveDataProcess(),
);
}
// ✅ Используй const
const complexWidget = WidgetToReuse(
child: ExpensiveDataProcess(),
);
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return complexWidget; // Переиспользуется
}
}
4. Layout Phase: Вычисление размеров
Каждый RenderObject вычисляет свой размер на основе constraints.
// Процесс Layout:
// 1. Root RenderView получает constraints (размер экрана)
// 2. Каждый RenderObject вычисляет свой размер
// 3. Размеры проходят вверх, constraints вниз
abstract class RenderBox extends RenderObject {
// Constraints, полученные от родителя
late BoxConstraints constraints;
// Вычисленный размер
late Size size;
// Метод, который вычисляет размер
void performLayout() {
// Реализация в подклассах
// Например, в RenderParagraph:
// textPainter.layout(maxWidth: constraints.maxWidth);
// size = constraints.constrain(textPainter.size);
}
}
// Пример: Container с Text
Container(
width: 300,
color: Colors.blue,
child: Text('Hello'),
)
// Layout процесс:
// 1. Container получает constraints (например, maxWidth: 500)
// 2. Container вычисляет: хочу 300 в ширину
// 3. Text получает constraints: maxWidth: 300
// 4. Text вычисляет: мне нужно 80 пикселей (для текста)
// 5. Результат:
// - Container: 300x(высота зависит от Text)
// - Text: 80x(его высота)
Дорогие операции Layout:
// ❌ Дорого: глубокое вложение
Column(
children: [
Container(
child: Row(
children: [
Container(
child: Column(
// 4 уровня глубины = 4 Layout pass'а
),
),
],
),
),
],
)
// ✅ Лучше: плоская структура
Column(
children: [
MyComplexWidget(),
MyComplexWidget(),
],
)
5. Paint Phase: Рисование
Каждый RenderObject рисует себя на Canvas.
class RenderContainer extends RenderBox {
@override
void paint(PaintingContext context, Offset offset) {
// Paint по порядку:
// 1. Фон
if (decoration != null) {
decoration.paint(
context.canvas,
Rect.fromLTWH(
offset.dx,
offset.dy,
size.width,
size.height,
),
);
}
// 2. Дети
if (child != null) {
context.paintChild(child!, offset + childOffset);
}
// 3. Border
if (decoration != null) {
decoration.paintBorder(...);
}
}
}
// Пример на Canvas
canvas.drawRect(
Rect.fromLTWH(10, 10, 100, 100),
Paint()..color = Colors.blue,
);
// Это вызывает Skia C++ код, который говорит GPU:
// "Раскрась пиксели с (10,10) до (110,110) в синий"
Оптимизация Paint Phase:
// ❌ Перерисовывается весь граф при каждом изменении
class BadPerformance extends StatefulWidget {
@override
State<BadPerformance> createState() => _BadPerformanceState();
}
class _BadPerformanceState extends State<BadPerformance> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $counter'), // Перерисовывается
Text('Static text'), // Перерисовывается зря!
Text('Another text'), // Перерисовывается зря!
],
);
}
}
// ✅ Лучше: изолируй перерисовку
class GoodPerformance extends StatefulWidget {
@override
State<GoodPerformance> createState() => _GoodPerformanceState();
}
class _GoodPerformanceState extends State<GoodPerformance> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
CounterDisplay(counter: counter), // Перерисовывается
const StaticContent(), // НЕ перерисовывается
],
);
}
}
class StaticContent extends StatelessWidget {
const StaticContent();
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Static text'),
Text('Another text'),
],
);
}
}
6. Display List
Результат Paint phase — это Display List (промежуточное представление).
Paint Phase создаёт Display List (в памяти):
[
DrawRect(...),
DrawText(...),
DrawCircle(...),
...
]
Этот список передаётся Skia (C++), который:
1. Оптимизирует команды
2. Преобразует в GPU команды
3. Отправляет на GPU
7. Слои (Layers) и Compositing
Для эффектов (Opacity, Transform) используются слои.
// ❌ Без слоя: каждый раз пересчитывается
Transform(
transform: Matrix4.identity()..setEntry(3, 2, 0.001),
alignment: Alignment.center,
child: MyWidget(),
)
// ✅ С слоем: более оптимально
RepaintBoundary(
child: Transform(
transform: Matrix4.identity()..setEntry(3, 2, 0.001),
alignment: Alignment.center,
child: MyWidget(),
),
)
// В Opacity это критично:
// ❌ Перестраивает дерево
Opacity(opacity: 0.5, child: HeavyWidget())
// ✅ Лучше: используй AnimatedOpacity
AnimatedOpacity(
opacity: 0.5,
duration: Duration(milliseconds: 300),
child: HeavyWidget(),
)
8. Практический пример: Профилирование рендеринга
# Запусти в profile mode
flutter run --profile
# Включи Performance Overlay
# В коде или DevTools Timeline
import 'package:flutter/foundation.dart';
// Добавь в MaterialApp
showPerformanceOverlay: !kReleaseMode,
// Или явно отслеживай
SchedulerBinding.instance.addPersistentFrameCallback((_) {
print('Frame took: ${DateTime.now()}')
});
9. Обход дерева
Render tree обходится в определённом порядке.
Pre-Order DFS (глубина сначала):
Root
├─ FirstChild
│ ├─ GrandChild1
│ └─ GrandChild2
├─ SecondChild
└─ ThirdChild
Порядок вычисления Layout:
1. Root performLayout()
2. FirstChild performLayout()
3. GrandChild1 performLayout()
4. GrandChild2 performLayout()
5. SecondChild performLayout()
6. ThirdChild performLayout()
10. Batching изменений
Flutter группирует изменения в один кадр.
// ❌ Может быть 3 кадра
setState(() => a++);
setState(() => b++);
setState(() => c++);
// ✅ Один кадр
setState(() {
a++;
b++;
c++;
});
11. Целая картина
APP START
↓
CreateRenderView
↓
VSYNC сигнал → Frame начинается
↓
BUILD: build() → RenderObject иерархия
↓
LAYOUT: performLayout() → размеры
↓
PAINT: paint() → Display List
↓
COMPOSIT: Skia + GPU
↓
Экран обновляется
↓
Следующий VSYNC → повторить
12. Ключевые моменты
Build часто, Paint редко:
- Build может вызваться много раз (setState, didUpdateWidget)
- Paint вызывается только если что-то визуально изменилось
- Layout вызывается только если размеры изменились
Используй const:
- const виджеты не перестраиваются
- const конструкторы = нет новых объектов
Избегай дорогих операций:
- В build(): нет вычислений
- В layout(): нет IO операций
- В paint(): максимум оптимизации
Вывод
Рендеринг в Flutter работает через:
- VSYNC синхронизация — 60 кадров в секунду
- Build — создание RenderObject иерархии
- Layout — вычисление размеров
- Paint — рисование на Canvas
- Composit — отправка на GPU
Чтобы приложение было быстрым:
- Минимизируй Build work (используй const)
- Профилируй Layout (избегай глубокого дерева)
- Оптимизируй Paint (используй RepaintBoundary)
- Понимай constraints и размеры
Flutter делает это всё автоматически, но понимание механики критично для высокопроизводительных приложений.