Привет, Хабр! Меня зовут Станислав Чернышев, я автор книги «Основы Dart», телеграм-канала MADTeacher и доцент кафедры прикладной информатики в Санкт-Петербургском государственном университете аэрокосмического приборостроения.
В этот раз сделал перевод статьи с Medium, в которой пошагово рассматривается процесс горячей перезагрузки в Flutter — «Flutter Reload: What’s Under the Hood«. Его лучше всего отнести к разряду вольных, т.е. он не дословный и отбрасывает некоторый авторский текст, сокращая и преобразуя его в тех местах, где это не критично для смысла.
Введение
Горячая перезагрузка (Hot Reload) в Flutter — одна из киллер-фич фреймворка, позволяющая практически мгновенно вносить изменения в разрабатываемое приложение в режиме отладки, без необходимости его перекомпиляции и повторного запуска. Но вы когда-нибудь задумывались, что на самом деле происходит в момент нажатия на кнопку перезагрузки? Давайте глубже погрузимся в внутренние механизмы данного процесса в Flutter и раскроем уличную магию этой мощной функции фреймворка.
Примечание: все примеры кода в статье взяты непосредственно из репозиториев Flutter и его движка (Flutter Engine). Для большей ясности описываемых действий и лучшего понимания происходящего в них, в код были внесены небольшие правки.
*****
Чтобы понять весь процесс, для начала представим стадии, которые проходит Hot Reload в виде следующей диаграммы:
А теперь разберем каждую стадию представленного процесса.
1. Пользователь нажимает на кнопку «Горячая перезагрузка»
Когда вы нажимаете на кнопку «Hot Reload» или сохраняете изменения в IDE, при запущенном в эмуляторе приложении, в работу вступает класс HotRunner из пакета инструментария Flutter. Он берет на себя управление и отвечает за организацию последующих стадий процесса. Другими словами, HotRunner:
-
проверяет, находится ли приложение в состоянии, допускающем «Горячую перезагрузку».
-
определяет, какие файлы были изменены с момента последней компиляции.
-
взаимодействует с виртуальной машиной Dart с помощью VM Service Protocol.
Future<OperationResult> restart({...}) async { final List<Uri> 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<bool> _reloadSources(List<Uri> 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<void> compileJIT(List<Library> 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<T extends StatefulWidget> { @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 и безумству нашей системы высшего образования
ссылка на оригинал статьи https://habr.com/ru/articles/843486/
Добавить комментарий