Flutter主要有三种运行模式
- debug : 主要用于调试代码,可以在真机和模拟器上同时运行。我们开发时运行的正是此模式。该模式下代码执行性能较差,特别是在模拟器下。
- profile:只能在真机上运行,不能在模拟器上运行。基本和Release模式一致,不同之处在于可以获取一些性能数据,用于性能测试。
- release:只能在真机上运行。对性能进行了优化,减小了安装包体积大小。发布给用户使用时正式此模式,以达到最佳的性能体验。
在发布之前,可以执行以下命令测试应用的启动时间(需真机)
flutter run --trace-startup --profile
将在Flutter工程目录在build目录下生成start_up_info.json文件,输出列出了从应用程序启动到这些跟踪事件(单位:微秒)所用的时间:
- 进入Flutter引擎时
- 展示应用第一帧时
- 初始化Flutter框架时
- 完成Flutter框架初始化时
另外,在开发中遇到一些BUG,我们可以使用Android Studio的断点调试功能,通过打断点来调试应用
更多调试工具和技巧,参见 调试文档
当我们需要发布App时,检查MaterialApp中的debugShowCheckedModeBanner是否设置为false,发布时需要去除屏幕上的debug横幅
Android
在生成正式的安卓安装包前,首先需要检查权限配置
打开项目下的\android\app\src\main\AndroidManifest.xml文件,检查是否已经配置相关权限。例如,通常debug模式默认配置网络权限,而发布模式则没有,当我们的app有网络访问需求时,需要添加如下内容
<uses-permission android:name="android.permission.INTERNET"/>
除此之外,从Android9.0开始,必须强制使用HTTPS协议,否则无法访问网络。如果我们app中使用明文的HTTP协议,则还需要配置一个属性,开启HTTP支持。在application标签中配置如下属性
android:usesCleartextTraffic="true"
以release模式生成APK文件
flutter build apk --release
# 也可省略release参数
flutter build apk
需要注意,直接使用以上命令,会生成包含32位和64位二进制文件的胖APK
为了减小安装包体积大小,可以使用如下命令打包。这将生成一个仅支持arm芯片的64位版本安装包。
flutter build apk --target-platform=android-arm64
该命令其他可选的参数android-arm、android-x64,分别代表仅生成支持arm 32位和Intel 64位芯片的安装包。
另外,也可以使用分包命令一次生成两个安装包。生成的两个包分别为arm 32 和 arm 64
flutter build apk --split-per-abi
关于签名
- 打开原生工程,选择【build】=》【generate signed bundle or apk】,可通过图形化程序生成一个签名文件
- 配置签名文件。打开【File】=》【Project Stucture】通过图形化方式配置签名文件
生成签名文件后,可以使用keytool -list -v -keystore app.jks来查看签名文件的信息(替换app.jks为自己的签名文件)
iOS
发布iOS应用到应用商店较为麻烦,需要花费99美元,注册Apple开发者计划,具体步骤可参见Flutter 官方文档
配置真机调试环境
环境安装参见Flutter 环境准备篇
配置Apple ID和证书
打开【Xcode】 -> 【Preferences】 -> 【Account】。添加 AppleId
手机连上电脑,确定手机的AppleID和电脑一致。选择【window】->【Devices and simulator】
在真机上启动调试。【设置】-【通用】-【设备管理】选择开发者并点击信任。
需注意,连接手机后,请全程保持手机为解锁状态。
这里编译使用flutter build ios命令生成iOS的包,但是内部测试时不建议使用该命令。因为生成的是未经优化的安装包,体积非常大,需要上传到Apple的App Store进行安全审查,通过之后会再次优化,此时产生的才是最终发布给用户的版本,安装包体积会大幅度减小。
那么我们想在内部测试时进行打包,则可以使用flutter build ios --profile命令,相比之下,该命令生成的包体积大小,最接近最终发布的安装包体积大小。
需要注意,以上命令生成的都是一个.app的包,而不是iOS的.ipa安装包,这里我们还需要一些步骤进行处理
- 创建一个名为
Payload的文件夹 - 拷贝生成的
.app包到Payload文件夹中 - 以
.zip格式压缩Payload文件夹,生成一个压缩文件包Payload.zip - 修改文件后缀名,例如
Product.ipa
经过以上步骤,我们就得到了一个iOS的安装包
Web
想要发布Web版本,首先检查项目对Web的兼容情况,然后执行以下命令,发布版将生成在工程目录下的/build/web中
flutter build web
服务器线上部署时,我们将生成的web目录拷贝出来,然后使用静态文件服务器进行配置,这里推荐使用Nginx。当然,我们也可以使用Aqueduct快速实现一个静态文件服务器,可参见 Aqueduct 文件服务 的文档
创建一个静态文件服务器项目
pub global run aqueduct create file_service
添加代码
@override
Controller get entryPoint {
final router = Router();
// 将路由映射到指定的文件夹
router.route("/*").link(() => FileController("web/"));
return router;
}
运行服务
pub global run aqueduct serve
访问 http://localhost:8888/
框架实现原理
简版架构图
- Framework:一个纯 Dart代码的 SDK。它实现了一套基础库, 包含动画、绘制和手势处理。并基于绘制封装了一套Widget控件库,这套控件库还根据
Material和Cupertino两种设计风格进行了风格化区分。 - Engine:一个 C++实现的 SDK。其包含了 Skia引擎、Dart运行时、文字排版引擎等。在安卓上,系统自带了Skia,在iOS上,则需要APP打包Skia库,这会导致Flutter开发的iOS应用安装包体积更大。 Dart运行时则可以以 JIT、JIT Snapshot 或者 AOT的模式运行 Dart代码。
其中 dart:ui库是对Engine中Skia库的C++接口的绑定。向上层提供了 window、text、canvas等通用的绘制能力,通过 dart:ui库就能使用Dart代码操作Skia绘制引擎。所以我们实际上可以通过实例化dart:ui包中的类(例如Canvas、Paint等)来绘制界面。然而,除了绘制,还要考虑到协调布局和响应触摸等情况,这一切实现起来都异常麻烦,这也正是Framework帮我们做的事。
渲染层Rendering是在dart:ui库之上的第一个抽象层,它为你做了所有繁重的数学工作(如跟踪计算坐标等)。为了做到这一点,它使用RenderObject对象,该对象是真正绘制到屏幕上的渲染对象。由这些RenderObject组成的树处理真正的布局和绘制。
在Engine之下,还包含一层Shell。这个单词是 “壳”的意思,这个壳组合了Dart运行时、第三方工具库、平台特性等,实现在不同平台调用和运行 Flutter应用。
总的来说, dart:ui给 Dart提供了绘制能力,Dart运行时为 Flutter提供了执行Dart代码的能力,而Shell将他们组合起来,并且将生成的数据渲染到不同的平台。
UI 构建原理
Flutter 的渲染流水线
屏幕上的每一帧的绘制过程,实际上是Engine通过接收的VSync信号不断地触发帧的绘制
从构建到渲染流程
构建UI体系的四棵树
Widget
是 Flutter中UI开发的基本单元。 一个Widget里面通常存储了视图的配置信息,包括布局、属性等。我们可以把它理解为一个UI元素的配置文件,类似于原生开发中的xml描述文件。所谓Widget树,就是我们手动编写的结构化的Widget代码,当被加载到内存时,就形成了Widget树。
Flutter中,除了Widget树是我们自己编写的,另外三棵树都需要借助Widget树构建出来。
Element
该对象实际上是一个上下文,将Widget与RenderObject映射关联起来。通过遍历Widget控件树来构建一个Element树结构。在原生开发中没有对应的概念,它的概念更接近于Web前端中的虚拟DOM,主要做的事情也是比较前后两次Widget的差异来决定如何更新真实的渲染对象树(RenderObject树)。
RenderObject
真正的渲染对象。最接近原生开发中的UI控件元素。它主要处理UI构建过程中的布局与绘制。它依赖于Element树来生成一颗RenderObject树。
Layer
该对象表示的是图层的意思。通常一棵RenderObject树经过绘制之后,就会生成一个Layer对象,但并不是所有RenderObject都会绘制到一个Layer中,某些情况下,例如不同路由页面,就会绘制到不同的Layer图层中。这些Layer对象组成的结构就是Layer树。
在绘制时,会根据 isRepaintBoundary是否为 true来决定是否绘制到新的图层。了解这一点,我们就可以使用RepaintBoundary 控件在外层包裹,然后通过设置该控件的isRepaintBoundary属性来提升绘制性能。因为 isRepaintBoundary 为 true 时,会形成了独立的 Layer,这样其他控件发生频繁的改变时,就不会影响到独立的图层,这个独立的图层也不会发生重绘,节省性能开销。
最后的最后,所有图层都交由 GPU 来负责合成并上屏显示。在渲染流程的最后两个步骤中,正是合成与光栅化。
UI构建过程
光栅化
合成已经理解了,那么什么是光栅化呢?
光栅化也称栅格化,是指将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上的过程。光栅化的本质是坐标变换、几何离散化。
相关类分析
BuildContext
一个指向小控件树中小控件位置的句柄。见BuildContext 文档
BuildContext对象被传递给WidgetBuilder函数(如StatelessWidget.build),并可从State.context成员中获得。一些静态函数(如showDialog、Theme.of等)也采用BuildContext,以便它们可以代表调用小控件执行操作,或获取特定于给定上下文的数据
每个widget都有自己的BuildContext,它成为StatelessWidget.build或State.build函数返回的那个widget的父节点。(同样地,也是RenderObjectWidget的任何子节点的父节点。)
需要特别注意,build方法所在的widget的BuildContext与该build方法返回的widget的BuildContext是不同的。如果需要那个被返回的控件树的子控件的BuildContext,可以使用 Builder 小控件,则传递给 Builder.builder 回调的BuildContext将是 Builder 本身的上下文。
随着小控件在树上移动,特定小控件的 BuildContext 可以随时间改变位置。正因为如此,从该类上的方法返回的值不应在执行单个同步函数之后被缓存。BuildContext对象实际上就是Element对象。BuildContext接口是用来阻止直接操作Element对象的。
BuildContext的常见方法
-
dependOnInheritedWidgetOfExactType({Object aspect}) → T获取给定类型T的最近的widget,该widget必须是一个具体的InheritedWidget子类的类型,并将此构建上下文注册到该widget中,这样当该widget发生变化时(或引入该类型的新widget,或该widget消失),此构建上下文将被重建,以便它可以从该widget中获取新的值。
-
findAncestorRenderObjectOfType() → T返回最近的祖先
RenderObjectWidget小控件的RenderObject对象,该对象是给定类型 T 的实例。不应从build方法中使用此方法,调用此方法比较耗费性能。 -
findAncestorStateOfType<T extends State<StatefulWidget>>() → T返回给定类型T的实例的最近祖先
StatefulWidget控件的State对象。不应从build方法中使用此方法,调用此方法比较耗费性能。 -
findAncestorWidgetOfExactType() → T返回给定类型T的最近祖先Widget,它必须是具体Widget子类的类型。调用此方法比较耗费性能。
-
findRenderObject() → RenderObject当前小控件的
RenderObject。如果小控件是RenderObjectWidget,则这是小控件为其自身创建的渲染对象。否则,它是第一个后代RenderObjectWidget的渲染对象 。 -
findRootAncestorStateOfType<T extends State<StatefulWidget>>() → T返回给定类型 T 的最远祖先
StatefulWidget控件的State对象。 -
getElementForInheritedWidgetOfExactType() → InheritedElement获取给定类型T的最近的widget对应的元素,它必须是一个具体的
InheritedWidget子类的类型。如果没有找到这样的元素,则返回null。 -
visitAncestorElements(bool visitor(Element element)) → void从此
context的小控件的父级开始遍历祖先链,为每个祖先调用参数。回调被赋予对祖先小控件的相应Element对象的引用。当到达根部小控件或回调返回false时,遍历停止。回调不得返回null。这对于检查小控件树很有用。 -
visitChildElements(ElementVisitor visitor) → void遍历此小控件的子级。这对于在子节点被构建后应用更改而无需等待下一帧非常有用,尤其是当子节点是已知的,且只有一个子节点时(StatefulWidgets 或 StatelessWidgets 总是如此)。递归调用此方法非常耗费性能,应尽可能避免。
Element
Element实际上就是BuildContext的实现类。见Element 文档
Element的属性
| 属性 | 类型 | 简述 |
|---|---|---|
| depth | int | 保证大于父级的整数(如果有)。树根元素的深度必须大于0。 |
| dirty | bool | 如果该元素被标记为需要重建,则返回true。 |
| owner | BuildOwner | 管理该Element生命周期的对象。 |
| renderObject | RenderObject | 树中此位置(或下方)的渲染对象。如果此对象是一个RenderObjectElement,那么渲染对象就是树中这个位置的对象。否则,这个getter方法将沿着树向下走,直到找到一个RenderObjectElement为止。 |
| size | Size | 返回的RenderBox的大小。这个getter方法只有在布局阶段完成后才会返回一个有效的结果。因此,从build中调用这个函数是无效的 |
| slot | dynamic | 由父级设置的信息,用于定义此子级在其父级的子级列表中的位置。仅具有一个子级的Element子类,则该子类的slot应该使用null。 |
| widget | Widget | 该Element的配置。 |
-
Element updateChild(Element child, Widget newWidget, dynamic newSlot)使用给定的新配置(newWidget)更新给定的子元素(child)
| newWidget == null | newWidget != null | |
|---|---|---|
| child == null | 返回null | 返回一个新的Element |
| child != null | 旧的子元素(child)被移除,返回null | 若可能,更新旧的子元素(child),返回child 或新的Element |
-
Element inflateWidget(Widget newWidget, dynamic newSlot)为给定的widget创建一个element,并将其作为该element的子元素添加到给定的Slot中。如果给定widget具有
global key,并且已经存在具有该全局键的widget的元素,则此函数将重用该元素(可能从树中的另一个位置移植它,或者从非活动元素列表中重新激活它),而不是创建一个element -
void mount(Element parent, dynamic newSlot)在给定的父元素的给定Slot位中添加这个元素。当新创建的元素第一次被添加到树上时,框架会调用这个函数,将Element从"initial"生命周期状态转换到 "active"生命周期状态
-
void update(covariant Widget newWidget)更改用于配置此Element的widget。当父元素希望使用不同的widget来配置这个元素时,框架会调用这个函数。新的widget保证与旧的widget具有相同的runtimeType。该函数仅在"active"生命周期状态下被调用。
updateChild 和 inflatWidget 主要作用是当Widget树发生变化时, 创建对应的Element。 不同的是updateChild做了一些优化,尽可能地复用之前的旧Element。 只有在无法复用的情况下,才调用inflatWidget去直接创建一个Element。 通常新创建一个Element时,会马上调用mount方法。 如果可以复用的场景,则调用update()方法
当新的Widget和旧Widget相同,或者新的Wiget和旧Widget类型相同,且key相同(如果key都为空也表示相同)时,可复用Element
Widget
是一个用于描述Element的配置。
Widget是Flutter框架中的核心类层次结构。是用户界面部分的不变描述。可以将Widget扩充为Element,以管理底层渲染树。
Widget本身没有可变的状态(它们的所有字段都必须为final)。如果希望将可更改的状态与Widget关联起来,请考虑使用StatefulWidget,每当它扩充成一个Element并被合并到树中时,它就会创建一个State对象(通过StatefulWidget.createState)。
RenderObject
渲染树中的对象。见RenderObject 文档
RenderObject具有一个parent属性,并具有一个名为parentData的插槽,父RenderObject可以在其中存储特定于孩子的数据,例如,孩子的位置。 RenderObject类还实现基本的布局和绘制协议。
然而,RenderObject类并没有定义子模型(例如,一个节点是否有零个、一个或多个子节点)。它也没有定义坐标系(例如子节点是以笛卡尔坐标定位,还是以极坐标定位等),也没有定义具体的布局协议。它的子类RenderBox引入了布局系统并使用笛卡尔坐标系。
通常,Flutter渲染对象树的根是RenderView。 该对象有一个child,必须是RenderBox。因此,如果你想在渲染树中拥有一个自定义的RenderObject子类,你有两个选择:要么你需要替换RenderView本身,要么你需要一个继承自RenderBox的自定义类。 (后者是更常见的情况。)
关于布局
布局协议从Constraints类派生。
performLayout`方法应该接受约束,并应用它们。布局算法的输出是在对象上设置的字段,这些字段描述了对象的几何形状,以便于父类的布局。例如,对于`RenderBox`,输出是`RenderBox.size`字段。仅当父级在子级上调用`layout`时并将`parentUsesSize`指定为true时,此输出才应由父级读取。只要渲染对象上发生任何会影响该对象布局的更改,则应调用`markNeedsLayout
Layer
参见 Layer 文档
图层类。在绘制过程中,渲染树生成一棵合成图层树,这些图层被传到到引擎中并由合成器显示。此类是所有图层的基类。大多数图层可以更改其属性,并且可以将图层移动到其他父对象
要合成树,需创建一个SceneBuilder对象,将其传递给根Layer对象的addToScene方法,然后调用 SceneBuilder.build获得一个Scene。然后可以使用Window.render来绘制一个Scene。
Scene对象代表合成场景的不透明对象。通过Window.render方法显示到屏幕上。实际上它是上层框架和底层引擎之间传递的数据结构,对应 Engine中的 scene.cc 结构。
BuildOwner
Widget框架的管理类。
该类跟踪哪些widget需要重建,并处理其他适用于widgets树的任务,如管理树的非活动元素列表,并在调试时的热重载期间在必要时触发 "reassemble "命令。
主构建所有者通常由WidgetsBinding拥有,并与构建/布局/绘制管道的其余部分一起由操作系统驱动。
可以构建其他构建所有者来管理屏幕外的小组件树。
要为树分配构建所有者,请在小组件树的根元素上使用 RootRenderObjectElement.assignOwner 方法。
PipelineOwner
用于管理渲染管道。
管道所有者提供了一个驱动渲染管道的接口,并存储了关于在管道的每个阶段中哪些渲染对象请求被访问的状态。