?
?Flutter Web 穩(wěn)定版本發(fā)布至今也有一年多了,經(jīng)過(guò)這一年多的發(fā)展,今天就讓我們來(lái)看看 Flutter Web 究竟有什么不同之處,本篇分享主要內(nèi)容是目前 Flutter 下少有較為全面的 Web 內(nèi)容。 ? ?一、起源與實(shí)現(xiàn)
?說(shuō)起 Flutter 的起源就很有意思,大家都知道早期 Flutter 最先支持的平臺(tái)是 Android 和 iOS,至今最核心的維護(hù)平臺(tái)依然是 Android 和 iOS,但是事實(shí)上 Flutter 其實(shí)起源于前端團(tuán)隊(duì)。 ??另外前端的同學(xué)應(yīng)該知道,Dart 起初也是為了 Web 而生,事實(shí)上?Dart 誕生至今也有 10 年了,所以可以說(shuō) Flutter 其實(shí)充滿了 Web 的基因。 ?但是作為從 Web 里誕生的框架,和 React Native/ Weex 不同的是,前者是先有了 Web 下的 React 和 Vue 實(shí)現(xiàn)之后才有的客戶端支持,而對(duì)于 Flutter 則是反過(guò)來(lái),先有客戶端實(shí)現(xiàn)之后才支持 Web 平臺(tái),這里其實(shí)可以和 Weex 做個(gè)簡(jiǎn)單對(duì)照。 ?Weex 作為曾經(jīng)閃耀過(guò)的跨平臺(tái)框架,它同樣支持 Android、iOS 和 Web 三個(gè)平臺(tái),在 Android 和 iOS 上 Weex 和 React Native 差異性不大,在 Web 上 Weex 則是刪減版的 Vue 支持,而由于 API 和平臺(tái)差異性的問(wèn)題,Weex 在 Web 上的支持體驗(yàn)一直不是很好:?Flutter 來(lái)源于前端 Chrome 團(tuán)隊(duì),起初 Flutter 的創(chuàng)始人和整個(gè)團(tuán)隊(duì)幾乎都是來(lái)自 Web,在 Flutter 負(fù)責(zé)人 Eric 的相關(guān)訪談中,Eric 表示 Flutter 來(lái)自 Chrome 內(nèi)部的一個(gè)實(shí)驗(yàn),他們把一些亂七八糟的 Web 規(guī)范去掉后,在一些內(nèi)部基準(zhǔn)測(cè)試的性能居然能提升 20?倍,因此 Google 內(nèi)部就開(kāi)始立項(xiàng),所以 Flutter 出現(xiàn)了。
?
?因?yàn)?Weex 需要依賴(lài)平臺(tái)控件實(shí)現(xiàn)渲染,導(dǎo)致一個(gè) Text 控件需要兼顧 Android、iOS 和 Web 上原生平臺(tái)接口的邏輯,從而出現(xiàn)各種由于耦合帶來(lái)的兼容性問(wèn)題。
而 Flutter 實(shí)現(xiàn)更為特別,通過(guò) Skia 實(shí)現(xiàn)了獨(dú)立的渲染引擎之后,在 Android 和 iOS 上控件幾乎就與平臺(tái)無(wú)關(guān),所以 Flutter 上的控件可以做到獨(dú)立且不同平臺(tái)上渲染一致的效果。
但是回到 Web 上又有些特殊,首先 Web 平臺(tái)完全是 html / js / css 的天下,并且 Web 平臺(tái)需要同時(shí)兼顧 PC 和 Mobile 的不同環(huán)境,這就讓 Flutter Web 成了 Flutter 所有平臺(tái)里 "最另類(lèi)又奇葩" 的落地。
?
首先 Flutter Web 和其他 Flutter 平臺(tái)一樣共用一套 Framework,理論上絕大多數(shù)的控件實(shí)現(xiàn)都是通用的,當(dāng)然如果要說(shuō)最不兼容的 API 對(duì)象,那肯定就是 Canvas 了,這其實(shí)和 Flutter Web 特殊的實(shí)現(xiàn)有關(guān)系,后面我們會(huì)聊到這個(gè)問(wèn)題。
?
而由于 Web 的特殊場(chǎng)景,Flutter Web 在 "幾經(jīng)周折" 之后落地了兩種不同的渲染邏輯:?html 和 canvaskit,它們的不同之處在于:?
- html
-
好處:?html 的實(shí)現(xiàn)更輕量級(jí),渲染實(shí)現(xiàn)基本依賴(lài)于 Web 平臺(tái)的各種 HTMLElement,特別是 Flutter Web 下定義的各種
實(shí)現(xiàn),可以說(shuō)它更貼近現(xiàn)在的 Web 環(huán)境,所以有時(shí)候我們也稱(chēng)呼它為 DomCanvas,當(dāng)然隨著 Flutter Web 的發(fā)展這個(gè)稱(chēng)呼也發(fā)生了一些變化,后續(xù)我們會(huì)詳細(xì)講到這個(gè)。 - 問(wèn)題: html 的問(wèn)題也在于太過(guò)于貼近 Web 平臺(tái),這就和 Weex 一樣,貼近平臺(tái)也就是耦合于平臺(tái),事實(shí)上 DomCanvas 實(shí)現(xiàn)理念其實(shí)和 Flutter 并不貼切,也導(dǎo)致了 Flutter Web 的一些渲染效果在 html 模式下存在兼容問(wèn)題,特別是?Canvas 的 API。 ?
- canvaskit
- 好處: canvaskit 的實(shí)現(xiàn)可以說(shuō)是更貼近 Flutter 理念,因?yàn)樗鋵?shí)就是 Skia + WebAssembly 的實(shí)現(xiàn)邏輯,能和其他平臺(tái)的實(shí)現(xiàn)更一致,性能更好,比如滾動(dòng)列表的渲染流暢度更高等。
- 問(wèn)題: 很明顯使用 WebAssembly 帶來(lái)的 wasm 文件會(huì)導(dǎo)致體積增大不少,Web 場(chǎng)景下其實(shí)很講究加載速度,而在這方面 wasm 能優(yōu)化的空間很小,并且 WebAssembly 在兼容上也是相對(duì)較差,另外 skia 還需要自帶字體庫(kù)等問(wèn)題都挺讓人頭痛。 ?
默認(rèn)情況下 Flutter Web 在打包渲染時(shí)會(huì)把 html 和 canvaskit 都打包進(jìn)去,然后在 PC 端使用 canvaskit 模式,在 mobile 端使用 html 模式,當(dāng)然您也可以在打包時(shí)通過(guò) flutter build web --web-renderer html --release 之類(lèi)的配置強(qiáng)行指定渲染模式。
?
既然這里我們講到了 Flutter Web 的打包構(gòu)建,那就讓我們先從構(gòu)建打包角度開(kāi)始來(lái)深入介紹 Flutter Web。 ? ?二、構(gòu)建和優(yōu)化
?Flutter Web 雖說(shuō)是和其他平臺(tái)共用一個(gè) framework,但是它在 dart 層開(kāi)始就有一套自己特殊的 engine 實(shí)現(xiàn),并且這套實(shí)現(xiàn)是獨(dú)立于 framework 的一套特殊代碼。
?
所以在 Flutter Web 打包時(shí),會(huì)把默認(rèn)的? /flutter/bin/cache/lib/_engine 變成了 flutter/bin/cache/flutter_web_sdk/lib/_engine 的相關(guān)實(shí)現(xiàn),這是因?yàn)?Flutter Web 在 framework 之下的 engine 需要一套特殊的 API。
?
下圖右側(cè)構(gòu)建是指定 web 的打包路徑,和左邊默認(rèn)時(shí)的對(duì)比。
?
同樣下圖所示,可以看到 web sdk 里會(huì)有如 html、canvaskit 這樣不同的實(shí)現(xiàn),甚至?xí)幸粋€(gè)特殊的 text 目錄,這是因?yàn)樵?web 上對(duì)于文本的支持是個(gè)十分復(fù)雜的問(wèn)題。
那到這里我們知道了在?_engine 層面,F(xiàn)lutter Web 有著自己一套獨(dú)立的實(shí)現(xiàn),那構(gòu)建之后的產(chǎn)物是什么樣的情況呢?
?
如下圖所示是 GSY 的一個(gè)簡(jiǎn)單的開(kāi)源示例項(xiàng)目,在部署到服務(wù)器后可以看到,默認(rèn)情況下在不做任何處理時(shí),在 PC 端打開(kāi)后會(huì)使用 canvaskit 渲染,主要會(huì)有:? ?
可以看到這些文件占據(jù)了 Flutter Web 編譯后產(chǎn)物的大部分體積,并且從大小上看確實(shí)讓人有些無(wú)法接受,因?yàn)槭纠?xiàng)目的代碼量并不大,結(jié)構(gòu)也不復(fù)雜,這樣的體積肯定十分影響加載速度。
?
所以我們首先考慮在 html 和 canvaskit 兩種渲染模式中先選定一種,出于實(shí)用性考慮,結(jié)合前面的對(duì)比情況,選用 html 渲染模式在兼容性和可優(yōu)化上會(huì)更友好,所以這里優(yōu)化的第一步就是先指定 html 模式作為渲染引擎。
?
開(kāi)始優(yōu)化首先可以看到 CupertinoIcons.ttf 這個(gè)矢量圖標(biāo)文件,雖然默認(rèn)創(chuàng)建項(xiàng)目時(shí)會(huì)通過(guò) cupertino_icons 被添加到項(xiàng)目里,但是由于我們不需要使用,所以可以在 yaml 文件里去除。
?
之后通過(guò)運(yùn)行 flutter build web --release --web-renderer html 后,可以看到使用 html 模式加載后的產(chǎn)物很干凈,而需要優(yōu)化的體積現(xiàn)在主要在 main.dart.js 和 MaterialIcons-Regular.otf 上。
?
?
雖然在項(xiàng)目中我們會(huì)使用到 MaterialIcons 的一些矢量圖標(biāo),但是每次加載都要全量加載一個(gè) 1.5 MB 的字體庫(kù)文件顯然并不符合邏輯,所以在 Flutter 里官方提供了 --tree-shake-icons 的命令幫助我們優(yōu)化這部分的內(nèi)容。
?
但是不幸的是,如下圖所示,在當(dāng)前的 2.10 版本下該配置運(yùn)行會(huì)有 bug,而不幸中的萬(wàn)幸是,在原生平臺(tái)的編譯中 shake-icons 行為是可以正常執(zhí)行。
?
?
所以我們可以先運(yùn)行 flutter build apk,然后通過(guò)如下命令,將 Android 上已經(jīng) shake-icons 的 MaterialIcons-Regular.otf 資源復(fù)制到已經(jīng)編譯好的 web/ 目錄下。
- ?
cp?-r?./build/app/intermediates/flutter/release/flutter_assets/?./build/web/assets
?再次打包后可以看到,經(jīng)過(guò)優(yōu)化后 MaterialIcons-Regular.otf 資源如今只剩下 3.2 kB,那接下來(lái)就是考慮針對(duì) 2.2 MB 的 main.dart.js 進(jìn)行優(yōu)化處理。
?
?
要優(yōu)化 main.dart.js,我們就要講到 Flutter 里的?deferred-components,在 Flutter 里可以通過(guò)把控件定義為 "deferred component"?來(lái)實(shí)現(xiàn)控件的懶加載,而這個(gè)行為在 Flutter Web 上被編譯之后就會(huì)變成多個(gè) *part.js 文本,原理上就是對(duì) main.dart.js 進(jìn)行拆包。
?
舉個(gè)例子,首先我們定義一個(gè)普通的 Flutter 控件,按照正常的控件進(jìn)行實(shí)現(xiàn)就可以。
?import 'package:flutter/widgets.dart';
class DeferredBox extends StatelessWidget {
DeferredBox() {}
Widget build(BuildContext context) {
return Container(
height: 30,
width: 30,
color: Colors.blue,
);
}
}
在需要的地方 import 對(duì)應(yīng)控件然后添加 deferred as box 關(guān)鍵字,之后在適當(dāng)時(shí)機(jī)通過(guò) box.loadLibrary() 加載控件,最后通過(guò) box.DeferredBox() 渲染。
?import 'box.dart' deferred as box;
class MainPage extends StatefulWidget {
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
void initState() {
super.initState();
}
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: box.loadLibrary(),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return box.DeferredBox();
}
return CircularProgressIndicator();
},
);
}
}
當(dāng)然,這里還需要額外在 ymal 文件里添加 deferred-components 來(lái)制定對(duì)應(yīng)的 libraries 路徑。
?deferred-components:
- name: crane
libraries:
- package:gsy_flutter_demo/widget/box.dart
回歸到上面的 GSY 示例項(xiàng)目中,通過(guò)相對(duì)極端的分包實(shí)現(xiàn),這里把 GSY 示例里的每個(gè)頁(yè)面都變成一個(gè)獨(dú)立的懶加載頁(yè)面,然后在頁(yè)面跳轉(zhuǎn)時(shí)再加載顯示,最終打包部署后如下圖所示:?
?
?
可以看到拆分之后 main.dart.js 從 2.2 MB 變成了 1.6 MB,而其他內(nèi)容通過(guò) deferred components 變成了各個(gè) part.js 的獨(dú)立文件,并且只在點(diǎn)擊時(shí)才動(dòng)態(tài)下載對(duì)應(yīng)的 part.js 文件,但是此時(shí)的 main.dart.js 依舊不小,而官方提供的能力上已經(jīng)沒(méi)有太多優(yōu)化的余地。
?
在這里可以通過(guò)前端的 source-map-explorer 工具去分析這個(gè)文件,首先在編譯時(shí)要添加 --source-maps 命令,這樣在打包時(shí)會(huì)生成 main.dart.js 的 source map 文件,然后就執(zhí)行 source-map-explorer main.dart.js --no-border-checks ?生成對(duì)應(yīng)的分析圖:?
?
?
這里只展示能夠被 mapped 的部分,可以看到 700k 幾乎就是 Flutter Web 整個(gè) framewok + engine + vm 的大小,而這部分內(nèi)容其實(shí)可以優(yōu)化的空間并不大,盡管會(huì)有一些如 kIsWeb 的冗余代碼,但是其實(shí)可以調(diào)整的內(nèi)容并不多,大概有 36 處可以調(diào)整和刪減的地方,實(shí)質(zhì)上打包時(shí) Flutter Web 也都有相應(yīng)的優(yōu)化壓縮處理,所以這部分收益并不高。
?
?
另外,如下圖所示是兩種不同 web rendder 構(gòu)建后代碼上的差異,可以看到 html 和 canvaskit 單獨(dú)構(gòu)建后的 engine 代碼結(jié)構(gòu)差異性還是很大的。
?
而如果您在編譯時(shí)默認(rèn)的 auto 模式,就會(huì)看到 html 和 canvaskit 的代碼都會(huì)打包進(jìn)去,所以相對(duì)的 main.dart.js 也會(huì)增加一些。
?
?
那還有什么可以優(yōu)化的地方嗎?還是有的,通過(guò)外部手段,例如通過(guò)在部署時(shí)開(kāi)啟 gzip 或者 brotli 壓縮,如下圖所示,開(kāi)始 gzip 后大概可以讓 main.dart.js 下降到 400k 左右。
?
?
另外也有在 index.html 里增加 loading 效果來(lái)做等待加載過(guò)程的展示,例如:?
?所以大致上以上這些就是今天關(guān)于 Flutter Web 上產(chǎn)物體積的優(yōu)化,總結(jié)起來(lái)就是:?<html>
<head>
<meta charset="UTF-8">
<title>gsy_flutter_demotitle>
<style>
.loading {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
position: absolute;
top: ;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.loader {
border: 16px solid #f3f3f3;
border-radius: 50%;
border: 15px solid ;
border-top: 16px solid blue;
border-right: 16px solid white;
border-bottom: 16px solid blue;
border-left: 16px solid white;
width: 120px;
height: 120px;
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
style>
head>
<body>
<div class="loading">
<div class="loader">div>
div>
<script src="main.dart.js" type="application/javascript">script>
body>
html>
-
去除無(wú)用的 icon 引用;
-
使用 tree-shake-icons 優(yōu)化引用矢量圖庫(kù);
-
通過(guò) deferred-components 實(shí)現(xiàn)懶加載分包;
-
開(kāi)啟 gzip 等壓縮算法壓縮?main.dart.js。
?
?
三、渲染
?
講完構(gòu)建,最后我們聊聊渲染,F(xiàn)lutter Web 的渲染在 Flutter 里是十分特殊的,前面我們說(shuō)過(guò)它自帶了兩種渲染模式,而我們知道 Flutter 的設(shè)計(jì)理念里,所有的控件都是通過(guò) Engine 繪制出來(lái)的,如果這時(shí)候您去 framework 里看 Canvas 的實(shí)現(xiàn),就會(huì)發(fā)現(xiàn)它其實(shí)繼承的是?NativeFieldWrapperClass1:?
?
?
NativeFieldWrapperClass1 也就是它的邏輯是由不同平臺(tái)的 Engine 區(qū)分實(shí)現(xiàn),其中編譯后的 Flutter Web 上的?Canvas 代碼應(yīng)該是繼承如下所示的結(jié)構(gòu):?
?
?
可以看到在 Flutter Web 的 Canvas 里會(huì)根據(jù)邏輯判斷是使用 CanvasKitCanvas 還是 SurfaceCanvas,而相對(duì)于直接使用 skia 的 CanvasKitCanvas,更貼近 Web 平臺(tái)的 SurfaceCanvas 在實(shí)現(xiàn)的耦合復(fù)雜度上會(huì)更高。
?
首先如下圖所示是 Flutter Web 里 Canvas 的大致結(jié)構(gòu),而接下來(lái)我們要聊的主要也是集中在 SurfaceCanvas 上,為什么 SurfaceCanvas 層級(jí)會(huì)這么復(fù)雜,它們又是怎么分配繪制,接下來(lái)就讓我們深入揭秘它們的規(guī)則。
?
?
先看例子,如下圖所示,可以看到在 html 渲染模式下,F(xiàn)lutter Web 是有一大堆自定義的
?
?
如果這時(shí)候我們放慢去看細(xì)節(jié),如下動(dòng)圖所示,可以看到當(dāng) item 處于不可見(jiàn)時(shí)
?
?
看到一個(gè)重點(diǎn)沒(méi)有?在這里的文本為什么是由? 標(biāo)簽繪制而不是 標(biāo)簽之類(lèi)的呢?這就是我們重點(diǎn)要講的 SurfaceCanvas 渲染邏輯。
?在 Flutter Web 的?SurfaceCanvas 里,文本繪制一般都會(huì)是以這樣的情況出現(xiàn),基本都是從 picture 開(kāi)始進(jìn)入繪制流程:??
?
那么在對(duì)應(yīng)的 picture.dart 的代碼實(shí)現(xiàn)里可以看到,如下關(guān)鍵代碼所示,當(dāng) hasArbitraryPaint 為 true 時(shí)就會(huì)進(jìn)入到 BitmapCanvas 的邏輯,不然就會(huì)使用 DomCanvas。
?那么這里有兩個(gè)問(wèn)題:?BitmapCanvas 和?DomCanvas 的區(qū)別是什么?hasArbitraryPaint 的判斷邏輯是什么?void applyPaint(EngineCanvas? oldCanvas) {
if (picture.recordingCanvas!.renderStrategy.hasArbitraryPaint) {
_applyBitmapPaint(oldCanvas);
} else {
_applyDomPaint(oldCanvas);
}
}
-
首先 BitmapCanvas 和?DomCanvas 的最大的區(qū)別就是:
-
DomCanvas 會(huì)通過(guò)創(chuàng)建標(biāo)簽來(lái)實(shí)現(xiàn)繪制,比如文本利用 p + span 標(biāo)簽進(jìn)行渲染;
-
BitmapCanvas 會(huì)考慮優(yōu)先使用 canvas 渲染,如果場(chǎng)景需要再使用標(biāo)簽來(lái)實(shí)現(xiàn)繪制。
?
-
在 web sdk 里 hasArbitraryPaint 參數(shù)默認(rèn)是 false,但是在需要執(zhí)行以下這些行為時(shí)就會(huì)被設(shè)置為 true,而這些調(diào)用上可以看出,其實(shí)大部分時(shí)候的繪制邏輯是會(huì)先進(jìn)入到?BitmapCanvas 里。
?
?
回到前面的文本問(wèn)題上,在 Flutter 的文本繪制一般都是通過(guò)?drawParagraph 實(shí)現(xiàn),所以理論上只要有文本存在,就會(huì)進(jìn)入到 BitmapCanvas 的繪制流程,那么目前看來(lái)這個(gè)結(jié)論符合上面 Item 里文本是使用 canvas 繪制的預(yù)期。
?
那 Flutter 里對(duì)于文本,在?BitmapCanvas 又是何時(shí)使用 canvas 何時(shí)使用 p+span 標(biāo)簽?zāi)?/span>?
?
我們先看如下代碼,運(yùn)行后效果如下圖所示,可以看到此時(shí)的文本是直接使用 canvas 渲染的,這個(gè)結(jié)果符合我們目前的預(yù)期。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
child: Text( "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
接下來(lái)給這段代碼加上一個(gè)紅色背景,運(yùn)行后可以看到,此時(shí)的文本變成了 p+span 標(biāo)簽,并且紅色的背景是通過(guò) draw-rect 標(biāo)簽實(shí)現(xiàn),層級(jí)里并沒(méi)有 canvas,這又是為什么呢?
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.red,
),
child: Text( "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
這里就需要先講到 BitmapCanvas 的 drawRect 實(shí)現(xiàn),如下關(guān)鍵代碼所示,在 drawRect 時(shí),如果在滿足 _useDomForRenderingFillAndStroke 這個(gè)函數(shù)條件的情況下,就會(huì)通過(guò) buildDrawRectElement 的方式實(shí)現(xiàn)渲染,也就是使用 draw-rect 標(biāo)簽而不是 canvas,所以我們需要先分析這個(gè)函數(shù)的判斷邏輯。
?@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
if (_useDomForRenderingFillAndStroke(paint)) {
final html.HtmlElement element = buildDrawRectElement(
rect, paint, 'draw-rect', _canvasPool.currentTransform);
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);
} else {
setUpPaint(paint, rect);
_canvasPool.drawRect(rect, paint.style);
tearDownPaint();
}
}
如下代碼所示,可以看到這個(gè)函數(shù)有很多的判斷條件,而得到 true 的條件就是滿足其中三大條件之一即可,下述表格里大致描述了每個(gè)條件所代表的意義。
?bool _useDomForRenderingFillAndStroke(SurfacePaintData paint) =>
_renderStrategy.isInsideSvgFilterTree ||
(_preserveImageData == false && _contains3dTransform) ||
((_childOverdraw ||
_renderStrategy.hasImageElements ||
_renderStrategy.hasParagraphs) &&
_canvasPool.isEmpty &&
paint.maskFilter == null &&
paint.shader == null);
isInsideSvgFilterTree |
例如有 ShaderMask 或者 ColorFilter 的時(shí)候?yàn)?true |
---|---|
_preserveImageData |
一般是在 toImage 的時(shí)候才會(huì)為?true |
_contains3dTransform | transformKind == TransformKind.complex 的時(shí)候,也就是矩陣包含縮放、旋轉(zhuǎn)、z 平移或透視變換 |
_childOverdraw | 有 _drawElement 或者 drawImage 的時(shí)候,大概就是使用了標(biāo)簽渲染之后,需要切換畫(huà)布 |
_renderStrategy.hasImageElements | 有圖片繪制的時(shí)候,用 Image 標(biāo)簽的情況 |
_renderStrategy.hasParagraphs | 有文本需要繪制的時(shí)候 |
_canvasPool.isEmpty | 簡(jiǎn)單說(shuō)就是 canvas == null 的時(shí)候 |
paint.maskFilter == null | 簡(jiǎn)單說(shuō)就是 Container 等控件沒(méi)有配置 shadow 的時(shí)候 |
paint.shader == null | 簡(jiǎn)單說(shuō)就是 Container 等控件沒(méi)有配置 gradient 的時(shí)候 |
大概流程也如圖所示,前面繪制紅色背景時(shí)并沒(méi)有添加什么特殊配置,所以會(huì)進(jìn)入到 _drawElement 的邏輯,可以看到針對(duì)不同的渲染場(chǎng)景,BitmapCanvas 會(huì)采取不一樣的繪制邏輯,那為什么前面多了紅色背景就會(huì)導(dǎo)致文本也變成標(biāo)簽?zāi)兀?/span>
?
?
這是因?yàn)樵?BitmapCanvas 如果有使用標(biāo)簽構(gòu)建,也就是?_drawElement 的時(shí)候,就會(huì)執(zhí)行一個(gè) _closeCurrentCanvas 函數(shù),該函數(shù)會(huì)把 _childOverdraw 設(shè)置為 true,并且清空 _canvasPool 里的 canvas。
?
所以我們看 drawParagraph 的實(shí)現(xiàn),如下所示代碼,可以看到由于 _childOverdraw 是 true 時(shí),文本會(huì)采用 Element 來(lái)繪制文本。
?而在?BitmapCanvas 里,有三個(gè)操作會(huì)觸發(fā) _childOverdraw = true 和 _canvasPool Empty:?void drawParagraph(EngineParagraph paragraph, ui.Offset offset) {
····
if (paragraph.drawOnCanvas && _childOverdraw == false &&
!_renderStrategy.isInsideSvgFilterTree) {
paragraph.paint(this, offset);
return;
}
····
final html.Element paragraphElement =
drawParagraphElement(paragraph, offset);
····
}
-
_drawElement
-
drawImage/drawImageRect
-
drawParagraph
?
所以先總結(jié)一下,結(jié)合前面的流程圖,我們可以簡(jiǎn)單認(rèn)為:?在沒(méi)有 maskFilter (shadow) 和 shader (gradient) 的情況下,只要觸發(fā)了上述三種情況,就會(huì)使用標(biāo)簽繪制。
?
是不是感覺(jué)有點(diǎn)亂?
?
不怕,先接著繼續(xù)看新的例子,在原本紅色背景實(shí)現(xiàn)的基礎(chǔ)上,這里給 Container 增加了 shadow 用于配置陰影,運(yùn)行之后可以看到,不管是背景色或者文本又都變成了 canvas 渲染的情況。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
結(jié)合前面的流程看這是符合預(yù)期的,因?yàn)榇藭r(shí)帶有 boxShadow 參數(shù),該參數(shù)會(huì)在繪制時(shí)通過(guò) toPaint 方法轉(zhuǎn)化為 maskFilter,所以在 maskFilter != null 的情況下,流程不會(huì)進(jìn)入到 Element 的判斷,所以使用 canvas。
?
?
繼續(xù)前面的例子,如果這時(shí)候我們?cè)偌右粋€(gè) ColorFiltered 控件,前面表格說(shuō)過(guò),有 ShaderMask 或者 ColorFilter 的時(shí)候,sInsideSvgFilterTree 參數(shù)就會(huì)是 true,這時(shí)候渲染就會(huì)直接進(jìn)入使用 Element 繪制而無(wú)視其他條件如 BoxShadow,從運(yùn)行結(jié)果上看也是如此。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: ColorFiltered(
colorFilter: ColorFilter.mode(Colors.yellow, BlendMode.hue),
child:Container(
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
),
)
可以看到此時(shí)變成了兩個(gè) draw-rect 和 p 標(biāo)簽的繪制,為什么會(huì)有這樣的邏輯,因?yàn)橐恍g覽器,例如 iOS 設(shè)備上的 Safari,它不會(huì)把 svg filter 等信息傳遞給 canvas,如果繼續(xù)使用 canvas 就會(huì)如 shader mask 等無(wú)法正常渲染,詳細(xì)可見(jiàn):?#27600。
?
?
繼續(xù)這個(gè)例子,如果此時(shí)不加 ColorFiltered,而是給 Container 添加一個(gè) transform,運(yùn)行后可以看到還是 draw-rect 和 p?標(biāo)簽的實(shí)現(xiàn),因?yàn)榇藭r(shí)的 transform 是屬于 TransformKind.complex 的狀態(tài),會(huì)導(dǎo)致 _contains3dTransform = true,從而進(jìn)入 Element 的邏輯。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
transform: Matrix4.identity()..setEntry(3, 2, 0.001) ..rotateX(100)..rotateY(100),
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
最后再來(lái)一個(gè)例子,這里回歸到只有紅色背景和陰影的情況,在之前它運(yùn)行后是使用 canvas 標(biāo)簽來(lái)渲染文本,因?yàn)樗?maskFilter != null,但是這時(shí)候我們給 Text 配置上 TextDecoratoin,運(yùn)行之后可以看到背景顏色依然是 canvas,但是文本又變成了 p 標(biāo)簽的實(shí)現(xiàn)。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
style: TextStyle(decoration: TextDecoration.lineThrough),
),
),
),
),
);
這是因?yàn)榍懊嬲f(shuō)過(guò)?drawParagraph,在這個(gè)函數(shù)里有另外一個(gè)判斷條件 _drawOnCanvas,在 Flutter Web 繪制文本時(shí),當(dāng)文本具備不為 none 的 TextDecoration 或者?fontFeatures 時(shí),_drawOnCanvas 就會(huì)被設(shè)置為 fasle,從而變成使用 p 標(biāo)簽渲染的情況。
?這也很好理解,例如?fontFeatures?是影響字形選擇的參數(shù),如下圖所示,這些行為在 Web 上用 Canvas 繪制相對(duì)會(huì)麻煩很多。
?
?
前面講了那么多例子都是 BitmapCanvas,那 Domcanvas ?什么時(shí)候會(huì)用到呢?
?
還記得前面列舉的方法嗎,需要進(jìn)入? _applyDomPaint 就需要 hasArbitraryPaint == false,換言之就是沒(méi)有文本,然后 drawRect 的時(shí)候沒(méi)有 shader (radient) 等就可以了。
?
依然是前面的例子,繪制一個(gè)帶有陰影的紅色方框,但是此時(shí)把文本內(nèi)容去掉,運(yùn)行后可以看到不是 canvas 而是 draw-rect 標(biāo)簽,因?yàn)殡m然此時(shí) maskFilter != null (有 shadow),但是因?yàn)闆](méi)有文本或者 shader (gradient),所以單純普通的 drawRect 并不會(huì)觸發(fā)?hasArbitraryPaint == true,所以會(huì)直接使用 Domcanvas 繪制,完全脫離了 canvas 的渲染。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
height: 50,
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
),
),
),
)
所以最后總結(jié)一下: 首先除了下圖所示之外的情況,大部分時(shí)候 Flutter Web 繪制都會(huì)進(jìn)入到 BitmapCanvas。
?
?
結(jié)合前面介紹的例子,進(jìn)入到 BitmapCanvas 之后的流程可以總結(jié):?-
存在 ShaderMask 或者 ColorFilter 就會(huì)使用 Element;
-
一般情況忽略?_preserveImageData,有復(fù)雜矩陣變換時(shí)也是直接使用 Element,因?yàn)閺?fù)雜矩陣變換 canvas 支持并不好;
-
_childOverdraw 經(jīng)常和 _canvasPool.isEmpty 一起達(dá)成條件,一般有 picture 上有 _drawElement 之后就會(huì)調(diào)用 _closeCurrentCanvas 設(shè)置? _childOverdraw = true 并且清空 _canvasPool;
-
結(jié)合上述第三個(gè)條件的狀態(tài),如果沒(méi)有 maskFilter 或者 shader,就會(huì)使用 Element 渲染 UI。
?
最后針對(duì)文本,在 drawParagraph 時(shí)還有特殊處理,關(guān)于 _childOverdraw 和 !isInsideSvgFilterTree 相關(guān)前面解釋過(guò)了,新增條件是在有 TextDecoration 或者 FontFeatures 時(shí),也會(huì)觸發(fā)文本繪制變?yōu)?Element,也就是 p + span 標(biāo)簽的形式。
?
?
?
四、最后
?
雖然本次介紹的東西不少,但是 Flutter Web 在 html 渲染模式下的知識(shí)點(diǎn)遠(yuǎn)不止這些,而由小窺大,以 drawRect 和文本為切入點(diǎn)去了解 SurfaceCanvas 就是很不錯(cuò)的開(kāi)始。
?
另外可以看到,在 Flutter Web 里有很多的自定義的
?
?
? ? 審核編輯 :李倩?
評(píng)論