Flutter是Google推出并开源的跨平台开发框架,主打跨平台、高保真、高性能。开发者可以通过Dart语言开发Flutter应用,一套代码同时运行在ios和Android平台。不仅如此,flutter还支持web、桌面、嵌入应用的开发。flutter提供了丰富的组件、接口,开发者可以很快地为flutter添加native扩展。同时flutter还使用skia引擎(其实Android原生中页面绘制也是由skia引擎来渲染的)渲染视图,这无疑为用户提供良好地体验。
简单来讲,Flutter 从上到下可以分为三层:框架层、引擎层和嵌入层。
Flutter Framework,即框架层。这是一个纯 Dart实现的 SDK,它实现了一套基础库,自底向上,我们来简单介绍一下:
Engine,即引擎层。毫无疑问是 Flutter 的核心, 该层主要是 C++ 实现,其中包括了 Skia 引擎、Dart 运行时(Dart runtime)、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到引擎层,然后实现真正的绘制和显示。
Embedder,即嵌入层。Flutter 最终渲染、交互是要依赖其所在平台的操作系统 API,嵌入层主要是将 Flutter 引擎 ”安装“ 到特定平台上。嵌入层采用了当前平台的语言编写,例如 Android 使用的是 Java 和 C++, iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。 Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。Flutter 本身包含了各个常见平台的嵌入层,假如以后 Flutter 要支持新的平台,则需要针对该新的平台编写一个嵌入层。
为了熟悉flutter地绘制原理,我们先从屏幕显示图像地基本原理说起:我们在买显示器时,都会关注显示器地刷新频率;那么对于手机屏幕也是一样,通常手机屏幕地刷新频率是60Hz,当然现在也有不少高刷新频率地手机也在推出,如:90Hz,120Hz。
一般来说,计算机系统中,CPU、GPU和显示器以一种特定地方式协作:CPU将计算好地显示内容提交给GPU,GPU渲染后放入帧缓冲区,然后视频控制器按照VSync信号从帧缓冲区取帧数据传递给显示器显示。当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号(VSync),所以60Hz的屏幕就会一秒内发出60次这样的信号。
上面是CPU、GPU和显示器协作方式,对于Flutter也不例外,Flutter也遵循了这种模式:
GPU的VSync信号同步给到UI线程,UI线程使用Dart来构建抽象的视图结构(这一步是在Framework层中实现的),绘制好的抽象视图结构会在GPU线程中进行图像的合成(这一步在引擎层中完成),然后提供给skia渲染成GPU所需要的数据,最终提供给GPU进行渲染。由此可知,flutter高性能的核心其实就在于根据vsync信号进行图像的快速构建,也就是上图中的绿色部分。
在Flutter框架中存在着一个渲染流程。这个渲染流水线是由垂直同步信号驱动的,而Vsync信号由系统提供的,如果你的Flutter app是运行在Android上,那Vsync信号就是我们熟悉的Android的那个Vsync信号。
当Vsync信号到来了以后,Flutter框架会按照图里的顺序执行一系列动作:
最终生成一个场景之后送往底层,由GPU绘制到屏幕上。
在 Flutter 中, widget 的功能是“描述一个UI元素的配置信息”,它就是说, Widget 其实并不是表示最终绘制在设备屏幕上的显示元素,所谓的配置信息就是 Widget 接收的参数,比如对于 Text 来讲,文本的内容、对齐方式、文本样式都是它的配置信息。
既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?Flutter 框架的处理流程是这样的:
真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 RenderObject 的粘合剂,可以理解为一个中间代理。
从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。
接下来,我们重点看下Element这个东西,Element的生命周期如下:
现在,读者应该能够知道为什么笔者说:“三棵树是为了性能而生的”。因为,视图中的控件都是Render树中的对象,我们知道new出一个对象都是需要消耗操作系统很多资源的,通过三棵树就可以做到对已有render树中的对象进行复用,而不是每更改一个widget就new出一个新的render对象。
这里额外多提一嘴:我们在日常开发的过程中经常会接触到BuildContext这个东西。那这个东西究竟是什么呢?笔者就直接说答案了,有兴趣的同学可以看下源码。BuildContext就是widget对应的Element,所以我们可以通过context在StatelessWidget和StatefulWidget的build方法中直接访问Element对象。我们获取主题数据的代码Theme.of(context)内部正是调用了Element的dependOnInheritedWidgetOfExactType()方法。
我们可以看到Element是Flutter UI框架内部连接widget和RenderObject的纽带,大多数时候开发者只需要关注widget层即可,但是widget层有时候并不能完全屏蔽Element细节,所以Framework在StatelessWidget和StatefulWidget中通过build方法参数又将Element对象也传递给了开发者,这样一来,开发者便可以在需要时直接操作Element对象。
那么,现在笔者有个问题:如果没有widget层,单靠Element层是否可以搭建起一个可用的UI框架?如果可以应该是什么样子?
class HomeView extends ComponentElement{
HomeView(Widget widget) : super(widget);
String text = "123456789";
Widget build() {
Color primary=Theme.of(this).primaryColor; //1
return GestureDetector(
child: Center(
child: TextButton(
child: Text(text, style: TextStyle(color: primary),),
onPressed: () {
var t = text.split("")..shuffle();
text = t.join();
markNeedsBuild(); //点击后将该Element标记为dirty,Element将会rebuild
},
),
),
);
}
}
上面build方法不接收参数,这一点和在StatelessWidget和StatefulWidget中build(BuildContext)方法不同。代码中需要用到BuildContext的地方直接用this代替即可,如代码注释1处Theme.of(this)参数直接传this即可,因为当前对象本身就是Element实例。
当text发生改变时,我们调用markNeedsBuild()方法将当前Element标记为dirty即可,标记为dirty的Element会在下一帧中重建。实际上,State.setState()在内部也是调用的markNeedsBuild()方法。
上面代码中build方法返回的仍然是一个widget,这是由于Flutter框架中已经有了widget这一层,并且组件库都已经是以widget的形式提供了,如果在Flutter框架中所有组件都像示例的HomeView一样以Element形式提供,那么就可以用纯Element来构建UI了HomeView的build方法返回值类型就可以是Element了。
如果我们需要将上面代码在现有Flutter框架中跑起来,那么还是得提供一个“适配器”widget将HomeView结合到现有框架中,下面CustomHome就相当于“适配器”:
class CustomHome extends Widget {
Element createElement() {
return HomeView(this);
}
}
给大家推荐一本flutter相关的电子书籍:《Flutter实战·第二版》