前言熟练使用Flutter开发APP的人,一定熟悉各种Widget的使用,但往往对Flutter是如何编译的,对应的产品有哪些知之甚少。在这篇文章中,我们来了解一下Flutter编译的相关知识。一、FlutterArchitectureLayerFlutter架构主要分为三层:1.Framework层基于Dart实现,主要包括两种风格的Widgets:MaterialDesign(Google)和Cupertino(iOS)。文本/图像/按钮等。小部件。渲染/动画/绘图/手势等小部件。核心基础类/方法主要是指Flutter仓库下的Flutter包,sky_engine仓库下的io、async、ui等包(dart:ui库提供了Flutter框架和引擎的接口)。2.Engine层基于C++实现,主要包括Skia开源的二维图形库,提供了适用于多种软硬件平台的通用API。Dart主要包括DartRuntime、GarbageCollection(GC),如果是Debug模式,还包括JIT(JustInTime)支持。在Release和Profile模式下,AOT(AheadOfTime)被编译成nativearm代码,没有JIT部分。Text文字渲染,渲染层级如下:libtxt库衍生自minikin(用于字体选择,行分隔);HartBuzz用于字形选择和整形;Skia用作渲染/GPU后端,在Android和Fuchsia上使用FreeType渲染,在iOS上使用CoreGraphics渲染字体。3、Embedder层Embedder是一个嵌入层,将Flutter嵌入到各种平台中。这里的主要工作包括渲染Surface设置、线程设置和插件。从这里可以看出Flutter的平台相关层很低。平台(如iOS)只提供画布,其余渲染相关逻辑全部在Flutter内部,这使得它具有很好的跨终端一致性。基于以上架构的了解,我们要想了解Flutter是如何编译的,首先要从Engine层入手。我们知道编程语言需要经过编译才能达到可运行的目的,比如Android开发中的.java->.class->.dex。我们编写.java文件,层层编译解释,最终成为机器可识别的机器语言。那么我们在日常的Flutter开发中使用Dart语言来开发.dart文件。一路上经历了怎样的风风雨雨,终于得到了机器的认可。2.Flutter编译模式上面提到的“编译模式”的概念简单来说就是:将一个源程序翻译成机器码让机器开始工作,然后在不同的编译模式下运行这段机器码。主要区别在于何时、何地以及如何执行上述两个步骤,这往往导致同一段代码“生效”的速度不同。那么根据需求和硬件环境选择合适的编译方式可以提高我们程序的运行效率,但是什么样的编译方式合适呢?我们先来看看常见的编译模式。一般来说,编译模式分为两种:JIT和AOT1.JITJIT代表JustInTime(即时编译),意思是源代码可以在运行时编译执行。一个典型的例子是v8引擎,它可以即时编译和运行JavaScript。一般来说,支持JIT的语言都可以支持内省功能。优点:采用JIT方式编译,可以在不考虑用户使用的机器架构的情况下,动态地交付和执行代码,从而达到动态地为用户提供丰富内容的目的。缺点:运行时编译必然会消耗时间和内存,给用户带来的直接感受就是应用启动缓慢。2、AOTAOT的全称是AheadOfTime(提前编译),意思是将源代码提前编译成机器码,用户机器在运行时直接执行相应的机器码。一个典型的例子就是C/C++,LLVM或者GCC编译生成C/C++二进制代码,然后这些二进制代码被用户安装并获得执行权限后,可以通过进程加载执行。优点:预先编译好的二进制代码,加载和执行的速度非常快。因此,编程语言运行速度排名靠前的是C、C++、Rust等编译型语言。这样的速度,在大型游戏的引擎渲染、逻辑执行等密集计算场景下,都能给用户带来非常好的体验。缺点:编译需要区分用户机器的架构,生成不同架构的二进制代码。除了架构之外,二进制代码本身也会让用户下载的安装包比较大。二进制代码一般需要获得执行权限才能执行,所以在权限比较严格的系统,比如iOS,是不能动态更新的。虽然我们会使用JIT和AOT来区分语言的类型,但其实很多语言并不是只使用JIT或AOT,通常它们会混合使用这两种模式来达到最大的性能优化(Java就是这样,所以Java有时被称为半编译半解释语言)。因为Flutter使用的是Dart作为编程语言,所以如果我们想了解Flutter的编译模式,我们先来看看Dart的编译模式。三、Dart编译方式1、DartVM类似于JVM,Dart也有对应的DartVM,我们可以理解为Dart虚拟机。它为Dart提供了一个执行环境。除了解释执行或JIT编译外,还可以利用Dart虚拟机的AOT管道将Dart代码编译成机器码,然后运行在简化版的DartVM上,称为预编译运行时(precompiledruntime)环境,不包含任何编译器组件,不能动态加载Dart源代码。2.Dart编译方式DartVM有多种执行代码的方式:脚本:最常见的JIT方式,可以直接在虚拟机中执行Dart源代码,像解释型语言一样使用,直接调用dartxxx.darton要执行的命令行dart源代码就是这种模式。KernelSnapshots:以前也叫ScriptSnapshots。它采用JIT模式。与Script模式不同,此模式执行KernelAST的二进制数据。它不包括已解析的类和函数、已编译的代码,因此它们可以在不同平台之间移植。DartKernel是Dart程序的中间语言。它是一种紧凑的二进制目标文件格式,支持单独的编译和链接。它是程序转换的基础设施。通过执行dart--snapshot-kind=kernel--snapshot=xx.snapshotxx.dart生成。JITApplicationSnapshots:JIT模式,这种模式执行的是解析出来的类和数据,所以需要区分架构,但是相应的数据会跑得更快。通过执行dart--snapshot-kind=app-jit--snapshot=xx.snapshotxx.dart生成。AOTApplicationSnapshots:AOT模式,在这种模式下,Dart源代码会被提前编译成平台特定的二进制文件。总结Dart的编译模式编译模式区分架构打包大小动态启动时间ScriptJIT最慢KernelSnapshotsJIT最小,slowJITApplicationSnapshots更大更快AOTApplicationSnapshotsAOT最大没有最快3。Flutter编译方式Flutter完全采用了Dart。按理说编译模式应该是一样的,但事实并非如此。原因是Flutter的开发要考虑到Android和iOS平台的生态差异,所以在Dart编译方式的基础上做了一些改动:Script:与DartScript方式一致。Flutter虽然支持,但是暂时不用,因为影响启动速度。KernelSnapshot:Dart的字节码模式,字节码模式不区分架构。KernelSnapshot在Flutter项目中也被称为CoreSnapshot。字节码模式可以归类为AOT编译。CoreJIT:Dart的一种二进制模式,将指令代码和堆数据打包成文件,然后在vm和isolate启动时加载,并直接将内存标记为可执行。可以说这是一种AOT模式。核心JIT也称为AOTBlob。AOTAssembly:Dart的AOT模式。直接生成汇编源码文件,由各个平台进行汇编。可见Flutter将Dart的编译方式复杂化了,增加了很多概念。为了理解这些概念,我们从Flutter应用开发的各个阶段来解读。4.Flutter不同阶段的编译模式Flutter支持三种编译应用程序的模式,即Debug模式:对应Dart中的JIT模式。该模式支持真机和模拟器,支持断点,支持服务扩展。为快速开发和运行周期编译和优化(执行速度、包大小、部署未优化),调试工具可以连接到进程,热重载功能。发布模式:对应Dart中的AOT模式。该模式只支持真实设备,关闭所有断点和调试信息,禁止调试,优化启动速度、执行速度和包大小,不能使用热重载功能。Profile模式:与Release模式基本类似,但保留了一些调试功能,分析app的性能,但只能在真机上使用,因为模拟器的性能分析不地道。启用了一些服务扩展。例如,支持性能叠加。三、Flutter编译流程1、Flutter运行初步了解了Flutter的编译模式之后,我们来看看Flutter的编译流程。以Android为例,当我们在AndroidStudio中点击运行按钮时,默认会执行Flutter运行命令。当Flutterrun命令后面不带任何参数时,默认使用debug模式。我们可以在Flutter运行命令中添加参数。更改相应的编译模式,如:Flutterrun-release运行release模式。Flutter运行过程涉及到一些Flutter相关的命令,它们之间的关系如下:Flutter命令的整个过程位于目录Flutter/packages/flutter_tools/下,整个Flutter运行过程主要包括以下几个核心functions:Flutterbuildapk:viagradle构建APKFlutterbuildaot:构建AOT编译产物frontend_server:前端编译器生成内核文件gen_snapshot:将dart代码编译成AOT产物Flutterbuildbundle:将相关文件放入flutter_assets目录通过adbinstall安装APK通过adbamstart启动整个应用程序,详细执行过程,如图:其中,我们比较关心的是这两个步骤和相关的编译产物:frontend_server:前端编译器生成内核文件gen_snapshot:将dart代码编译成AOT产品2.frontend_server命令KernelCompiler.compile()过程等价于th如下命令:可以看出,frontend_server.dart.snapshot是由dart虚拟机启动的,dart代码以app.dill的形式转化为内核文件。frontend_server前端编译器将dart代码转换为AST并生成app.dill文件,其中字节码生成过程默认是关闭的。3.gen_snapshot命令GenSnapshot.run具体命令根据之前的包在Android和iOS平台有所不同:对于Android:对于iOS:上面的命令主要是将dart内核转为机器码,对应的流程图在这里gen_snapshot是一个Binary可执行文件,主要功能是将dart代码生成AOT二进制机器码。四、Flutter编译产品大致了解了Flutter的编译流程后,我们就可以知道Flutter在不同平台、不同模式下的编译产品有哪些:1.iOS-release模式:结构与原生APP差别不大,而Frameworks文件文件夹中主要有App.framework和Flutter.framework。App.framework中的flutter_assets文件夹存放了Flutter中引用的资源文件。2.Android-release模式:结构上和原生APP没有太大区别,不同的是assets中有多个flutter_assets存放Flutter引用的资源文件,lib中多了libapp.so和libflutter.so。3、iOS-debug模式:App.framework文件夹下多了isolate_snapshot_data、kernel_blob.bin、vm_snapshot_data,App的二进制文件只有33KB,release模式下有8.5MB。4.Android-调试模式:flutter_assets下多了isolate_snapshot_data、kernel_blob.bin、vm_snapshot_data。lib下缺少libapp.so。产品概要FlutterAPP最终会包含两个库,一个是dart代码编译出来的app库,一个是engine代码编译出来的Flutter库。为了实现hotreload,在debug模式下,dart代码至少会生成3个,这三个文件是每次编译时动态生成的,可以在运行时替换,实现hotreload。在发布模式下,这三个文件将合并到应用程序库中。Flutter库的大小在调试和发布模式下也不同。isolate_snapshot_data:用于加速isolate、业务无关代码的启动kernel_blob.bin:业务代码的产物原始资源文件和Flutter资源文件分离。5.我们能做什么?那么,了解了Flutter的相关编译原理之后,我们可以做什么呢?结合平台特性优化Flutter相关产品内容的体积,从而减小包体积;从Flutter方向拆分模块,优化编译速度,减少打包时间;探索Flutter动态和FlutterforWeb等更多可能性。当然,这些进一步的探索还需要我们更多的学习和实践。更多精彩,敬请关注我们的公众号《百瓶科技》,不定期有福利哦!
