原来你是这样的Flutter
前面我们提到过Flutter其实就是个Dart编写的UI库,附带了自己的渲染引擎。我们通过Widget
来描述
我们的view,然后Flutter会用它的渲染引擎根据我们的Widget
树来绘制我们的界面。注意,是根据Widget
树来绘制界面,而不是直接绘制Widget
树,这是一个很重要的概念,咱们接下来慢慢来探讨。
绘制的到底是什么?
我们来看一张Flutter的架构图:
Flutter在我们跟渲染引擎之间提供了好几层抽象,我们日常开发主要接触到的就是那些个Widget
库了,Rendering
做了一些渲染相关的抽象,而dart:ui
则是用Dart编写的最后一层代码,它实现了一些与底层的引擎交互的胶水代码,我们使用到的canvas API也是在这里定义的。
当我们组合好我们Widget
树后,Flutter会从根节点向叶节点传递他们的约束或者说叫配置,约束限制了minHeight
,minWidth
,maxHeight
,maxWidth
等等。比如Center
就向它的子Widget
传递居中的约束,当访问到叶节点的时候,这时候Widget
树上所有的Widget
都知道了它们的约束,这时候他们就可以根据已有的约束自己确定它们实际要占有的大小跟位置,再一层层往上传递,只需要线性的时间复杂度,整个界面的上的元素绘制在哪个像素上就都确定下来了。
这是我从谷歌找到的一张图:
那屏幕上绘制的既然不是我们代码里写的Widget
树,那到底是什么呢?我之前也说过了Flutter里面其实不只有Widget
,还有其他的对象类型,只不过我们作为开发者日常开发任务中关心的只有Widget
而已,所以Everything is Widget
这句话也不能算错。我们这里要提到的其他对象类型就是RenderObject
,这个类虽然也暴露给我们了,但是基本上只在Flutter框架内部使用,我们平常开发大多数不会碰到的。从名字可以猜到它们跟渲染相关,确实,RenderObject
在Flutter里面负责实际在屏幕上的绘制,并且每一个Widget
都有一个对应的RenderObject
,也就是说,除了Widget
树,我们还会有一个RenderObject
树。
我们平时编写的Dart代码,组合的那些Widget
,其实就是给RenderObject
提供了草图,提供了对UI的描述信息,然后RenderObject
根据这些信息去绘制我们的界面。RenderObject
有一些方法诸如performLayout
,paint
,这些方法负责在屏幕上绘制,我们使用的Widget
的概念为我们在RenderObject
上提供了很好的抽象,我们只需要声明我们想要什么东西就好了。那有些同学可能会想,其实我们也可以抛开Widget
去直接绘制的呀?大部分人应该都不愿意直接跟底层绘制打交道,那样就要自己计算每个像素应该绘制的位置,工作量会大大增加,就像我们之前开发android app不会所有的界面都用OpenGL去绘制一样,而是使用各种View、ViewGroup,Widget
跟View一样是框架提供给我们的编写界面的抽象。
那RenderObject
干了什么?
本质上,RenderObject
是没有任何状态的,它也不包含任何业务逻辑,它们只知道一点点关于它们父RenderObject
的信息,同时还有访问它们子RenderObject
的能力。在整个app的层面上它们不会互相协作,也不能帮别人做决定,只会按照顺序在屏幕上绘制。
widget
在他们的build
方法里面会返回其它Widget
,导致Widget
树越来越庞大。在树的最下端最底下会遇到一个或多个RenderObjectWidget
,就是这个类帮整个Widget
树创建了RenderObject
。
我们前面提到过Widget
拿到自己的约束后会决定自己的大小,其实这些约束拿到了之后是给了自己对应的RenderObject
,它们会根据约束决定Widget
在屏幕上的真实的物理大小。不同的RenderObject
决定大小的方式也不同,主要就三大类:
•尽可能地占满空间,比如Center
对应的RenderObject
•跟子Widget
保持一样大,比如Opacity
对应的RenderObject
•特定大小,比如Image
对应的RenderObject
关于Flutter自带的RenderObject
就这三点比较重要,一般我们也不会去自定义RenderObject
。
我还有个兄弟:Element
再来看看我们开头那张Flutter架构图。我们Widget
层抽象出了一个Widget
树,我们dart:ui
负责实际绘制,抽象出了一个RenderObject
树,中间的一层Rendering
干了啥?它其实也抽象出来了一个树:Element
树。
当一个Widget
build方法被调用时,Flutter会调用Widget.createElement(this)
创建一个Element
,这个Widget
就是此Element
一开始的配置,这个Element
会持有它的引用。值得一提的是我们的StatefulWidget
关联的State
对象其实也是由Element
管理的,因为State
一般都存活的比较长,widget
却可能频繁build
。对应的,Element
跟Widget
就有一个显著的不同,它会更新,当build
方法再被调用时,它会更新它的引用指向新的Widget
。我们之前说过了在屏幕绘制的不是Widget
树,现在可以说绘制的到底是什么东西了,是Element
树。Element
树代表着app的实际结构,是app的骨架,是实际绘制在屏幕上的东西。Element
会通过引用查询Widget
携带的信息,在一系列的判断后交给RenderObject
去绘制。(主要判断有木有修改,要不要重绘)
现在就很明朗了:
Element
持有Widget
跟RenderObject
的引用,RederObject
负责把上层描述转换成可以跟底层渲染引擎通信的东西,而Element
则是Widget
跟RenderObject
之间的胶水代码。
为什么有三兄弟?
那到底为什么要设计出这三层呢,直接绘制不好吗?为什么要增加这样的复杂度呢?我们知道Flutter是一个响应式的框架,所有的Widget
也都是immutable的,任何修改都会导致重新build
,也就是会重新构建它的Widget
树,一个app每天build
界面几百万次不过分吧?而RenderObject
是开销比较大的对象,因为负责底层的绘制,比较expensive,这样它也频繁地销毁重建的话肯定会影响性能,大多数时候界面上仅有一小部分被修改,比如在一个动画中,一帧可能就改变一点点,可能只改个某部分的颜色,其它的都不变,那么随便我们的Widget
树怎么变,我们的app骨架也就是我们的Element
树结构完全不需要重新构建,只需要把改变的那部分重新绘制就好了。Widget
只是配置文件,比较轻量,想怎么变你就怎么变,我们实际绘制在屏幕上的是Element
,只要想办法判断它指向的Widget
有没有改变就好了,变了就重新绘制,没变就不管,这样虽然我们可能频繁地通过setState
之类的手段去频繁通知重绘,Widget
树也频繁地重新build
,Flutter的性能并不会受到影响。我们在享受了immutable带给我的便利的同时也复用了那些个实际在屏幕上做绘制的对象。
Flutter的复用机制
之前我们说过build
方法被调用后Element
会更新引用,然后判断要不要重绘。具体的判断标准就是运行时类型有木有改变,或者说如果一个Widget
有key的话,key有木有变等等。这么说听起来也有点抽象,我们就来实际写一点代码来感受一下Flutter的这个机制。
还是用昨天的那个app为例,这次我们希望我们点击重置那个FAB的时候,可以交换加减两个按钮的位置。可能大家没看我之前的文章,有的人还不熟悉Flutter开发,我这里先带大家定义一个按钮叫做FancyButton
,看完大家就知道Flutter代码怎么写了:
class FancyButton extends StatefulWidget {
final Widget child;
final VoidCallback callback;
const FancyButton({Key key, this.child, this.callback}) : super(key: key);
@override
FancyButtonState createState() {
return FancyButtonState();
}
}
因为它是一个StatefulWidget
,它的核心逻辑都在它对应的State
里面,StatelessWidget
更简单,它包含了一个类似的build
方法,这里就不带大家写了,后面直接看源代码就好了:
class FancyButtonState extends State<FancyButton> {
@override
Widget build(BuildContext context) {
return Container(
child: RaisedButton(
color: _getColors(),
child: widget.child,
onPressed: widget.callback,
),
);
}
Color _getColors() {
return _buttonColors.putIfAbsent(this, () => colors[next(0, 5)]);
}
}
Map<FancyButtonState, Color> _buttonColors = {};
final _random = Random();
int next(int min, int max) => min + _random.nextInt(max - min);
List<Color> colors = [
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.amber,
Colors.lightBlue,
];
其实我们也只是包装了RaisedButton
并提供了颜色而已,其它的还是要上游去配置的。
接下来,我们就可以把这按钮添加到主页面去了:
@override Widget build(BuildContext context) {
final incrementButton =
FancyButton(child: Text("增加"), callback: _incrementCounter);
final decrementButton =
FancyButton(child: Text("减少"), callback: _decrementCounter);
List<Widget> _buttons = [incrementButton, decrementButton];
if (_reversed) {
_buttons = _buttons.reversed.toList();
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
color: Colors.green.withOpacity(0.3)),
child: Image.asset("qrcode.jpg"),
margin: EdgeInsets.all(4.0),
padding: EdgeInsets.only(bottom: 4.0),
),
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _buttons),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _resetCounter,
tooltip: 'Increment',
child: Icon(Icons.refresh),
),
);
}
其中交换按钮位置的逻辑就很简单了:
void _swap() {
setState(() {
_reversed = !_reversed;
});
}
好,可以运行代码了。
一切都如我们期望的那样,按钮交换过来了并且点击事件也都正常...等等!怎么按钮的颜色没动!
这就是我们前面提到的判断逻辑,复用机制了!原来,当重新build
的时候,Element
还是指向它原来位置对应的Widget
,我们的Widget
并没有key,那它只根据运行时类型来判断是否有改变,我们这儿俩个类型都是一样的,都是FancyButton
,我们本来期望Flutter能发现两个按钮的颜色不一样从而去重新绘制。但是颜色是在State
里面定义的,State
并没有被销毁,因此只根据运行时类型Element
最终会认为没有修改,所以我们看到颜色没有更新,那为什么文字跟点击事件变了呢,那是因为这俩是从外部传递过来的,外部重新创建了呀。解决这个问题也很简单,我们只要根据规则给这两个按钮加上key就好了,这样Flutter根据key就知道我们的Widget
不一样了:
List<UniqueKey> _buttonKeys = [UniqueKey(), UniqueKey()];
...
final incrementButton = FancyButton(
key: _buttonKeys.first, child: Text("增加"), callback: _incrementCounter);
final decrementButton = FancyButton(
key: _buttonKeys.last, child: Text("减少"), callback: _decrementCounter);
Key
的类型有好几种,不过不是今天的重点我们暂且不讨论。这下Flutter再也不会认为没有改变啦,再次运行项目,这下按钮切换的同时背景色也会跟着改变了。
好啦,到了这儿,Flutter的基本工作流程我们算是搞明白了,怪不得它频繁build却不卡顿!想深入了解的朋友们也可以看看Flutter团队的这个视频:Flutter渲染过程[1]。今天的信息量确实很大,好在我们日常开发不用直接跟它们打交道。大家也不用强迫自己一下子明白,尤其是刚入门的朋友们,不要急,虽然懂得原理会帮助我们处理一些问题,目前知道有这么个东西有个印象就好,时间长了自然就懂啦。
代码地址:counter[2]
References
[1]
Flutter渲染过程: https://www.youtube.com/watch?v=UUfXWzp0-DU
[2]
counter: https://github.com/zongyunfeng/counter
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 聊聊“异步”
- springboot解决前后端数据跨域问题
- 单细胞数据中到底应该如何处理线粒体基因
- Seurat小提琴图为什么有的只有点儿?
- Layui解决table日期的格式化问题
- Telegraf+Influxdb+Grafana 轻量级监控系统部署
- 国产开源文档管理系统——Wizard
- 力扣 1519——子树中标签相同的节点数
- PythonforResearch | 1_文件操作
- 你应该知道关于Python的这几个技巧!
- Pytest之fixture
- JAVA|Java的Scanner类初级使用
- systemd设置nginx开机自启动
- C盘爆满,如何移除软件~
- Microsoft PowerToys