高效开发与高性能并存的UI框架——携程Flutter实践
Flutter已经开源了三年,但是最近两年才开始在开源社区活跃起来,尤其是最近还发布了Preview 1版本。作为可以实现一套代码同时在iOS、Android平台上运行的又一个新的UI框架,Flutter提供给开发者的不仅仅是高速实现,还有高质量、流畅的UI。免费开源的协议对于开发者来说也很友好。
本文将从Flutter架构理念与UI渲染逻辑,来解释为什么Flutter的渲染效率非常高,以及从Flutter开发实践的角度,介绍框架的特性及Flutter开发中所遇到的问题,希望给对Flutter感兴趣的小伙伴在选型时一些启发和思考,避免重复踩坑。
一、Flutter Layers
Flutter的主要设计人之一Ian Hickson,之前是HTML规范编写者,因此Flutter的设计理念也与HTML的实现方法有很多相似之处。
Flutter最初的理念是实现跨平台的Material Design的跨平台框架。平台框架大致可以分为四层:
-
dart:ui : 最底层的是UI层,由Flutter引擎所暴露的库,可以理解为一个布局层。
-
Rendering : 这一层是抽象的布局层,它依赖于UI层,可以构建一个UI树,通过更新UI树来更新UI。
-
Material与Widgets : 最后就是Material层使用Widget层来构建UI。
起初Flutter是没有Rendering层的,直接通过坐标计算每个像素点需要显示什么,这让框架的代码变得特别复杂,每当UI更新的时候需要重新计算这些坐标是否需要改变。后来增加Randering层来抽象UI显示的位置,通过抽象位置来判断像素点是否需要更新。
在Flutter项目的初期,Dart-lang也不是特别成熟。Dart虚拟机在垃圾回收的频率与回收机制表现当时并不是特别好,比如当时Flutter如果运行一个时间很长的动画,动画结束之后所占用的内存对于Flutter框架就是一个很大的垃圾。后来Dart团队在垃圾回收上进行了很多优化,使Flutter在UI显示更流畅。
如今,国内最大的使用厂商应该就是阿里闲鱼了,在Flutter发布Preview 1版本的时候,闲鱼App也一起协同展示了他们用Flutter编写的商品详情页面。我也在使用Flutter仿小米计算器开发后,体验到release版的流畅度确实堪比原生:
(已上架Google,可以通过包名搜索下载体验:top.basking.calculator)
二、Flutter的UI渲染
Flutter渲染效率堪比原生,快于RN。Flutter更新UI的时候,并不是更新整个UI,而是更新所需要更新的部分。比如从网络异步下载一个图片,设置到“Image”(ImageView)中,如果这个Image Widget大小并没有改变,只需要将图片对象传入Widget中,接着直接重新绘制这一个Widget就可以了。为了达到这样的UI渲染理念,Flutter是如何设计的呢?
FlutterUI渲染过程
Flutter 的UI渲染过程简单可以分为3个分支,Widget树、Element树、Rendering树。
当Widget改变的时候,只有将它添加到Element树上时,才会改变Rendering树,展示到UI界面上。将它添加到Element树的方法就是setState()方法,它会自动寻找改变了的Widget,然后添加到Element树,等待后续的操作。
可以看到,矩形的子Widget并没有改变,所以在Element树上也没有改变,到了Rendering树也没有重新渲染,这种设计理念对于刷新UI操作可以大大提高效率。
FlutterUI渲染 —— onDraw与onLayout
与其他的UI框架渲染逻辑不同的是,Widget的Draw与Layout的顺序不一定相同。比如在Android端onDraw与onLayout的顺序是相同的。关于Flutter框架的渲染顺序大家可以看以下的例子:
在Row Widget中有三个子Widget,其中中间的是固定宽度的Widget,还有两个是根据剩下宽度比例占用位置的Widget,其中绿色Widget是橙色的宽度的两倍。而他们的layout order与rendering order如下:
这么做是因为Flutter为了保证对于每个Widget的访问是单一线性的。所以在layout order中Flutter框架就会先layout固定宽度的Widget,然后再layout比例宽度的Widget。接着到了Rendering树再会根据Element树的顺序逐个对每个Widget进行渲染。
三、Flutter框架UI特性
Dart语言
Flutter的开发语言是由ChromeV8引擎团队的领导者Lars Bak主持开发的Dart。Dart语言语法类似于C。Dart语言为了更好的适应FlutterUI框架,在内存分配和垃圾回收做了很多优化。
因为Dart在连续分配多个对象的时候,所需消耗的资源非常少。Dart虚拟机可以快速分配内存给短期生存的对象,这样可以使很复杂的UI在60ms内完成一帧的渲染(实际感觉每一帧渲染时间更短),这样就保证了Flutter可以平滑的展示UI滑动及动画等效果。Flutter团队与Dart团队的密切合作让提升效率变得更加容易。
FlutterUI开发样式
Flutter在开发UI界面的时候,又比较像HTML的标签式语言,前文也提到,这是受Flutter创始人之一的Ian Hickson影响。其实很多UI布局都是类似标签的样式来编写的,比如Android的XML以及网页的HTML,所以Flutter会采用这样一个成熟的布局开发样式。
new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
new FlatButton(
color: Colors.blue,
)
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
Flutter插件、依赖与包管理器
Flutter与RN一样,在原生开发中很依赖于插件来调用系统API,毕竟它是一个UI框架。但是现阶段的Flutter插件并不是像RN那么全,可以看到维护Flutter的开发者只有200多人,而维护react-native的开发者已经近1700人了,一个数量级之差的维护者肯定在插件数量与开发体验上差别很大。
在包管理上,flutter并不需要依赖第三方类似于RN的npm包管理器来添加依赖,flutter本身就自带了包管理器,只需要在pubspec.yaml文件中添加相关依赖即可。但是,因为Google的库在国不能访问,需要添加环境变量指定库镜像才可以使用。
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
Flutter框架特性
在代码实现上,Flutter并没有Android的findViewById,页面布局是通过有状态Widget(StatefulWidget)和无状态Widget(StatelessWidget)实现的。顾名思义,无状态的Widget就是一些不可以改变的UI,而需要改变的UI则是通过有状态的Widget来实现,并且通过setStatus()来刷新UI的状态:
...
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
...
setState(() {
_counter--;
});
这种方法很简单的实现了动态化的UI及Android长久以来希望达到的目标 —— data binding。
四、Flutter待完善的方面及使用中遇到的问题
Flutter至今没有反射
Dart并不是没有反射,dart:mirrors就具有Mirror概念的反射。在安全、分发、部署方面,Mirror-Base具有很大优势。但是反射生成的代码冗长,会使Flutter编译过后的包很大。Flutter通过将Dart编译成原生代码本身就会增加包大小,再加上反射的话包大小更会进一步扩大。所以Flutter团队在现阶段并没有开放dart:mirrors的使用。
没有反射也就意味着Json String to Model 也没有办法完成,对于这一点,官方也比较无奈。至今Flutter中Dart只支持将JsonString 转化为Map,然后再由开发者手写代码将key值一一对应到相应的字段上。
/**
"result": {
"status": "ALREADY",
"scur": "CNY",
"tcur": "EUR",
"ratenm": "人民币/欧元",
"rate": "0.127839",
"update": "2018-07-13 23:28:01"
}
*/
///
class ExchangeResult {
final String status;
final String scur;
final String tcur;
final String ratem;
final String rate;
final String update;
ExchangeResult(this.status, this.scur, this.tcur, this.ratem, this.rate,
this.update,);
ExchangeResult.fromJson(Map<String, dynamic> json)
: status = json['status'],
scur = json['scur'],
tcur = json['tcur'],
ratem = json['ratem'],
rate = json['rate'],
update = json['update'];
}
Map exchangeMap = json.decode(Utf8Codec().decode(response.bodyBytes));
var resultModel = new ExchangeResult.fromJson(exchangeMap);
Dart-langhttp请求response解码问题
Http请求返回的response中Header会包含编码格式charset=utf-8,官方给出的Demo如下:
var dataURL = "http://api.k780.com?app=finance.rate&scur=CNY&tcur=GBP&appkey=35134&sign=fb020c3129435bb5ff21b7113e9cb1c1&format=json";
var response = await http.get(dataURL);
print(response.body);
看起来是非常简单的实现了异步请求服务,但是如果返回的charset后面多加了一个";"的话 (charset=utf-8;),http client就不会自动根据header中的charset解析,会返回错误:
[ERROR:topaz/lib/tonic/logging/dart_error.cc(16)] Unhandled exception:
Error on line 1, column 33: Invalid media type: expected
所以,如果要解析返回的json string,必须要指定UTF8字符解析response才可以:
print(Utf8Codec().decode(response.bodyBytes));
Flutter并不能指定Dart lang version
安装Flutter的同时也会安装Dart lang SDK,集成在Flutter的SDK中的$FLUTTER_SDK/bin/cache/dart-sdk。假如你发现一个Dart lang bug,那就需要更改DartSDK的代码,但是这个修正并不能让你马上使用。因为Flutter与Dart lang SDK 的version是一一绑定好的。