mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 14:13:46 +08:00
del
This commit is contained in:
129
极客时间专栏/geek/Flutter核心技术与实战/Flutter开发起步/03 | 深入理解跨平台方案的历史发展逻辑.md
Normal file
129
极客时间专栏/geek/Flutter核心技术与实战/Flutter开发起步/03 | 深入理解跨平台方案的历史发展逻辑.md
Normal file
@@ -0,0 +1,129 @@
|
||||
<audio id="audio" title="03 | 深入理解跨平台方案的历史发展逻辑" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/68/b8/6831a6a31ad3e3a6413121bfefc82ab8.mp3"></audio>
|
||||
|
||||
你好,我是陈航。
|
||||
|
||||
今天,我会从跨平台开发方案的诞生背景、原理和发展历史的角度,和你聊聊这些常见的开发方案,以及针对不同的场景我们应该如何选择对应的方案。
|
||||
|
||||
## 浅述跨平台开发的背景
|
||||
|
||||
我们当下所处的移动互联网时代,以它独有的变革方式,带给我们快捷、经济、安全和方便,改变着生活的方方面面。而对于企业来说,移动应用已然成为各类手机终端上一张必备的产品名片。
|
||||
|
||||
在移动互联网的浪潮下,我们开发的应用要想取胜,开发效率和使用体验可以说是同等重要。但是,使用原生的方式来开发App,就要求我们必须针对iOS和Android这两个平台分别开发,这对于中小型团队来说就是隐患和额外的负担。
|
||||
|
||||
因为这样的话,我们不仅需要在不同的项目间尝试用不同的语言去实现同样的功能,还要承担由此带来的维护任务。如果还要继续向其他平台(比如Web、Mac或Windows)拓展的话,我们需要付出的成本和时间将成倍增长。而这,显然是难以接受的。于是,跨平台开发的概念顺势走进了我们的视野。
|
||||
|
||||
所以从本质上讲,**跨平台开发是为了增加业务代码的复用率,减少因为要适配多个平台带来的工作量,从而降低开发成本。**在提高业务专注度的同时,能够为用户提供一致的用户体验。用一个词来概括这些好处的话,就是“多快好省”。
|
||||
|
||||
“一次编码,到处运行”。二十多年前Java正是以跨平台特性的口号登场,击败了众多竞争对手。这个口号,意味着Java可以在任何平台上进行开发,然后编译成一段标准的字节码后,就可以运行在任何安装有Java虚拟机(JVM)的设备上。虽然现在跨平台已经不是Java的最大优势(而是它繁荣的生态),但不可否认它当年打着跨平台旗号横空出世确实势不可挡。
|
||||
|
||||
而对于移动端开发来讲,如果能实现“一套代码,多端运行”,这样的技术势必会引发新的生产力变革,在目前多终端时代的大环境下,可以为企业节省人力资源上,从而带来直接的经济效益。
|
||||
|
||||
伴随着移动端的诞生和繁荣,为了满足人们对开发效率和用户体验的不懈追求,各种跨平台的开发方案也如雨后春笋般涌现。除了React Native和Flutter之外,这几年还出现过许多其他的解决方案,接下来我将会为你一一分析这些方案。这样,你在选择适合自己的移动开发框架时,也就有章可循了。
|
||||
|
||||
在此,我特地强调一下,我在下文提到的跨平台开发方案,如果没有特殊说明的话,指的就是跨iOS和Android开发。
|
||||
|
||||
## 跨平台开发方案的三个时代
|
||||
|
||||
根据实现方式的不同,业内常见的观点是将主流的跨平台方案划分为三个时代。
|
||||
|
||||
- Web容器时代:基于Web相关技术通过浏览器组件来实现界面及功能,典型的框架包括Cordova(PhoneGap)、Ionic和微信小程序。
|
||||
- 泛Web容器时代:采用类Web标准进行开发,但在运行时把绘制和渲染交由原生系统接管的技术,代表框架有React Native、Weex和快应用,广义的还包括天猫的Virtual View等。
|
||||
- 自绘引擎时代:自带渲染引擎,客户端仅提供一块画布即可获得从业务逻辑到功能呈现的多端高度一致的渲染体验。Flutter,是为数不多的代表。
|
||||
|
||||
接下来,我们先看一下目前使用最广泛的Web容器方案。
|
||||
|
||||
### Web容器时代
|
||||
|
||||
Web时代的方案,主要采用的是原生应用内嵌浏览器控件WebView(iOS为UIWebView或WKWebView,Android为WebView)的方式进行HTML5页面渲染,并定义HTML5与原生代码交互协议,将部分原生系统能力暴露给HTML5,从而扩展HTML5的边界。这类交互协议,就是我们通常说的JS Bridge(桥)。
|
||||
|
||||
这种开发模式既有原生应用代码又有Web应用代码,因此又被称为Hybrid开发模式。由于HTML5代码只需要开发一次,就能同时在多个系统运行,因此大大降低了开发成本。
|
||||
|
||||
由于采用了Web开发技术,社区和资源非常丰富,开发效率也很高。但,**一个完整HTML5页面的展示要经历浏览器控件的加载、解析和渲染三大过程,性能消耗要比原生开发增加N个数量级**。
|
||||
|
||||
接下来,我以加载过程为例,和你说明这个过程的复杂性。
|
||||
|
||||
1. 浏览器控件加载HTML5页面的HTML主文档;
|
||||
1. 加载过程中遇到外部CSS文件,浏览器另外发出一个请求,来获取CSS文件;
|
||||
1. 遇到图片资源,浏览器也会另外发出一个请求,来获取图片资源。这是异步请求,并不会影响HTML文档的加载。
|
||||
1. 加载过程中遇到JavaScript文件,由于JavaScript代码可能会修改DOM树,因此HTML文档会挂起渲染(加载解析渲染同步)的线程,直到JavaScript文件加载解析并执行完毕,才可以恢复HTML文档的渲染线程。
|
||||
1. JavaScript代码中有用到CSS文件中的属性样式,于是阻塞,等待CSS加载完毕才能恢复执行。
|
||||
|
||||
而这,只是完成HTML5页面渲染的最基础的加载过程。加载、解析和渲染这三个过程在实际运行时又不是完全独立的,还会有交叉。也就是说,会存在一边加载,一边解析,一边渲染的现象。这,就使得页面的展示并不像想象中那么容易。
|
||||
|
||||
通过上面的分析你可以看出,一个HTML5页面的展示是多么得复杂!这和原生开发通过简单直接的创建控件,设置属性后即可完成页面渲染有非常大的差异。Web与原生在UI渲染与系统功能调用上各司其职,因此这个时代的框架在Web与原生系统间还有比较明显的、甚至肉眼可见的边界。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6d/79/6d4035e44b4af68b7588a750fec06c79.png" alt="">
|
||||
|
||||
我也曾碰到过很多人觉得跨平台开发不靠谱。但其实,Web容器方案是跨平台开发历史上最成功的例子。也正是因为它太成功了,以至于很多人都忽略了它也是跨平台方案之一。
|
||||
|
||||
### 泛Web容器时代
|
||||
|
||||
虽然Web容器方案具有生态繁荣、开发体验友好、生产效率高、跨平台兼容性强等优势,但它最大的问题在于承载着大量Web标准的Web容器过于笨重,以至于性能和体验都达不到与原生同样的水准,在复杂交互和动画上较难实现出优良的用户体验。
|
||||
|
||||
而在实际的产品功能研发中,我们通常只会用到Web标准中很小的一部分。面对这样的现实,我们很快就想到:能否对笨重的Web容器进行功能裁剪,在仅保留必要的Web标准和渲染能力的基础上,使得友好的开发体验与稳定的渲染性能保持一个平衡?
|
||||
|
||||
答案当然是可以。
|
||||
|
||||
泛Web容器时代的解决方案优化了Web容器时代的加载、解析和渲染这三大过程,把影响它们独立运行的Web标准进行了裁剪,以相对简单的方式支持了构建移动端页面必要的Web标准(如Flexbox等),也保证了便捷的前端开发体验;同时,这个时代的解决方案基本上完全放弃了浏览器控件渲染,而是采用原生自带的UI组件实现代替了核心的渲染引擎,仅保持必要的基本控件渲染能力,从而使得渲染过程更加简化,也保证了良好的渲染性能。
|
||||
|
||||
也就是说,在泛Web容器时代,我们仍然采用前端友好的JavaScript进行开发,整体加载、渲染机制大大简化,并且由原生接管绘制,即将原生系统作为渲染的后端,为依托于JavaScript虚拟机的JavaScript代码提供所需要的UI控件的实体。这,也是现在绝大部分跨平台框架的思路,而React Native和Weex就是其中的佼佼者。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3a/0b/3af2c590f42c0b924a22bb135134380b.png" alt="">
|
||||
|
||||
为了追求性能体验的极致,并进一步维持方案的简单可扩展性,有些轻量级的跨平台方案甚至会完全抛弃Web标准、放弃JavaScript的动态执行能力而自创一套原生DSL,如天猫的[VirtualView](http://pingguohe.net/2017/12/07/Tangram-2.html)框架。从广义上来说,这些方案也是泛Web容器类方案。
|
||||
|
||||
### 自绘引擎时代
|
||||
|
||||
泛Web容器时代使用原生控件承载界面渲染,固然解决了不少性能问题,但同时也带来了新的问题。抛开框架本身需要处理大量平台相关的逻辑外,随着系统版本变化和API的变化,我们还需要处理不同平台的原生控件渲染能力差异,修复各类奇奇怪怪的Bug。始终需要Follow Native的思维方式,就使得泛Web容器框架的跨平台特性被大打折扣。
|
||||
|
||||
而这一时期的代表Flutter则开辟了一种全新的思路,即从头到尾重写一套跨平台的UI框架,包括渲染逻辑,甚至是开发语言。
|
||||
|
||||
- 渲染引擎依靠跨平台的Skia图形库来实现,Skia引擎会将使用Dart构建的抽象的视图结构数据加工成GPU数据,交由OpenGL最终提供给GPU渲染,至此完成渲染闭环,因此可以在最大程度上保证一款应用在不同平台、不同设备上的体验一致性。
|
||||
- 而开发语言选用的是同时支持JIT(Just-in-Time,即时编译)和AOT(Ahead-of-Time,预编译)的Dart,不仅保证了开发效率,更提升了执行效率(比使用JavaScript开发的泛Web容器方案要高得多)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/85/2c/85dfb3f8a803a0bf573f3fce63ddc92c.png" alt="">
|
||||
|
||||
通过这样的思路,Flutter可以尽可能地减少不同平台之间的差异, 同时保持和原生开发一样的高性能。所以说,Flutter成了三类跨平台移动开发方案中最灵活的那个,也成了目前最受业界关注的框架。
|
||||
|
||||
现在,我们已经弄明白了三类跨平台方案,那么我在开发应用的时候,到底应该如何选择最适合自己的框架呢?
|
||||
|
||||
## 我该选择哪一类跨平台开发方案?
|
||||
|
||||
从不同的角度来看,三个时代的跨平台框架代表们在开发效率、渲染性能、维护成本和社区生态上各有优劣,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/3f/40a62d2a54b1814d07479dca1178fb3f.png" alt="">
|
||||
|
||||
我们在做技术选型时,可以参考以上维度,从开发效率、技术栈、性能表现、维护成本和社区生态来进行综合考虑。比如,是否必须支持动态化?是只解决Android、iOS的跨端问题,还是要包括Web?对性能要求如何?对多端体验的绝对一致性和维护成本是否有强诉求?
|
||||
|
||||
从各个维度综合考量,React Native和Flutter无疑是最均衡的两种跨平台开发方案,而其他的方案或多或少都“偏科严重”。
|
||||
|
||||
- React Native依托于Facebook,经过4年多的发展已经成长为跨平台开发方案的实际领导者,并拥有较为丰富的第三方库和开发社区;
|
||||
- Flutter以挑战者姿态出现在我们的面前,可以提供更彻底的跨平台技术解决方案。虽然Flutter推出时间不长,但也有了诸多商用案例,加上清晰的[产品路线图](https://mp.weixin.qq.com/s/aRhQdMd0R74adph9_V0wgQ)和Google的强大号召力,Flutter未来的发展非常值得期待。
|
||||
|
||||
那么问题来了,我究竟应该选择React Native还是Flutter呢?
|
||||
|
||||
在这里,我和你说一下我的建议吧。
|
||||
|
||||
**对于知识学习来说,**这两个应用层面的框架最好都学。学习的过程中最重要的是打好基础,深入理解框架的原理和设计思想,重点思考它们的API设计的取舍,发现它们的共性和差异。
|
||||
|
||||
Flutter作为后来者,也从React Native社区学习和借鉴了不少的优秀设计,很多概念两边都有对应,比如React Native的Component和Flutter的Widget、Flex布局思想、状态管理和函数式编程等等,这类的知识都是两个框架通用的技术。**未来也许还会出现新的解决方案,老框架也会不断更新,只有掌握核心原理才能真正立于不败之地。**
|
||||
|
||||
**对于实际项目来说,**这两个框架都已达到了大面积商业应用的标准。综合成熟度和生态,目前俩看React Native略胜Flutter。因此,如果是中短期项目的话,我建议使用React Native。但作为技术选型,我们要看得更远一些。Flutter的设计理念比较先进,解决方案也相对彻底,在渲染能力的一致性以及性能上,和React Native相比优势非常明显。
|
||||
|
||||
此外,Flutter的野心不仅仅是移动端。前段时间,Google团队已经完成了Hummingbird,即Flutter的Web的官方Demo,在桌面操作系统的探索上也取得了进展,未来大前端技术栈是否会由Flutter完成统一,值得期待。
|
||||
|
||||
## 小结
|
||||
|
||||
这就是今天分享的全部内容了。
|
||||
|
||||
在不同平台开发和维护同一个产品,所付出的成本一直以来一个令人头疼的问题,于是各类跨平台开发方案顺应而生。从Web容器时代到以React Native、Weex为代表的泛Web容器时代,最后再到以Flutter为代表的自绘引擎时代,这些优秀的跨平台开发框架们慢慢抹平了各个平台的差异,使得操作系统的边界变得越来越模糊。
|
||||
|
||||
与此同时,这个时代对开发者的要求也到达了一个新的阶段,拥抱大前端的时代已经向我们走来。在这个专栏里,我会假设你有一定的前端(Android、iOS或Web)开发基础。比如,你知道View是什么,路由是什么,如何实现一个基本页面布局等等。我会让希望迅速掌握Flutter开发的爱好者们,通过一种比较熟悉和友好的路径去学习Flutter相关的代码和程序,以及背后的原理和设计思想。
|
||||
|
||||
## 思考题
|
||||
|
||||
你有哪些跨平台开发框架的使用经历呢?
|
||||
|
||||
欢迎你在评论区给我留言分享你的经历和观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
<audio id="audio" title="04 | Flutter区别于其他方案的关键技术是什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/de/ab/de5bdc701dffe439dee3a9db7138c8ab.mp3"></audio>
|
||||
|
||||
你好,我是陈航。
|
||||
|
||||
Flutter是什么?它出现的动机是什么,解决了哪些痛点?相比其他跨平台技术,Flutter的优势在哪里?……相信很多人在第一眼看到Flutter时,都会有类似的疑问。
|
||||
|
||||
别急,在今天的这篇文章中,我会与你介绍Flutter的历史背景和运行机制,并以界面渲染过程为例与你讲述其实现原理,让你对Flutter能够有一个全方位的认知和感受。在对Flutter有了全面了解后,这些疑问自然也就迎刃而解了。
|
||||
|
||||
接下来,我们就从Flutter出现的历史背景开始谈起吧。
|
||||
|
||||
## Flutter出现的历史背景
|
||||
|
||||
为不同的操作系统开发拥有相同功能的应用程序,开发人员只有两个选择:
|
||||
|
||||
1. 使用原生开发语言(即Java和Objective-C),针对不同平台分别进行开发。
|
||||
1. 使用跨平台解决方案,对不同平台进行统一开发。
|
||||
|
||||
原生开发方式的体验最好,但研发效率和研发成本相对较高;而跨平台开发方式研发虽然效率高,但为了抹平多端平台差异,各类解决方案暴露的组件和API较原生开发相比少很多,因此研发体验和产品功能并不完美。
|
||||
|
||||
所以,最成功的跨平台开发方案其实是依托于浏览器控件的Web。浏览器保证了99%的概率下Web的需求都是可以实现的,不需要业务将就“技术”。不过,Web最大的问题在于它的性能和体验与原生开发存在肉眼可感知的差异,因此并不适用于对体验要求较高的场景。
|
||||
|
||||
对于用户体验更接近于原生的React Native,对业务的支持能力却还不到浏览器的5%,仅适用于中低复杂度的低交互类页面。面对稍微复杂一点儿的交互和动画需求,开发者都需要case by case地去review,甚至还可能要通过原生代码去扩展才能实现。
|
||||
|
||||
这些因素,也就导致了虽然跨平台开发从移动端诞生之初就已经被多次提及,但到现在也没有被很好地解决。
|
||||
|
||||
带着这些问题,我们终于迎来了本次专栏的主角——Flutter。
|
||||
|
||||
Flutter是构建Google物联网操作系统Fuchsia的SDK,主打跨平台、高保真、高性能。开发者可以通过 Dart语言开发App,一套代码可以同时运行在 iOS 和 Android平台。 Flutter使用Native引擎渲染视图,并提供了丰富的组件和接口,这无疑为开发者和用户都提供了良好的体验。
|
||||
|
||||
从2017年5月,谷歌公司发布的了Alpha版本的Flutter,到2018年底Flutter Live发布的1.0版本,再到现在最新的1.5版本(截止至2019年7月1日),Flutter正在赢得越来越多的关注。
|
||||
|
||||
很多人开始感慨,跨平台技术似乎终于迎来了最佳解决方案。那么,接下来我们就从原理层面去看看,Flutter是如何解决既有跨平台开发方案问题的。
|
||||
|
||||
## Flutter是怎么运转的?
|
||||
|
||||
与用于构建移动应用程序的其他大多数框架不同,Flutter是重写了一整套包括底层渲染逻辑和上层开发语言的完整解决方案。这样不仅可以保证视图渲染在Android和iOS上的高度一致性(即高保真),在代码执行效率和渲染性能上也可以媲美原生App的体验(即高性能)。
|
||||
|
||||
这,就是Flutter和其他跨平台方案的本质区别:
|
||||
|
||||
- React Native之类的框架,只是通过JavaScript虚拟机扩展调用系统组件,由Android和iOS系统进行组件的渲染;
|
||||
- Flutter则是自己完成了组件渲染的闭环。
|
||||
|
||||
那么,**Flutter是怎么完成组件渲染的呢**?这需要从图像显示的基本原理说起。
|
||||
|
||||
在计算机系统中,图像的显示需要CPU、GPU和显示器一起配合完成:CPU负责图像数据计算,GPU负责图像数据渲染,而显示器则负责最终图像显示。
|
||||
|
||||
CPU把计算好的、需要显示的内容交给GPU,由GPU完成渲染后放入帧缓冲区,随后视频控制器根据垂直同步信号(VSync)以每秒60次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。
|
||||
|
||||
操作系统在呈现图像时遵循了这种机制,而Flutter作为跨平台开发框架也采用了这种底层方案。下面有一张更为详尽的示意图来解释Flutter的绘制原理。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/95/2a/95cb258c9103e05398f9c97a1113072a.png" alt="">
|
||||
|
||||
可以看到,Flutter关注如何尽可能快地在两个硬件时钟的VSync信号之间计算并合成视图数据,然后通过Skia交给GPU渲染:UI线程使用Dart来构建视图结构数据,这些数据会在GPU线程进行图层合成,随后交给Skia引擎加工成GPU数据,而这些数据会通过OpenGL最终提供给GPU渲染。
|
||||
|
||||
在进一步学习Flutter之前,我们有必要了解下构建Flutter的关键技术,即Skia和Dart。
|
||||
|
||||
## Skia是什么?
|
||||
|
||||
要想了解Flutter,你必须先了解它的底层图像渲染引擎Skia。因为,Flutter只关心如何向GPU提供视图数据,而Skia就是它向GPU提供视图数据的好帮手。
|
||||
|
||||
Skia是一款用C++开发的、性能彪悍的2D图像绘制引擎,其前身是一个向量绘图软件。2005年被Google公司收购后,因为其出色的绘制表现被广泛应用在Chrome和Android等核心产品上。Skia在图形转换、文字渲染、位图渲染方面都表现卓越,并提供了开发者友好的API。
|
||||
|
||||
因此,架构于Skia之上的Flutter,也因此拥有了彻底的跨平台渲染能力。通过与Skia的深度定制及优化,Flutter可以最大限度地抹平平台差异,提高渲染效率与性能。
|
||||
|
||||
底层渲染能力统一了,上层开发接口和功能体验也就随即统一了,开发者再也不用操心平台相关的渲染特性了。也就是说,Skia保证了同一套代码调用在Android和iOS平台上的渲染效果是完全一致的。
|
||||
|
||||
## 为什么是Dart?
|
||||
|
||||
除了我们在第2篇预习文章“[预习篇 · Dart语言概览](https://time.geekbang.org/column/article/104071)”中提到的,Dart因为同时支持AOT和JIT,所以具有运行速度快、执行性能好的特点外,Flutter为什么选择了Dart,而不是前端应用的准官方语言JavaScript呢?这个问题很有意思,但也很有争议。
|
||||
|
||||
很多人说,选择Dart是Flutter推广的一大劣势,毕竟多学一门新语言就多一层障碍。想想Java对Android,JavaScript对NodeJS的推动,如果换个语言可能就不一样了。
|
||||
|
||||
但,**Google公司给出的原因很简单也很直接**:Dart语言开发组就在隔壁,对于Flutter需要的一些语言新特性,能够快速在语法层面落地实现;而如果选择了JavaScript,就必须经过各种委员会和浏览器提供商漫长的决议。
|
||||
|
||||
事实上,Flutter的确得到了兄弟团队的紧密支持。2018年2月发布的Dart 2.0,2018年12月发布的Dart 2.1,2019年2月发布的Dart 2.2,2019年5月发布的Dart2.3,每次发布都包含了为Flutter量身定制的诸多改造(比如,改进的AOT性能、更智能的类型隐式转换等)。
|
||||
|
||||
当然,Google公司选择使用Dart作为Flutter的开发语言,我想还有其他更有说服力的理由:
|
||||
|
||||
1. Dart同时支持即时编译JIT和事前编译AOT。在开发期使用JIT,开发周期异常短,调试方式颠覆常规(支持有状态的热重载);而发布期使用AOT,本地代码的执行更高效,代码性能和用户体验也更卓越。
|
||||
1. Dart作为一门现代化语言,集百家之长,拥有其他优秀编程语言的诸多特性(比如,完善的包管理机制)。也正是这个原因,Dart的学习成本并不高,很容易上手。
|
||||
1. Dart避免了抢占式调度和共享内存,可以在没有锁的情况下进行对象分配和垃圾回收,在性能方面表现相当不错。
|
||||
|
||||
Dart是一门优秀的现代语言,最初设计也是为了取代JavaScript成为Web开发的官方语言。竞争对手如此强劲,最后的结果可想而知。这,也是为什么相比于其他热门语言,Dart的生态要冷清不少的原因。
|
||||
|
||||
而随着Flutter的发布,Dart开始转型,其自身定位也发生了变化,专注于改善构建客户端应用程序的体验,因此越来越多的开发者开始慢慢了解、学习这门语言,并共同完善它的生态。凭借着Flutter的火热势头,辅以Google强大的商业运作能力,相信转型后的Dart前景会非常光明。
|
||||
|
||||
## Flutter的原理
|
||||
|
||||
在了解了Flutter的基本运作机制后,我们再来深入了解一下Flutter的实现原理。
|
||||
|
||||
首先,我们来看一下Flutter的架构图。我希望通过这张图以及对应的解读,你能在开始学习的时候就建立起对Flutter的整体印象,能够从框架设计和实现原理的高度去理解Flutter区别其他跨平台解决方案的关键所在,为后面的学习打好基础,而不是直接一上来就陷入语言和框架的功能细节“泥潭”而无法自拔。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ac/2f/ac7d1cec200f7ea7cb6cbab04eda252f.png" alt="">
|
||||
|
||||
备注:此图引自[Flutter System Overview](https://flutter.dev/docs/resources/technical-overview)
|
||||
|
||||
Flutter架构采用分层设计,从下到上分为三层,依次为:Embedder、Engine、Framework。
|
||||
|
||||
- Embedder是操作系统适配层,实现了渲染Surface设置,线程设置,以及平台插件等平台相关特性的适配。从这里我们可以看到,Flutter平台相关特性并不多,这就使得从框架层面保持跨端一致性的成本相对较低。
|
||||
- Engine层主要包含Skia、Dart和Text,实现了Flutter的渲染引擎、文字排版、事件处理和Dart运行时等功能。Skia和Text为上层接口提供了调用底层渲染和排版的能力,Dart则为Flutter提供了运行时调用Dart和渲染引擎的能力。而Engine层的作用,则是将它们组合起来,从它们生成的数据中实现视图渲染。
|
||||
- Framework层则是一个用Dart实现的UI SDK,包含了动画、图形绘制和手势识别等功能。为了在绘制控件等固定样式的图形时提供更直观、更方便的接口,Flutter还基于这些基础能力,根据Material和Cupertino两种视觉设计风格封装了一套UI组件库。我们在开发Flutter的时候,可以直接使用这些组件库。
|
||||
|
||||
接下来,我**以界面渲染过程为例,和你介绍Flutter是如何工作的。**
|
||||
|
||||
页面中的各界面元素(Widget)以树的形式组织,即控件树。Flutter通过控件树中的每个控件创建不同类型的渲染对象,组成渲染对象树。而渲染对象树在Flutter的展示过程分为四个阶段:布局、绘制、合成和渲染。
|
||||
|
||||
### 布局
|
||||
|
||||
Flutter采用深度优先机制遍历渲染对象树,决定渲染对象树中各渲染对象在屏幕上的位置和尺寸。在布局过程中,渲染对象树中的每个渲染对象都会接收父对象的布局约束参数,决定自己的大小,然后父对象按照控件逻辑决定各个子对象的位置,完成布局过程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f9/00/f9e6bbf06231fbad54ed11ef291e8d00.png" alt="">
|
||||
|
||||
为了防止因子节点发生变化而导致整个控件树重新布局,Flutter加入了一个机制——布局边界(Relayout Boundary),可以在某些节点自动或手动地设置布局边界,当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/de/42961a84ecc8181d1afe7ffbaa1a55de.png" alt="">
|
||||
|
||||
### 绘制
|
||||
|
||||
布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置。Flutter会把所有的渲染对象绘制到不同的图层上。与布局过程一样,绘制过程也是深度优先遍历,而且总是先绘制自身,再绘制子节点。
|
||||
|
||||
以下图为例:节点1在绘制完自身后,会再绘制节点2,然后绘制它的子节点3、4和5,最后绘制节点6。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/b8/8c1d612990d9ada0508c5a41c9e4cab8.png" alt="">
|
||||
|
||||
可以看到,由于一些其他原因(比如,视图手动合并)导致2的子节点5与它的兄弟节点6处于了同一层,这样会导致当节点2需要重绘的时候,与其无关的节点6也会被重绘,带来性能损耗。
|
||||
|
||||
为了解决这一问题,Flutter提出了与布局边界对应的机制——重绘边界(Repaint Boundary)。在重绘边界内,Flutter会强制切换新的图层,这样就可以避免边界内外的互相影响,避免无关内容置于同一图层引起不必要的重绘。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/da/ee/da430de8f265f444c4801758902a8bee.png" alt="">
|
||||
|
||||
重绘边界的一个典型场景是Scrollview。ScrollView滚动的时候需要刷新视图内容,从而触发内容重绘。而当滚动内容重绘时,一般情况下其他内容是不需要重绘的,这时候重绘边界就派上用场了。
|
||||
|
||||
### 合成和渲染
|
||||
|
||||
终端设备的页面越来越复杂,因此Flutter的渲染树层级通常很多,直接交付给渲染引擎进行多图层渲染,可能会出现大量渲染内容的重复绘制,所以还需要先进行一次图层合成,即将所有的图层根据大小、层级、透明度等规则计算出最终的显示效果,将相同的图层归类合并,简化渲染树,提高渲染效率。
|
||||
|
||||
合并完成后,Flutter会将几何图层数据交由Skia引擎加工成二维图像数据,最终交由GPU进行渲染,完成界面的展示。这部分内容,我已经在前面的内容中介绍过,这里就不再赘述了。
|
||||
|
||||
接下来,我们再看看学习Flutter,都需要学习哪些知识。
|
||||
|
||||
## 学习Flutter需要掌握哪些知识?
|
||||
|
||||
终端设备越来越碎片化,需要支持的操作系统越来越多,从研发效率和维护成本综合考虑,跨平台开发一定是未来大前端的趋势,我们应该拥抱变化。而Flutter提供了一套彻底的移动跨平台方案,也确实弥补了如今跨平台开发框架的短板,解决了业界痛点,极有可能成为跨平台开发领域的终极解决方案,前途非常光明。
|
||||
|
||||
那么,**我们学习Flutter都需要掌握哪些知识呢?**
|
||||
|
||||
我按照App的开发流程(开发、调试测试、发布与线上运维)将Flutter的技术栈进行了划分,里面几乎包含了Flutter开发需要的所有知识点。而这些所有知识点,我会在专栏中为你一一讲解。掌握了这些知识点后,你也就具备了企业级应用开发的必要技能。
|
||||
|
||||
这些知识点,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/99/64/9959006fe52706a123cc7fc596346064.jpg" alt="">
|
||||
|
||||
有了这张图,你是否感觉到学习Flutter的路线变得更加清晰了呢?
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我带你了解了Flutter的历史背景与运行机制,并以界面渲染过程为例,从布局、绘制、合成和渲染四个阶段讲述了Flutter的实现原理。此外,我向你介绍了构建Flutter底层的关键技术:Skia与Dart,它们是Flutter有别于其他跨平台开发方案的核心所在。
|
||||
|
||||
最后,我梳理了一张Flutter学习思维导图,围绕一个应用的迭代周期介绍了Flutter相关的知识点。我希望通过这个专栏,能和你把Flutter背后的设计原理和知识体系讲清楚,让你能对Flutter有一个整体感知。这样,在你学完这个专栏以后,就能够具备企业级应用开发的理论基础与实践。
|
||||
|
||||
## 思考题
|
||||
|
||||
你是如何理解Flutter的三大特点:跨平台、高保真、高性能的?你又打算怎么学习这个专栏呢?
|
||||
|
||||
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<audio id="audio" title="05 | 从标准模板入手,体会Flutter代码是如何运行在原生系统上的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/59/83/5938890d5ed2d824e18237299f9fb283.mp3"></audio>
|
||||
|
||||
你好,我是陈航。
|
||||
|
||||
在专栏的第一篇预习文章中,我和你一起搭建了Flutter的开发环境,并且通过自带的hello_world示例,和你演示了Flutter项目是如何运行在Android和iOS模拟器以及真机上的。
|
||||
|
||||
今天,我会通过Android Studio创建的Flutter应用模板,带你去了解Flutter的项目结构,分析Flutter工程与原生Android和iOS工程有哪些联系,体验一个有着基本功能的Flutter应用是如何运转的,从而加深你对构建Flutter应用的关键概念和技术的理解。
|
||||
|
||||
如果你现在还不熟悉Dart语言也不用担心,只要能够理解基本的编程概念(比如,类型、变量、函数和面向对象),并具备一定的前端基础(比如,了解View是什么、页面基本布局等基础知识),就可以和我一起完成今天的学习。而关于Dart语言基础概念的讲述、案例分析,我会在下一个模块和你展开。
|
||||
|
||||
## 计数器示例工程分析
|
||||
|
||||
首先,我们打开Android Studio,创建一个Flutter工程应用flutter_app。Flutter会根据自带的应用模板,自动生成一个简单的计数器示例应用Demo。我们先运行此示例,效果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3a/24/3afe6b35238d1e57c8ae6bec9be61624.png" alt="">
|
||||
|
||||
每点击一次右下角带“+”号的悬浮按钮,就可以看到屏幕中央的数字随之+1。
|
||||
|
||||
### 工程结构
|
||||
|
||||
在体会了示例工程的运行效果之后,我们再来看看Flutter工程目录结构,了解Flutter工程与原生Android和iOS工程之间的关系,以及这些关系是如何确保一个Flutter程序可以最终运行在Android和iOS系统上的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e7/fc/e7ecbd5c21895e396c14154b2f226dfc.png" alt="">
|
||||
|
||||
可以看到,除了Flutter本身的代码、资源、依赖和配置之外,Flutter工程还包含了Android和iOS的工程目录。
|
||||
|
||||
这也不难理解,因为Flutter虽然是跨平台开发方案,但却需要一个容器最终运行到Android和iOS平台上,所以**Flutter工程实际上就是一个同时内嵌了Android和iOS原生子工程的父工程**:我们在lib目录下进行Flutter代码的开发,而某些特殊场景下的原生功能,则在对应的Android和iOS工程中提供相应的代码实现,供对应的Flutter代码引用。
|
||||
|
||||
Flutter会将相关的依赖和构建产物注入这两个子工程,最终集成到各自的项目中。而我们开发的Flutter代码,最终则会以原生工程的形式运行。
|
||||
|
||||
### 工程代码
|
||||
|
||||
在对Flutter的工程结构有了初步印象之后,我们就可以开始学习Flutter的项目代码了。
|
||||
|
||||
Flutter自带的应用模板,也就是这个计数器示例,对初学者来说是一个极好的入门范例。在这个简单示例中,从基础的组件、布局到手势的监听,再到状态的改变,Flutter最核心的思想在这60余行代码中展现得可谓淋漓尽致。
|
||||
|
||||
为了便于你学习理解,领会构建Flutter程序的大体思路与关键技术,而不是在一开始时就陷入组件的API细节中,我删掉了与核心流程无关的组件配置代码及布局逻辑,在不影响示例功能的情况下对代码进行了改写,并将其分为两部分:
|
||||
|
||||
- 第一部分是应用入口、应用结构以及页面结构,可以帮助你理解构建Flutter程序的基本结构和套路;
|
||||
- 第二部分则是页面布局、交互逻辑及状态管理,能够帮你理解Flutter页面是如何构建、如何响应交互,以及如何更新的。
|
||||
|
||||
首先,我们来看看**第一部分的代码**,也就是应用的整体结构:
|
||||
|
||||
```
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() => runApp(MyApp());
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(home: MyHomePage(title: 'Flutter Demo Home Page'));
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
MyHomePage({Key key, this.title}) : super(key: key);
|
||||
final String title;
|
||||
@override
|
||||
_MyHomePageState createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
Widget build(BuildContext context) => {...};
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在本例中,Flutter应用为MyApp类的一个实例,而MyApp类继承自StatelessWidget类,这也就意味着应用本身也是一个Widget。事实上,在Flutter中,Widget是整个视图描述的基础,在Flutter的世界里,包括应用、视图、视图控制器、布局等在内的概念,都建立在Widget之上,**Flutter的核心设计思想便是一切皆Widget**。
|
||||
|
||||
Widget是组件视觉效果的封装,是UI界面的载体,因此我们还需要为它提供一个方法,来告诉Flutter框架如何构建UI界面,这个方法就是build。
|
||||
|
||||
在build方法中,我们通常通过对基础Widget进行相应的UI配置,或是组合各类基础Widget的方式进行UI的定制化。比如在MyApp中,我通过MaterialApp这个Flutter App框架设置了应用首页,即MyHomePage。当然,MaterialApp也是一个Widget。
|
||||
|
||||
MaterialApp类是对构建material设计风格应用的组件封装框架,里面还有很多可配置的属性,比如应用主题、应用名称、语言标识符、组件路由等。但是,这些配置属性并不是本次分享的重点,如果你感兴趣的话,可以参考Flutter官方的[API文档](https://api.flutter.dev/flutter/material/MaterialApp/MaterialApp.html),来了解MaterialApp框架的其他配置能力。
|
||||
|
||||
MyHomePage是应用的首页,继承自StatefulWidget类。这,代表着它是一个有状态的Widget(Stateful Widget),而_MyHomePageState就是它的状态。
|
||||
|
||||
如果你足够细心的话就会发现,虽然MyHomePage类也是Widget,但与MyApp类不同的是,它并没有一个build方法去返回Widget,而是多了一个createState方法返回_MyHomePageState对象,而build方法则包含在这个_MyHomePageState类当中。
|
||||
|
||||
那么,**StatefulWidget与StatelessWidget的接口设计,为什么会有这样的区别呢?**
|
||||
|
||||
这是因为Widget需要依据数据才能完成构建,而对于StatefulWidget来说,其依赖的数据在Widget生命周期中可能会频繁地发生变化。由State创建Widget,以数据驱动视图更新,而不是直接操作UI更新视觉属性,代码表达可以更精炼,逻辑也可以更清晰。
|
||||
|
||||
在了解了计数器示例程序的整体结构以后,我们再来看看这个**示例代码的第二部分**,也就是页面布局及交互逻辑部分。
|
||||
|
||||
```
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
void _incrementCounter() => setState(() {_counter++;});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(Widget.title)),
|
||||
body: Text('You have pushed the button this many times:$_counter')),
|
||||
floatingActionButton: FloatingActionButton(onPressed: _incrementCounter)
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
_MyHomePageState中创建的Widget Scaffold,是Material库中提供的页面布局结构,它包含AppBar、Body,以及FloatingActionButton。
|
||||
|
||||
- AppBar是页面的导航栏,我们直接将MyHomePage中的title属性作为标题使用。
|
||||
- body则是一个Text组件,显示了一个根据_counter属性可变的文本:‘You have pushed the button this many times:$_counter’。
|
||||
- floatingActionButton,则是页面右下角的带“+”的悬浮按钮。我们将_incrementCounter作为其点击处理函数。
|
||||
|
||||
_incrementCounter的实现很简单,使用setState方法去自增状态属性_counter。setState方法是Flutter以数据驱动视图更新的关键函数,它会通知Flutter框架:我这儿有状态发生了改变,赶紧给我刷新界面吧。而Flutter框架收到通知后,会执行Widget的build方法,根据新的状态重新构建界面。
|
||||
|
||||
**这里需要注意的是:状态的更改一定要配合使用setState。**通过这个方法的调用,Flutter会在底层标记Widget的状态,随后触发重建。于我们的示例而言,即使你修改了_counter,如果不调用setState,Flutter框架也不会感知到状态的变化,因此界面上也不会有任何改变(你可以动手验证一下)。
|
||||
|
||||
下面的图3,就是整个计数器示例的代码流程示意图。通过这张图,你就能够把这个实例的整个代码流程串起来了:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/34/36/3401b99587eafa7c0c1ed446eb936f36.png" alt="">
|
||||
|
||||
MyApp为Flutter应用的运行实例,通过在main函数中调用runApp函数实现程序的入口。而应用的首页则为MyHomePage,一个拥有_MyHomePageState状态的StatefulWidget。_MyHomePageState通过调用build方法,以相应的数据配置完成了包括导航栏、文本及按钮的页面视图的创建。
|
||||
|
||||
而当按钮被点击之后,其关联的控件函数_incrementCounter会触发调用。在这个函数中,通过调用setState方法,更新_counter属性的同时,也会通知Flutter框架其状态发生变化。随后,Flutter会重新调用build方法,以新的数据配置重新构建_MyHomePageState的UI,最终完成页面的重新渲染。
|
||||
|
||||
Widget只是视图的“配置信息”,是数据的映射,是“只读”的。对于StatefulWidget而言,当数据改变的时候,我们需要重新创建Widget去更新界面,这也就意味着Widget的创建销毁会非常频繁。
|
||||
|
||||
为此,Flutter对这个机制做了优化,其框架内部会通过一个中间层去收敛上层UI配置对底层真实渲染的改动,从而最大程度降低对真实渲染视图的修改,提高渲染效率,而不是上层UI配置变了就需要销毁整个渲染视图树重建。
|
||||
|
||||
这样一来,Widget仅是一个轻量级的数据配置存储结构,它的重新创建速度非常快,所以我们可以放心地重新构建任何需要更新的视图,而无需分别修改各个子Widget的特定样式。关于Widget具体的渲染过程细节,我会在后续的第9篇文章“Widget,构建Flutter界面的基石”中向你详细介绍,在这里就不再展开了。
|
||||
|
||||
## 总结
|
||||
|
||||
今天的这次Flutter项目初体验,我们就先进行到这里。接下来,我们一起回顾下涉及到的知识点。
|
||||
|
||||
首先,我们通过Flutter标准模板创建了计数器示例,并分析了Flutter的项目结构,以及Flutter工程与原生Android、iOS工程的联系,知道了Flutter代码是怎么运行在原生系统上的。
|
||||
|
||||
然后,我带你学习了示例项目代码,了解了Flutter应用结构及页面结构,并认识了构建Flutter的基础,也就是Widget,以及状态管理机制,知道了Flutter页面是如何构建的,StatelessWidget与StatefulWidget的区别,以及如何通过State的成员函数setState以数据驱动的方式更新状态,从而更新页面。
|
||||
|
||||
有原生Android和iOS框架开发经验的同学,可能更习惯命令式的UI编程风格:手动创建UI组件,在需要更改UI时调用其方法修改视觉属性。而Flutter采用声明式UI设计,我们只需要描述当前的UI状态(即State)即可,不同UI状态的视觉变更由Flutter在底层完成。
|
||||
|
||||
虽然命令式的UI编程风格更直观,但声明式UI编程方式的好处是,可以让我们把复杂的视图操作细节交给框架去完成,这样一来不仅可以提高我们的效率,也可以让我们专注于整个应用和页面的结构和功能。
|
||||
|
||||
所以在这里,我非常希望你能够适应这样的UI编程思维方式的转换。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我给你留下一个思考题吧。
|
||||
|
||||
示例项目代码在_MyHomePageState类中,直接在build函数里以内联的方式完成了Scaffold页面元素的构建,这样做的好处是什么呢?
|
||||
|
||||
在实现同样功能的情况下,如果将Scaffold页面元素的构建封装成一个新Widget类,我们该如何处理?
|
||||
|
||||
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user