Flutter Hot Reload: Что под капотом?

от автора

Привет, Хабр! Меня зовут Станислав Чернышев, я автор книги «Основы Dart», телеграм-канала MADTeacher и доцент кафедры прикладной информатики в Санкт-Петербургском государственном университете аэрокосмического приборостроения.

В этот раз сделал перевод статьи с Medium, в которой пошагово рассматривается процесс горячей перезагрузки в Flutter — «Flutter Reload: What’s Under the Hood«. Его лучше всего отнести к разряду вольных, т.е. он не дословный и отбрасывает некоторый авторский текст, сокращая и преобразуя его в тех местах, где это не критично для смысла.


Введение

Горячая перезагрузка (Hot Reload) в Flutter — одна из киллер-фич фреймворка, позволяющая практически мгновенно вносить изменения в разрабатываемое приложение в режиме отладки, без необходимости его перекомпиляции и повторного запуска. Но вы когда-нибудь задумывались, что на самом деле происходит в момент нажатия на кнопку перезагрузки? Давайте глубже погрузимся в внутренние механизмы данного процесса в Flutter и раскроем уличную магию этой мощной функции фреймворка.

Flutter hot reload

Flutter hot reload

Примечание: все примеры кода в статье взяты непосредственно из репозиториев Flutter и его движка (Flutter Engine). Для большей ясности описываемых действий и лучшего понимания происходящего в них, в код были внесены небольшие правки.

*****

Чтобы понять весь процесс, для начала представим стадии, которые проходит Hot Reload в виде следующей диаграммы:

Стадии Hot Reload

Стадии 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *