flutter热重载机制

569 阅读7分钟

背景

我们在原生开发中,经常性出现的场景就是等待编译,为了验证修改后的运行效果,必须要经过漫长的重新编译,才能同步到设备上,并且随着你工程的规模增加,编译时长也持续增加,这就诞生了组件化,二进制的一些操作,这里先不谈这个。

但是在flutter中,由于debug模式下支持JIT,并且为开发期的运行和调试提供了大量优化,因此代码修改后,我们可以通过亚秒级的热重载(hot reload)进行增量代码的快速刷新,而无需经过全量的代码编译,从而大大缩短了从修改到验证结果所需要的时间。

那flutter的热重载到底是如果实现的呢?

热重载

热重载就是在不中断App正常运行的情况下,动态注入修改后的代码。这个能力的背后,离不开flutter所提供的运行时编译能力。为了更好的理解flutter的热重载实现原理,我们先简单回顾一下flutter编译模式背后的技术。

  • JIT(Just In Time),指的是即时编译编译或运行时编译,在debug模式中使用,可以动态下发和执行代码,启动速度快,但是执行性能受运行时编译影响。

image.png

  • AOT(Ahead Of Time),指的是提前编译或运行前编译,在release模式下使用,可以为特定的平台生成稳定的二进制代码,执行性能好,运行速度快,但每次执行需要提前编译,开发调试效率低。

image.png

从上面的示意图,我们可以看到,flutter提供的两种编译模式中,AOT是静态编译,即编译成设备可执行的二进制码。而JIT是动态编译,即将Dart代码编译成中间代码(Script Snapshot),在运行时设备需要dart VM解释执行。

热重载之所以只能在debug模式下使用,是因为在debug模式下,flutter采用的是JIT动态编译,而在release模式下,采用的是AOT静态编译。JIT编译器将Dart代码编译成可以运行在dart VM上的Dart Kernel,而Dart Kernel是可以动态更新的,这就实现了代码的实时更新功能。

image.png

总结来说,热重载的流程可以分为扫描工程改动、增量编译、推送更新、代码合并、Widget重建5个步骤:

1.工程改动。热重载模块会注意扫描工程中的文件,检查是否有新增、删除或者改动,直到找到在上次编译之后,发生变化的Dart代码。

2.增量编译。热重载模块将发生变化的Dart代码,通过编译转化为增量的Dart Kernel文件。

3.推送更新。热重载模块将增量的Dart Kernel文件通过HTTP端口,发送给正在移动设备商运行的Dart VM。

4.代码合并。Dart VM会将收到的增量Dart Kernel文件,与原有的Dart Kernel文件进行合并,然后重新加载新的Dart Kernel文件

5.Widget重建。在确认Dart VM资源加载成功后,Flutter会将其UI线程重置,通知Flutter Framework重建Widget

可以看到,flutter提供的热重载在收到代码变更后,并不会让App重新启动执行,而只会触发widget树的重绘机制,因此可以保持改动前的状态,这就大大节省了调试复杂交互界面的时间。

不支持热重载的场景

flutter提供的亚秒级热重载一直是开发者的调试利器。通过热重载我们可以快速修改UI,调试bug。但是flutter的热重载也有一定的局限性,因为涉及到状态的保存与回复,所以并不是所有的代码改动都可以通过热重载来更新的。

下面几个就是不支持热重载的典型场景:

  • 代码出现编译错误
  • widget状态无法兼容
  • 全局变量和静态属性的更改
  • main方法里的更改
  • initState方法里的更改
  • 枚举和泛类型更改

下面我们对这几个典型场景做一下具体说明,遇到这些情况如何解决

代码出现编译错误

这个很好理解,就是更改代码导致的无法编译通过,此时热重载会提示错误信息,更正代码就可以继续使用热重载了

Widget状态无法兼容

当代码更改会影响Widget的状态时,会使得热重载前后Widget所使用的数据不一致,即应用程序保留的状态与新的更改不兼容,此时热重载是无法使用的。

//改动前
class MyWidget extends StatelessWidget {
  Widget build(BuildContext context) {
    return GestureDetector(onTap: () => print('T'));
  }
}

//改动后
class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> { /*...*/ }

这种情况下,我们需要重启应用,才能使更新的内容生效。

全局变量和静态属性的变更

在flutter中,全局变量和静态属性都被视为状态,在第一次运行应用程序是,会将他们的值设为初始化语句执行的结果,因此在热重载期间不会重新初始化。

比如下面的代码中,我们修改了一个静态 Text 数组的初始化元素。虽然热重载并不会报错,但由于静态变量并不会在热重载之后初始化,因此这个改变并不会产生效果:

//改动前
final sampleText = [
  Text("T1"),
  Text("T2"),
];

//改动后
final sampleText = [
  Text("T1"),
  Text("T10"), // 改动点
];

如果我们需要更改全局变量和静态属性的初始化语句,重启应用才能查看更改效果。

main方法里的修改

在 flutter 中,由于热重载之后只会根据原来的根节点重新创建控件树,因此 main 函数的任何改动并不会在热重载后重新执行。所以,如果我们改动了 main 函数中的代码,是无法通过热重载看到更新效果的。

//更新前
class MyAPP extends StatelessWidget {
@override
  Widget build(BuildContext context) {
    return const Center(child: Text('Hello World', textDirection: TextDirection.ltr));
  }
}

void main() => runApp(new MyAPP());

//更新后
void main() => runApp(const Center(child: Text('Hello, 2022', textDirection: TextDirection.ltr)));

由于 main 函数并不会在热重载后重新执行,因此以上改动是无法通过热重载查看更新的.

initState方法里的更改

在热重载时,flutter 会保存 Widget 的状态,然后重建 Widget。而 initState 方法是 Widget 状态的初始化方法,这个方法里的更改会与状态保存发生冲突,因此热重载后不会产生效果。

在下面的例子中,我们将计数器的初始值由2021改为2022:

//更改前
class _MyHomePageState extends State<MyHomePage> {
  int _counter;
  @override
  void initState() {
    _counter = 2021;
    super.initState();
  }
  ...
}

//更改后
class _MyHomePageState extends State<MyHomePage> {
  int _counter;
  @override
  void initState() {
    _counter = 2022;
    super.initState();
  }
  ...
}

由于这样的改动发生在 initState 方法中,因此无法通过热重载查看更新,我们需要重启应用,才能看到更改效果.

枚举和泛类型更改

在 flutter 中,枚举和泛型也被视为状态,因此对它们的修改也不支持热重载。比如在下面的代码中,我们将一个枚举类型改为普通类,并为其增加了一个泛型参数:


//更改前
enum Color {
  red,
  green,
  blue
}

class C<U> {
  U u;
}

//更改后
class Color {
  Color(this.r, this.g, this.b);
  final int r;
  final int g;
  final int b;
}

class C<U, V> {
  U u;
  V v;
}

这两类更改都会导致热重载失败,并生成对应的提示消息。同样的,我们需要重启应用,才能查看到更改效果。

总结

Flutter 的热重载是基于 JIT 编译模式的代码增量同步。由于 JIT 属于动态编译,能够将 Dart 代码编译成生成中间代码,让 Dart VM 在运行时解释执行,因此可以通过动态更新中间代码实现增量同步。而另一方面,由于涉及到状态保存与恢复,因此涉及状态兼容与状态初始化的场景,热重载是无法支持的,比如改动前后 Widget 状态无法兼容、全局变量与静态属性的更改、main 方法里的更改、initState 方法里的更改、枚举和泛型的更改等。

可以发现,热重载提高了调试 UI 的效率,非常适合写界面样式这样需要反复查看修改效果的场景。但由于其状态保存的机制所限,热重载本身也有一些无法支持的边界。