前面咱们提到过Flutter其实就是个Dart编写的UI库,附带了本身的渲染引擎。咱们经过Widget
来描述
咱们的view,而后Flutter会用它的渲染引擎根据咱们的Widget
树来绘制咱们的界面。注意,是根据Widget
树来绘制界面,而不是直接绘制Widget
树,这是一个很重要的概念,我们接下来慢慢来探讨。android
咱们来看一张Flutter的架构图:git
Flutter在咱们跟渲染引擎之间提供了好几层抽象,咱们平常开发主要接触到的就是那些个Widget
库了,Rendering
作了一些渲染相关的抽象,而dart:ui
则是用Dart编写的最后一层代码,它实现了一些与底层的引擎交互的胶水代码,咱们使用到的canvas API也是在这里定义的。github
当咱们组合好咱们Widget
树后,Flutter会从根节点向叶节点传递他们的约束或者说叫配置,约束限制了minHeight
,minWidth
,maxHeight
,maxWidth
等等。 好比Center
就向它的子Widget
传递居中的约束,当访问到叶节点的时候,这时候Widget
树上全部的Widget
都知道了它们的约束,这时候他们就能够根据已有的约束本身肯定它们实际要占有的大小跟位置,再一层层往上传递,只须要线性的时间复杂度,整个界面的上的元素绘制在哪一个像素上就都肯定下来了。canvas
这是我从谷歌找到的一张图: markdown
那屏幕上绘制的既然不是咱们代码里写的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同样是框架提供给咱们的编写界面的抽象。app
RenderObject
干了什么?本质上,RenderObject
是没有任何状态的,它也不包含任何业务逻辑,它们只知道一点点关于它们父RenderObject
的信息,同时还有访问它们子RenderObject
的能力。在整个app的层面上它们不会互相协做,也不能帮别人作决定,只会按照顺序在屏幕上绘制。框架
widget
在他们的build
方法里面会返回其它Widget
,致使Widget
树愈来愈庞大。在树的最下端最底下会遇到一个或多个RenderObjectWidget
,就是这个类帮整个Widget
树建立了RenderObject
。less
咱们前面提到过Widget
拿到本身的约束后会决定本身的大小,其实这些约束拿到了以后是给了本身对应的RenderObject
,它们会根据约束决定Widget
在屏幕上的真实的物理大小。不一样的RenderObject
决定大小的方式也不一样,主要就三大类:dom
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带给个人便利的同时也复用了那些个实际在屏幕上作绘制的对象。
以前咱们说过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渲染过程。今天的信息量确实很大,好在咱们平常开发不用直接跟它们打交道。你们也不用强迫本身一会儿明白,尤为是刚入门的朋友们,不要急,虽然懂得原理会帮助咱们处理一些问题,目前知道有这么个东西有个印象就好,时间长了天然就懂啦。
代码地址:counter