原来你是这样的Flutter

前面咱们提到过Flutter其实就是个Dart编写的UI库,附带了本身的渲染引擎。咱们经过Widget描述咱们的view,而后Flutter会用它的渲染引擎根据咱们的Widget树来绘制咱们的界面。注意,是根据Widget树来绘制界面,而不是直接绘制Widget树,这是一个很重要的概念,我们接下来慢慢来探讨。android

绘制的究竟是什么?

咱们来看一张Flutter的架构图:git

Flutter架构图

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有一些方法诸如performLayoutpaint,这些方法负责在屏幕上绘制,咱们使用的Widget的概念为咱们在RenderObject上提供了很好的抽象,咱们只须要声明咱们想要什么东西就行了。那有些同窗可能会想,其实咱们也能够抛开Widget去直接绘制的呀?大部分人应该都不肯意直接跟底层绘制打交道,那样就要本身计算每一个像素应该绘制的位置,工做量会大大增长,就像咱们以前开发android app不会全部的界面都用OpenGL去绘制同样,而是使用各类View、ViewGroup,Widget跟View同样是框架提供给咱们的编写界面的抽象。app

RenderObject干了什么?

本质上,RenderObject是没有任何状态的,它也不包含任何业务逻辑,它们只知道一点点关于它们父RenderObject的信息,同时还有访问它们子RenderObject的能力。在整个app的层面上它们不会互相协做,也不能帮别人作决定,只会按照顺序在屏幕上绘制。框架

widget在他们的build方法里面会返回其它Widget,致使Widget树愈来愈庞大。在树的最下端最底下会遇到一个或多个RenderObjectWidget,就是这个类帮整个Widget树建立了RenderObjectless

咱们前面提到过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。对应的,ElementWidget就有一个显著的不一样,它会更新,当build方法再被调用时,它会更新它的引用指向新的Widget。咱们以前说过了在屏幕绘制的不是Widget树,如今能够说绘制的究竟是什么东西了,是Element树。Element树表明着app的实际结构,是app的骨架,是实际绘制在屏幕上的东西。Element会经过引用查询Widget携带的信息,在一系列的判断后交给RenderObject去绘制。(主要判断有木有修改,要不要重绘)

如今就很明朗了:

最终关系图

Element持有WidgetRenderObject的引用,RederObject负责把上层描述转换成能够跟底层渲染引擎通讯的东西,而Element则是WidgetRenderObject之间的胶水代码。

为何有三兄弟?

那到底为何要设计出这三层呢,直接绘制很差吗?为何要增长这样的复杂度呢?咱们知道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渲染过程。今天的信息量确实很大,好在咱们平常开发不用直接跟它们打交道。你们也不用强迫本身一会儿明白,尤为是刚入门的朋友们,不要急,虽然懂得原理会帮助咱们处理一些问题,目前知道有这么个东西有个印象就好,时间长了天然就懂啦。

代码地址:counter

关注我,一块儿学习Flutter吧!
相关文章
相关标签/搜索