"A fast app is great, but a smooth app is even better."
使用Flutter beta版上线了一个APP的故事
2018年的11月底,我第一次打开Flutter的官网,想看看Flutter到底是什么;3个星期后,我们赶在Apple的App Store审核团队圣诞节休假前,提交了第一个使用Flutter开发的App。当然,是iOS和Android双端同时提交。
我们使用Flutter开发的产品是一个图片feed流,作为一个模块嵌入到一个美颜相机里面。
在接下来的2个月内,我们保持着每2个星期发版的频率,成功上线了以下核心功能:
- 登陆
- 作品发布
- 作品删除
- 关注他人
- 作品点赞
- 作品评论
- 消息中心
- 发现feed流
- 关注feed流(你所关注的发布者的动态feed流)
- 各个页面时长的数据统计
再来看看我们整个Feed流团队有多少人:
- 产品经理:1人
- 作品质量把控:1人
- 后端开发:2人
- iOS开发:1人
- Android开发:2人
- Web端开发:2人
- 测试:2人
以上是第一个版本发布后的团队组成。其实,我们的第一个版本期间,开发只有4人(后端,iOS,Android,Web,各1人)。
在使用Flutter的这几个月内,我被Flutter这特立独行的跨端思想和优秀的表现所感动。从一开始的全身每个细胞都在抵触到短短几个星期之后就差点成为脑残粉,我经历了难忘的一次“真香”之旅。
思绪回到我们决定使用Flutter的那一天,我们做了一个冷静之后看起来十分激进和冒险的技术选型。因为我们当时的场景是:Flutter beta版 和已有的native APP混合 已有的native是一个相机类App Flutter开发的功能是一个feed流。这个组合在当时的场景下是十分苛刻的。接下来我具体解释一下其中的挑战在哪里:
- Flutter beta:因为是beta版本,所以框架功能不面面俱到,也存在bug。
- Flutter和已有的native混合:因为当时使用的是beta版本,并没有官方的集成方案。混合模式下如何开发,调试,打包,集成之后对整个App包大小的影响有多大,都是挑战。
- 已有的native是一个相机类App:相机类的App本身占用的内存就相对来说很大。
- feed流:feed流功能,本身对性能要求高,因为刷起来需要流畅,因为图片很多,对内存也是有极高的要求。
- 上线时间短:第一个版本,必须要赶在圣诞节前上,从项目立项到上线,不过3个星期的时间。
- 上线频率高:保持每2周发一个新版。
但是,上面的这些挑战,Flutter都很好地消化了。基于我个人的开发体验来讲,是因为Flutter具备以下优点:
- 高性能:我们的核心是feed流,对于一个feed流来说,滑起来流畅是重要的指标。因为Flutter的高性能的特性,我们的feeds能达到60fps。
- 双端一致:因为我们的功能是iOS和Android双端都要支持,因为Flutter优秀的跨端技术,使得我们写一份代码,可以同时在双端运行,并且保持双端UI,功能等高度的一致性。
- 减少测试时间:因为双端的高度一致,测试同学不需要每一端都事无巨细地测试,特别是UI的部分,只需要测试一端便行。
- 高效的开发效率:Flutter提供丰富又高度定制化的组件,加上Flutter拥有Hot Reload,使得你在秒级的时间内看到你的代码更改,而不是像以前那样哪怕只是1px的改动,都需要经历重新的编译,打包,安装,可能2分钟过去之后,才能看到最后的结果。
看到这里的同学,脑子里面可能一直萦绕着一个问号:你一直在说的Flutter到底是什么?
Flutter是什么?
Flutter是谷歌推出的跨平台UI框架,目前已经支持的平台有:mobile,desktop,web。Flutter的目标是以上平台能支持到以下对应的操作系统或平台:
- mobile:iOS && Android
- desktop:macOS, Windows,Linux
- web:mobile && PC
就目前来说,mobile端的支持是最成熟的。desktop端仅支持macOS,web端有beta版可以使用,但是还不是十分成熟。
Flutter VS React Native
说到跨平台的技术或者框架,我们可能自然会想到React Native。虽然很多产品和公司现目前已经停止了对React Native的使用,但是不可否认地是,在Flutter之前,React Native是最为大家熟悉的跨端UI框架。
所以,这里简单地做一个Flutter和React native的对比。二者最大的不同在于实现跨端的原理不同,从而导致了二者在相关的指标上的差异。所以,接下来,我会就二者的跨端原理做一下简单地比较。
(flutter vs react native)
上图以在Android端为例。
假如我们需要一个Native端的button,React Native的原理是,使用JavaScript写一个button,由C 写的Bridge把JavaScript写的button转换为Java写的button,最终被编译为机器码。
但是在Flutter里面,采用一门新的语言(Dart)来写button,因为Dart也是基于VM的一门语言,所以,Dart可以直接被编译为机器语言。
所以,我们可以看出Flutter没有采用‘Bridge’,这样带来的好处是:
- 高性能:因为省去了JavaScript和Native之间的转换过程。
- 双端统一:不论是在iOS端还是Native端,都是采用Dart语言编写UI,底层都是采用Skia这个图形库绘制。
以上,我们对Flutter和React Native做了简单地对比,相信大家已经能看出Flutter天然具有的一些优势了。接下来,我们更全面地来认识一下Flutter优秀的特性:
Flutter有什么特性
- 开发效率高
- 双端一致的UI
- 丰富而美丽的UI
- 媲美Native一般的性能
- open source
开发效率高:Flutter拥有hot reload功能,每一次修改代码之后,只需要保存,不论是在模拟器上还是在连接的真机,都可以在秒级的时间内,马上看到效果,而不用再像以前以下需要经历重新的编译,打包,安装。
双端一致的UI:React Native是把JavaScript代码转换为Java或者Swift语言,最终调用平台各自的渲染机制来渲染UI。而Flutter不论是在iOS还是Android端都统一采用Skia(一个二维图形库)来渲染UI,这样就从根源上解决了由于平台不一致带来的UI不一致的问题。
丰富和美丽的UI:Flutter内置的Material Design和Cupertino(iOS风格)组件,能让你的App拥有现代化的漂亮的UI。又因为在上一点提到的,因为采用了Skia,你页面上的每一个像素都是Skia画的,所以你可以对你的组件进行高度的定制化。
媲美Native一般的性能:Flutter使用Dart作为它的编程语言,dart的编译器会把你写的Flutter代码直接编译成机器码,从而带来跟native一样的性能。
如果只是平白地罗列以上的这些优点,可能无法令人信服。接下来我们就更底层,更细致地来论证以上观点。
首先,我们来深入了解一下Dart这门开发语言,因为它对Flutter高效的开发效率,和高性能都是至关重要的。
为什么Futter采用Dart语言
在了解Dart之前,我们先来认识2个概念:AOT,JIT。
- AOT: Ahead-of-Time
- JIT: Just-in-Time
AOT是在运行前,已经完全编译好;而JIT则是在运行中会进行分析和编译。AOT的优点是运行速度快,因为它不需要在运行时再进行分析和编译,因为它已经提前编译好了。相对的,JIT的运行速度慢,因为它在运行的过程中会停下来做分析和编。
思考一下,在日常开发native app的时候,我们希望代码修改可以以最快的速度被看到,而不用每一次都需要经历编译,打包,安装。相应地,当我们在使用一个native app的时候,我希望它能很快响应我的操作,比如滑动的时候,页面很流畅;动画也会不会卡顿。
为了满足以上2种需求,我们希望拥有一种结合了JIT和AOT的技术。事实上,Dart正是如此。
在我们开发的时候(debug mode)的时候,Dart采用JIT的模式,我们前面提到的Hot Reload也正是依赖于此。Hot Reload的工作原理是通过把修改后的源代码文件塞给Dart的虚拟机(VM),等虚拟机根据最新的属性和方法更新类文件之后,Flutter会自动重新构建组件树(widget tree),从而你可以迅速地看到你修改的结果。
在打包的时候(release mode)的时候,Dart采用AOT模式。AOT模式的好处是使得用户可以在很短的时间内启动App,在使用App的时候,也会很流畅,因为所有的东西都已经被编译好了。
Dart的线程
如果你了解Java,C ,或者Swift等,你知道这些支持多线程并发的语言,采用的是一种叫做抢占式调度(Preemptive scheduling)的机制。抢占式调度,即操作系统给每一个进程都分配一定的CPU占用时间,当进程A的时间已经花完,这时候就该轮到进程B来占用CPU了。但是,当不同的进程或者线程使用了同一个资源(比如同一段内存)就会造成资源竞争(Race condition)。
资源竞争可以造成严重的后果,比如让你的App崩溃掉,或者造成数据的丢失。经典地解决资源竞争的办法是加锁,但是加锁本身又能带来别的问题,比如死锁和资源饥饿。
(Preemptive Scheduling)
Dart则采用了另外一种思路。线程到了Dart里面叫做isolate。不同的isolate之间是不共享内存的,也是独立做垃圾回收的。
我们的一个Dart程序执行在一个isolate里面。在一个isolate里面,所有的事件都是通过eventLoop的方式来进行异步处理。
相应地,Dart提供futures,async, await来处理异步请求。这使得我们在渲染UI的同时,可以进行一个HTTP的请求或者读取文件之类的的操作,但是不会造成页面的卡顿。
Dart统一了UI编写
Dart不仅是从语言特性层面为Flutter的性能起到至关重要的作用,在对开发效率的提高上也是十分终于的。前面我们已经提到了Hot Reload功能以来与Dart的JIT编译模式,除此之外Dart统一的UI组件编写方式,也对我们日常的开发效率起到了非常大作用。我们先来看一段使用Dart编写一个Flutter组件的示例:
(dart-widget)
Dart编写一个组件的方式,和iOS,Android,Web端的方式都不一样。
传统的Web端开发,是把一个组件所需要的HTML,CSS, JavaScript分开到不同的模块里面。假如你需要改动css,你得先跳到css所有的领域(可能是另外一个单独的css文件,或者假如你使用vue,那么就是在这个.vue文件的style模块)。然而,在Dart里面,一个组件的dom,样式,事件处理都是作为一个组件的属性存在,他们都是在一个地方,既不会分开到不同的文件,也不会分开到不同的模块。
我记得我一开始写Flutter的组件的时候,非常地不习惯,甚至产生抗拒心理。因为Flutter的组件结构方式与我写了多年的web端组件写法是完全对立的。但是,当我写了2个星期后(我在网上到讨论,一般大家的过渡期也是2个周),开始觉得这种写法是如此自然又高效, 甚至开始怀疑以前自己写的那些代码:web端那种把HTML, CSS, JavaScritp分开的形式,是不是本身是种错误。也开始反思,为什么我从来没有怀疑过这种既定规则的合理行。
因为我自己没有长期iOS端和Android端开发的经验,我不知道从iOS端的Layout布局和Android端的XML布局转换到Dart,是怎样的心路历程。但是,下面是我从网上找到的一些感想:
(native)
Dart dev tool
Dart提供了一些工具来帮助你日常的开发和调试,其中一个是非常厉害的工具就是Dart dev tool。举一个例子:下图展示的是一个在Flutter里面常见的bug:子元素溢出了。这个时候你打开Dart dev tool,你可以看到这个组件的布局,在这个工具上会显现这个元素相关的一些属性值,给你提供排除bug的思路。比如这个例子里,我们看到flex的值为‘null’,这可能是bug的原因,你可以通过下拉框选择一个flex的值,看是否可以解决这个bug:
(dar dev tool)
是否推荐项目采用Flutter
前面花了很大的篇幅来介绍我和我实际使用Flutter上线了一个App的故事和感受,也从Skia和Dart层面去分析了为什么Flutter具备有那么多的优点,而不只是官方宣传。那么最后的最后,作为一个还十分年轻的技术,flutter是否适合在项目上使用呢?
没有一个技术是完美的,但是除了一些不可抗力的因素外,我们去做一个技术选型,依据的标准应该是它的优点是否超越它的缺点。不如我们再次来总结一下Flutter比较核心的的优点和缺点:
Flutter的优点:
- 跨端,跨平台
- 双端高度一致的UI
- 漂亮的UI
- 高性能
- 开发效率高
Flutter目前存在的缺点:
- 包的大小不算小(特别是混合项目)
- 目前github上的open的issue有7000多
- Flutter的error message不友好
- 可能会有内存泄漏的问题(常见在iOS端)
Flutter从2018.02发布beta版,2018.12发布1.0版本,短短2年时间,到现在github上的start数量已经有81.6K。不论是国内还是国外,已经有大量的使用Flutter开发的产品,比如 Realtor.com Real Estate Search, Google Assistant,咸鱼等。
当下,Flutter和Fuchsia(谷歌正在研发的一个新的操作系统)都是谷歌的重心,所以大概率Flutter不会成为一个烂尾的项目。而且,就Flutter目前拥有的成绩证明,它已经足够优秀,何况它还这么年轻。
所以从我自己的角度来说,十分推荐采用Flutter。可能web端和desktop端目前还不那么成熟,但是native端可以大胆尝试。
本文版权属ThoughtWorks公司所有,如需转载请在后台留言联系。