[Перевод] 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 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 и безумству нашей системы высшего образования

© Habrahabr.ru