Flutter 热重载

6,117 阅读6分钟

概念

什么是热重载

热重载是指,在不中断 App 正常运行的情况下,动态注入修改后的代码片段。

  • Flutter 的热重载功能可帮助您在无需重新启动应用程序的情况下快速添加功能以及修复错误。通过将更新的源代码文件注入到正在运行的 Dart 虚拟机(VM) 来实现热重载。在虚拟机使用新的字段和函数更新类之后, Flutter 框架会自动重新构建 widget 树,以便您可以快速查看更改的效果
  • iOS 热重载是通过注入动态库的方式实现的

解释器 & 编译器

  • 解释器:运行时才去解析代码
    • 解释器会在运行时解释执行代码,获取一段代码后就会将其翻译成目标代码(就是字节码(Bytecode)),然后一句一句地执行目标代码。解释器,是在运行时才去解析代码,这样就比在运行之前通过编译器生成一份完整的机器码再去执行的效率要低。
    • 解释器可以在运行时去执行代码,说明它具有动态性,程序运行后能够随时通过增加和更新代码来改变程序的逻辑。
  • 编译器:编译时,编译器把代码编译成机器码,然后直接在 CPU 上执行机器码的
    • 链接器:最主要的作用,就是将符号绑定到地址上

那么,使用编译器和解释器执行代码的特点,我们就可以概括如下

  • 采用编译器生成机器码执行的好处是效率高,缺点是调试周期长。
  • 解释器执行的好处是编写调试方便,缺点是执行效率低。

1594886745171-cd087e62-4dc4-4e7a-9c02-b728664b1911.png

Flutter编译方式

Flutter在Debug和Relase执行不同的编译模式

  • JIT:Just In Time . 动态解释,一边翻译一边执行,也称为即时编译,如JavaScript,Python等,在开发周期中使用,可以动态下发和执行代码,开发测试效率高,但是运行速度和性能则会受到影响,Flutter中的热重载正是基于此特性

  • AOT: Ahead of Time. 静态编译,是指程序在执行前全部被翻译为机器码,提前编译,如 C ,C++ ,OC等,发布时期使用AOT,就不需要像RN那样在跨平台JavaScript代码和原生Android、iOS代码间建立低效的方法调用映射关系。

iOS App 如何通过注入动态库的方式实现极速编译调试

现在苹果公司使用的编译器是 LLVMLLVM 是编译器工具链技术的一个集合。而其中的 lldb 项目,就是内置链接器。

  • 编译器会对每个文件进行编译,生成 Mach-O(可执行文件)
  • 链接器会将项目中的多个 Mach-O 文件合并成一个。

在真实的 iOS 开发中,你会发现很多功能都是现成可用的,不光你能够用,其他 App 也在用,比如 GUI 框架、I/O、网络等。链接这些共享库到你的 Mach-O 文件,也是通过链接器来完成的

链接的共用库分为静态库和动态库

  • 1、静态库是编译时链接的库
    • 需要链接进你的 Mach-O 文件里,如果需要更新就要重新编译一次,无法动态加载和更新
  • 2、动态库是运行时链接的库,使用 dyld 就可以实现动态加载。

Mach-O 文件是编译后的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接。

所以 Mach-O 文件中并没有包含动态库里的符号定义。也就是说,这些符号会显示为“未定义”,但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。

dlopen 会把共享库载入运行进程的地址空间,载入的共享库也会有未定义的符号,这样会触发更多的共享库被载入。dlopen 也可以选择是立刻解析所有引用还是滞后去做。dlopen 打开动态库后返回的是引用的指针,dlsym 的作用就是通过 dlopen 返回的动态库指针和函数符号,得到函数的地址然后使用

借助Injection实现即时编译

为了使iOS app能够即时编译,我们可以借助一个工具Injection

Injection 是怎么做到的呢

Injection 会监听源代码文件的变化,如果文件被改动了,Injection Server 就会执行 rebuildClass 重新进行编译、打包成动态库,也就是 .dylib 文件。编译、打包成动态库后使用 writeSting 方法通过 Socket 通知运行的 App

1595244088796-72b8fc68-9d44-47d7-bc29-e3d598b307aa.png

Flutter 热重载

2dfbedae7b95dd152a587070db4bb9fa_900x528.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,无需重启应用即可看到改动效果,从而大大提升了 UI 调试效率。

不过,Flutter 的热重载也有一定的局限性。因为涉及到状态保存与恢复,所以并不是所有的代码改动都可以通过热重载来更新。

接下来,我就与你介绍几个不支持热重载的典型场景:

  • 代码出现编译错误;

  • Widget 状态无法兼容;

  • 全局变量和静态属性的更改;

  • main 方法里的更改;

  • initState 方法里的更改;

  • 枚举和泛类型更改。