[Перевод] Flutter Hot Reload: Что под капотом?
Привет, Хабр! Меня зовут Станислав Чернышев, я автор книги «Основы Dart», телеграм-канала MADTeacher и доцент кафедры прикладной информатики в Санкт-Петербургском государственном университете аэрокосмического приборостроения.
В этот раз сделал перевод статьи с Medium, в которой пошагово рассматривается процесс горячей перезагрузки в Flutter — «Flutter Reload: What«s Under the Hood». Его лучше всего отнести к разряду вольных, т.е. он не дословный и отбрасывает некоторый авторский текст, сокращая и преобразуя его в тех местах, где это не критично для смысла.
Введение
Горячая перезагрузка (Hot Reload) в Flutter — одна из киллер-фич фреймворка, позволяющая практически мгновенно вносить изменения в разрабатываемое приложение в режиме отладки, без необходимости его перекомпиляции и повторного запуска. Но вы когда-нибудь задумывались, что на самом деле происходит в момент нажатия на кнопку перезагрузки? Давайте глубже погрузимся в внутренние механизмы данного процесса в Flutter и раскроем уличную магию этой мощной функции фреймворка.
Flutter hot reload
Примечание: все примеры кода в статье взяты непосредственно из репозиториев Flutter и его движка (Flutter Engine). Для большей ясности описываемых действий и лучшего понимания происходящего в них, в код были внесены небольшие правки.
*****
Чтобы понять весь процесс, для начала представим стадии, которые проходит Hot Reload в виде следующей диаграммы:
Стадии Hot Reload
А теперь разберем каждую стадию представленного процесса.
1. Пользователь нажимает на кнопку «Горячая перезагрузка»
Когда вы нажимаете на кнопку «Hot Reload» или сохраняете изменения в IDE, при запущенном в эмуляторе приложении, в работу вступает класс HotRunner из пакета инструментария Flutter. Он берет на себя управление и отвечает за организацию последующих стадий процесса. Другими словами, HotRunner:
проверяет, находится ли приложение в состоянии, допускающем «Горячую перезагрузку».
определяет, какие файлы были изменены с момента последней компиляции.
взаимодействует с виртуальной машиной Dart с помощью VM Service Protocol.
Future restart({...}) async {
final List updatedFiles = await _getChangedFiles();
final bool success = await _reloadSources(updatedFiles);
if (!success) {
return OperationResult(1, 'Hot reload failed');
}
// ...
}
Ссылка: hot_runner.dart
2. Обновление исходного кода (внедрение кода Dart VM)
Dart VM получает обновленный исходный код и делает его инъекцию в работающее приложение. На этой стадии:
Создаются новые версии измененных библиотек.
Для эффективной обработки обновлений используется механизм копирования при записи (copy-on-write).
Старые версии кода продолжают храниться в памяти, что позволяет, в случае необходимости, обеспечить возможность отката.
Future _reloadSources(List files) async {
final vm_service.VM vm = await _vmService.getVM();
final vm_service.IsolateRef isolateRef = vm.isolates.first;
final vm_service.ReloadReport report = await _vmService.reloadSources(isolateRef.id);
return report.success;
}
Ссылка: app_snapshot.cc
3. Компиляция кода с использованием JIT
Компилятор Dart VM, работающий по принципу Just-In-Time (JIT), быстро компилирует новый код (измененные библиотеки), используя многоуровневую компиляцию. Такой подход позволяет постепенно оптимизировать самые горячие участки кода, используя такие методы, как встроенное кэширование и обратная связь по типу (type feedback).
На текущей стадии JIT-компилятор старается использовать как можно большую часть из предыдущей компиляции, тем самым ускоряя процесс.
class DartVM {
Future compileJIT(List modifiedLibraries) async {
for (var library in modifiedLibraries) {
await _jitCompiler.compile(library);
}
}
}
Ссылка: compiler.cc
4. Сравнение старых и новых деревьев виджетов
Фреймворк Flutter сравнивает старое дерево виджетов с новым. На этой стадии определяются виджеты, которые нужно обновить. Для этого используется дерево элементов, которое является согласующим слоем Flutter между виджетами и объектами рендеринга, к которым применяется эффективный алгоритм сравнения, способный обрабатывать идентификаторы виджетов и сохраненные состояния.
Следует отметить, что сравнение деревьев оптимизировано таким образом, чтобы минимизировать количество необходимых перестроек.
class Element {
void update(Widget newWidget) {
_widget = newWidget;
markNeedsBuild();
}
void markNeedsBuild() {
if (!_dirty) {
_dirty = true;
owner.scheduleBuildFor(this);
}
}
}
Ссылка: widget_framework.dart
5. Определение и перестройка затронутых виджетов
На основе результатов предыдущей стадии сравнения, Flutter определяет, какие виджеты нуждаются в перестройке и составляет график их перестройки. Здесь на сцене блистает класс BuildOwner. Он управляет текущей стадией процесса горячей перезагрузки, ведет список «грязных» элементов, нуждающихся в перестройке, и запускает сам процесс перестройки, используя обход в глубину по дереву виджетов. В ходе перестроения объекты State сохраняются там, где это возможно, чтобы поддерживать текущее состояние приложения (вспоминаем про приколы с ключами и StatefulWidget).
class BuildOwner {
void buildScope(Element context, [VoidCallback callback]) {
_dirtyElements.sort(Element._sort);
_dirtyElements.reversed.forEach((Element element) {
if (element._dirty)
element.rebuild();
});
}
}
Ссылка: widgets_framework.dart
6. Управление памятью и сборка мусора
На текущей стадии процесса горячей перезагрузки запускается сборщик мусора Dart и удаляет из кучи объекты, которые больше не нужны. В случае с удалением виджетов, задействуются механизмы Flutter для правильной утилизации принадлежащих им ресурсов. Также Flutter старается, где это возможно, повторно использовать существующие объекты. Такой подход минимизирует риск переполнения памяти.
class State {
@protected
void dispose() {
// Clean up resources here
}
}
Ссылка: dart_heap.cc
7. Обновление пользовательского интерфейса в реальном времени
На последнем шаге Flutter, а точнее — класс RendererBinding, обновляет пользовательский интерфейс и отражает на нем изменения, внесенные во время горячей перезагрузки. Так как Flutter использует систему рендеринга с сохранением режима, это позволяет ему эффективно обновлять только те части пользовательского интерфейса, которые изменились.
class RendererBinding extends BindingBase with SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding {
void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame();
}
}
Ссылка: rendering_binding.dart
Заключение
Hot Reload в Flutter — мощная функция, которая значительно ускоряет разработку. Она мгновенно применяет изменения кода без потери состояния приложения, позволяя в процессе разработки быстро проводить итерации и эксперименты. Такие сложные процессы, как: инъекция кода, эффективное сравнение деревьев виджетов и интеллектуальное обновление пользовательского интерфейса — явно говорят о том, как много команда Flutter уделяет внимания производительности разработчиков. Несмотря на некоторые ограничения, «горячая перезагрузка» показывает, как продуманное инженерное решение может значительно улучшить рабочий процесс разработки приложений, что приводит к созданию более качественных и отполированных приложений.
Перевод подготовил затраханный за лето препод, ведущий телеграм-канал MADTeacher, посвященный Dart/Flutter и безумству нашей системы высшего образования