Flutter渲染原理

widget介绍

flutter开发最常用到的对象就是widget,它不仅包含了各种UI组件,还囊括了手势操作组件(GestureDetector)和动画组件(AnimatedWidget)等各种功能的widget。因此官方说“everything is widget”,widget是配置信息,我们用它来方便快捷的实现各种UI效果,例如,我们做一个最简单的页面。

void main() {
  runApp(Container(
    color: Colors.white,
    child: Center(
      child: Text("hello Flutter",
      textDirection: TextDirection.ltr,
      textAlign: TextAlign.center,
      style: TextStyle(color: Colors.red,fontSize: 22),),
    ),
  ));
}

这里我们只声明了一个container的根widget和一个text的字widget,页面就显示出来了,看起来非常简单,那么它是怎么绘制到屏幕上的呢?

三棵树

实际上,flutter的UI绘制包含了三个元素,widget,element和renderObject。这三个元素分别组成了三棵树:Widget 树,Element 树和 RenderObject 树,如下图:


三棵树.png

系统启动时,runApp方法会被调用,flutter会从最外层的widget去遍历创建一颗widget树;每一个widget创建后会调用createElement()创建相应的element,形成一颗element树;element创建后会通过createRenderObject()创建相应的renderObject树,如此就形成了三棵树。

三棵树的作用

那么,这三棵树都是用来干嘛的呢?

  • widget:配置信息,用来描述UI特征,比如尺寸多大,颜色是什么,位置在哪里
  • element:element是widget的实际实例,它同时持有了widget和renderObject的引用,用来决定是否进行UI更新。
  • renderObject:UI更新的执行者,保存了元素的大小,布局等信息。它才是真正调用渲染引擎去进行更新的对象
    那么,flutter为什么要设计成这样呢?为什么要弄成复杂的三层结构?
    答案是性能优化。如果每一点细微的操作就去完全重绘一遍UI,将带来极大的性能开销。flutter的三棵树型模式设计可以有效地带来性能提升。widget的重建开销非常小,所以可以随意的重建,因为它不一会导致页面重绘,并且它也不一定会常常变化。而renderObject如果频繁创建和销毁成本就很高了,对性能的影响比较大,因此它会缓存所有页面元素,只是当这些元素有变化时才去重绘页面。而判断页面有无变化就依靠element了,每次widget变化时element会比较前后两个widget,只有当某一个位置的Widget和新Widget不一致,才会重新创建Element和widget;其他时候则只会修改renderObject的配置而不会进行耗费性能的RenderObject的实例化工作了。
    Element的updateChild方法:
    assert(() {
      final int oldElementClass = Element._debugConcreteSubtype(child);
      final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
      hasSameSuperclass = oldElementClass == newWidgetClass;
      return true;
    }());
    if (hasSameSuperclass && child.widget == newWidget) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      newChild = child;
    } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      assert(child.widget == newWidget);
      assert(() {
        child.owner!._debugElementWasRebuilt(child);
        return true;
      }());
      newChild = child;
    } else {
      deactivateChild(child);
      assert(child._parent == null);
      newChild = inflateWidget(newWidget, newSlot);
    }
  } else {
    newChild = inflateWidget(newWidget, newSlot);
  }

  assert(() {
    if (child != null)
      _debugRemoveGlobalKeyReservation(child);
    final Key? key = newWidget.key;
    if (key is GlobalKey) {
      assert(owner != null);
      owner!._debugReserveGlobalKeyFor(this, newChild, key);
    }
    return true;
  }());

  return newChild;
}

可以看出,当widget指针相同或者类型相同时,只会进行微更新,即只更新内容,renderObject并不会重建。而当widget类型不同或key不同时,element才会将旧的widget从widget树中移除并attach上新的widget。
element的更新策略:

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

接下来看两个例子:

当widget不变时

class ChangeWidget extends StatefulWidget{
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    throw UnimplementedError();
  }

  @override
  State<StatefulWidget> createState() {
    return ChangeWidgetState();
  }
}

class ChangeWidgetState extends State{
  bool isChangeText =false;

  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      appBar: AppBar(
        title: Text("计数器"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            isChangeText?buildText1():buildText2(),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(onPressed: () {
        increaseCount();
      },tooltip: "切换textWidget",
        child: Icon(Icons.add),
      ),
    );
  }

  void increaseCount(){
    setState(() {
      isChangeText = ! isChangeText;
    });
  }

  Widget buildText1(){
    return Text("flutter1",style: Theme.of(context).textTheme.headline4);
  }

  Widget buildText2(){
    return Text("flutter2",style: Theme.of(context).textTheme.headline3);
  }

}

如上代码,一个statefulWidget,有两个方法分别返回了不同文字的TextWidget,由一个按钮控制来显示哪一个。运行后切换按钮会发现文字会来回变动。然而打开Dart DevTools观察,会发现Text下面的RichText的RenderObject的ID一直是#63684,没有变化,不管怎么切换这个UI都不会发生变化。


image.png

说明虽然这两个text的文字变化了,但是widget类型并没有变化,所以并不会重建renderObject。只会将变化了的元素在页面上进行了渲染。

当widget改变时

而如果我们将其中的一个组件更改成不同类型的,renderObject的类型还会发生变化吗?
让buildText2返回一个不同的widget试试。

  Widget buildText2() {
    // return Text("flutter2", style: Theme.of(context).textTheme.headline3);
    return SizedBox(height: 30,width: 30);
  }
image.png

刷新一下发现这两个widget的renderObject的ID是不同的。由此我们可以验证当widget类型不同时,element和renderObject都会发生变化,旧的销毁,新的重建。
这就是flutter高性能的秘诀之一。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容