浅谈Flutter

1,880 阅读7分钟

本次主要分享Flutter的一些基本技术内容,主要涵盖有:

  1. Flutter的优势
  2. Flutter的两大利器
  3. Flutter的渲染

Flutter的优势

目前,业界上流行的跨平台技术有H5、React Native、以及后来的Flutter,判断一项跨平台的技术是否有落地价值,在于有可接受的页面性能和支持高效开发。

页面性能

在页面性能上,H5页面的性能一般,依赖浏览器的性能,对于复杂的动画和交互有点力不从心。

React Native的性能会优于H5,因为是通过客户端直接进行页面渲染,速度上比WebView上快不少,虽然对产品来说性能上达到可接受的范围内,但是也存在性能短板,JS和Native的通信、页面的事件监听、复杂动画的渲染都有一定的性能挑战。

为了解决H5和RN存在的性能问题,以Flutter为代表的自绘引擎技术方案应运而生,Flutter通过Skia自己绘制图形界面,避免和原生频繁通信,保证了性能和双端统一。

高效开发

H5和RN都是使用JavaScript开发,对前端的童鞋非常友好,而且JavaScripe社区资源丰富,学习成本和开发成本较低,同时可以做到动态化、热更新。

Flutter则是采用Dart作为开发语言,存在一定的学习成本,同时技术栈上没有JavaScript丰富。而Dart在开发环境下处于JIT模式,避免每次改动都需要重新编译,在生产环境处于AOT模式,生成的是高效的ARM代码,这是JavaScript不具备的能力,同时也兼顾了高效开发。

热更新

当APP出现Bug时,传统的客户端只能让用户更新APP,进行Bug修复。正确的使用热更新技术,可以在线修复Bug。RN得益于JavaScript可以实现热更新,而Dart是一门静态语言,本身无法做到热更新,但也可以通过一些特殊手段实现热更新。

Flutter的两大利器

Dart是Flutter唯一的开发语言,Google选择Dart作为Flutter开发语言是有一定的道理的,而Flutter的高性能离不开其优秀的架构设计。

Dart

Flutter使用Dart语言开发,Dart在JIT模式下,速度与JavaScript基本持平,但Dart支持AOT模式,这是JavaScript所没有的,我们先了解下什么是JIT和AOT。

目前程序编译有两种运行方式:静态编译和动态解释。静态编译时在运行前全部被翻译成机器码,这种类型成为AOT,即提前编译;而解释执行则是一句一句边翻译边运行,称为JIT,即即时编译。

Dart具备JIT和AOT,在开发阶段,采用了JIT模式可以快速改动,节省开发时间,在发布阶段使用AOT生成ARM保证了性能;在内存分配上,Dart的开发团队成员来自Chrome团队,该团队对JavaScript引擎的内存分配出了很多优化成果,相信Dart基本上也能满足日常需求;Dart同时也是一门类型安全的语言,支持静态类型检测,在开发阶段可以发现一些类型错误,并排除潜在问题。

Flutter架构

Flutter Framework

Flutter Framework是一个纯Dart实现的SDK,实现了一套基础库,我们从下往上看:

  • 底下两层(Foundation和Animation、Painting、Gestures)在Flutter中的dart:ui包里,是Flutter的底层UI库
    • Foundation在最底层,主要定义底层工具类和方法,以提供给其他层使用
    • Animation:动画相关类,有补间动画等
    • Painting:封装了Flutter Engine提供的绘制接口
    • Gesutre:提供手势事件传递和事件响应
  • Rendering:是一个抽象的布局层,依赖dart UI层,Rendering层会构建一个UI树,当UI树发生变化,通过diff算法计算更改部分,重新更新UI树,并绘制到屏幕上
  • Wigets层是Flutter提供的一套基础组件库,有Meterial和Cupertino两种风格

Engine

Engine是一个纯C++实现的SDK,其中包括了Skia引擎、Dart运行时、文本排版引擎等,真正实现绘制逻辑的地方

Embedder

Embedder叫操作系统适配层,主要是移动和桌面操作系统执行Flutter应用程序的代码,包含Surface设置、线程管理、平台插件等相关特性适配,Flutter平台相关特性并不多,从而对于跨段一致性的成本维护也比较低。

渲染

Flutter是一个专注跨平台UI的组件,所以布局和绘制是其核心的职责。

当用户看到一张图片的时候,首先需要经过CPU负责布局计算、位图解码等,将数据打包提交到GPU,GPU负责进行顶点计算、片元计算、光栅化后渲染到帧缓存上,显示器将切换帧缓存,将渲染的数据呈现给用户。操作系统控制着一个垂直同步信号,对于60帧/1秒的屏幕,一般是1/60ms发出一个信号,在信号内完成CPU和GPU的工作,用户可以看到流畅的页面,相反,用户看到的页面不流畅甚至卡顿。

Flutter在渲染主要分为三个步骤:

  • 布局:计算页面元素的位置和大小
  • 绘制:将页面元素进行渲染
  • 合成:将页面元组按规则组合

布局

Flutter布局的时候使用的是深度优先遍历渲染UI树,我们可以以Stack为例,Stack是依赖RenderStack的perfromLayout方法进行实际的布局:

// 伪代码
void performLayout() {
    .....
    while (child != null) {
        ...
        //对child以及child的左右节点进行布局计算
        layoutPositionedChild(child, ...)
        ...
        // 使用child的兄弟节点替换
        child = childParentData.nextSibling;
    }
}

数据流的传递方式是从上往下传递约束,通过后序遍历根据约束计算自己的大小,依次返回给父节点。整个过程中,位置信息由父节点来控制,子节点不关心自己的位置,只需要计算自己的大小即可,如下图所示:

Flutter在布局过程中,为了防止子节点变化而导致整个UI树需要重新布局,加入了Relayout Boundary,通过Relayout Boundary标记需要重绘的部分,避免其他节点被污染触发重建:

Flutter为了提升布局性能,加入了缓存,在Flutter中,Element都具有一个key,key值是位移的,当子树重建,只会刷新key不同的部分。

在确认了Widget的位置和大小之后,就进入了绘制环节。

绘制

Flutter使用的是不依赖平台的自绘引擎Skia进行绘制的,Chrome和Android均使用Skia作为绘图引擎,而iOS上不是,所以在打包iOS需要将Skia打包进ipa包内,体积相对比Android的大。

Skia的存在,对Flutter收益有:

  • Flutter底层的渲染能力得到了统一,不再需要做双端适配了
  • 通过OpenGL,高效渲染UI

在布局结束之后,UI树中的每一个节点都有明确的位置和大小,Flutter会把所有Element绘制到不同的图层上,绘制的过程也是深度优先遍历,先绘制父节点,然后绘制子节点。同时绘制也有一个重绘边界的东西——Repaint Boundary,可以避免全局绘制,做到局部绘制,提升绘制性能。

总结

对于Flutter的内容,需要学习的东西还有很多,想要描述的东西也有很多,但由于篇幅和时间问题,本次就先暂时分享到这,文中有什么描述不妥的内容,欢迎指正。