在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線(xiàn)課程
  • 觀(guān)看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Flutter Web有什么不同之處

谷歌開(kāi)發(fā)者 ? 來(lái)源:GSYTech ? 作者:戀貓de小郭 ? 2022-07-08 09:51 ? 次閱讀

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)是 AndroidiOS,至今最核心的維護(hù)平臺(tái)依然是 Android 和 iOS,但是事實(shí)上 Flutter 其實(shí)起源于前端團(tuán)隊(duì)。

Flutter 來(lái)源于前端 Chrome 團(tuán)隊(duì),起初 Flutter 的創(chuàng)始人和整個(gè)團(tuán)隊(duì)幾乎都是來(lái)自 Web,在 Flutter 負(fù)責(zé)人 Eric 的相關(guān)訪(fǎng)談中,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)了。

另外前端的同學(xué)應(yīng)該知道,Dart 起初也是為了 Web 而生,事實(shí)上 Dart 誕生至今也有 10 年了,所以可以說(shuō) Flutter 其實(shí)充滿(mǎn)了 Web 的基因。

但是作為從 Web 里誕生的框架,和 React Native/ Weex 不同的是,前者是先有了 Web 下的 React 和 Vue 實(shí)現(xiàn)之后才有的客戶(hù)端支持,而對(duì)于 Flutter 則是反過(guò)來(lái),先有客戶(hù)端實(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)一直不是很好:

因?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)上渲染一致的效果。

d62c4d6e-fe5e-11ec-ba43-dac502259ad0.png

但是回到 Web 上又有些特殊,首先 Web 平臺(tái)完全是 html / js / css 的天下,并且 Web 平臺(tái)需要同時(shí)兼顧 PC 和 Mobile 的不同環(huán)境,這就讓 Flutter Web 成了 Flutter 所有平臺(tái)里 “最另類(lèi)又奇葩” 的落地。

d63caf38-fe5e-11ec-ba43-dac502259ad0.png

首先 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)景,F(xiàn)lutter Web 在 “幾經(jīng)周折” 之后落地了兩種不同的渲染邏輯: html 和 canvaskit,它們的不同之處在于:

html

好處: html 的實(shí)現(xiàn)更輕量級(jí),渲染實(shí)現(xiàn)基本依賴(lài)于 Web 平臺(tái)的各種 HTMLElement,特別是 Flutter Web 下定義的各種 《flt-*》 實(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ì)比。

d653a300-fe5e-11ec-ba43-dac502259ad0.png

同樣下圖所示,可以看到 web sdk 里會(huì)有如 html、canvaskit 這樣不同的實(shí)現(xiàn),甚至?xí)幸粋€(gè)特殊的 text 目錄,這是因?yàn)樵?web 上對(duì)于文本的支持是個(gè)十分復(fù)雜的問(wèn)題。

d65eed1e-fe5e-11ec-ba43-dac502259ad0.png

那到這里我們知道了在 _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ì)有:

2.3 MB 的 main.dart.js;

2.8 MB 的 canvaskit.wasm;

1.5 MB 的 MaterialIcons-Regular.otf;

284 kB 的 CupertinoIcons.ttf

d6841cc4-fe5e-11ec-ba43-dac502259ad0.png

可以看到這些文件占據(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 上。

d6939550-fe5e-11ec-ba43-dac502259ad0.png

雖然在項(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í)行。

d6a67df0-fe5e-11ec-ba43-dac502259ad0.png

所以我們可以先運(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)化處理。

d6af327e-fe5e-11ec-ba43-dac502259ad0.png

要優(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() {} @override 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 { @override _MainPageState createState() =》 _MainPageState();}class _MainPageState extends State《MainPage》 { @override void initState() { super.initState(); } @override 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í)再加載顯示,最終打包部署后如下圖所示:

d6bc08b4-fe5e-11ec-ba43-dac502259ad0.png

可以看到拆分之后 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)的分析圖:

d6cf05d6-fe5e-11ec-ba43-dac502259ad0.png

這里只展示能夠被 mapped 的部分,可以看到 700k 幾乎就是 Flutter Web 整個(gè) framewok + engine + vm 的大小,而這部分內(nèi)容其實(shí)可以?xún)?yōu)化的空間并不大,盡管會(huì)有一些如 kIsWeb 的冗余代碼,但是其實(shí)可以調(diào)整的內(nèi)容并不多,大概有 36 處可以調(diào)整和刪減的地方,實(shí)質(zhì)上打包時(shí) Flutter Web 也都有相應(yīng)的優(yōu)化壓縮處理,所以這部分收益并不高。

d6e2d9ee-fe5e-11ec-ba43-dac502259ad0.png

另外,如下圖所示是兩種不同 web rendder 構(gòu)建后代碼上的差異,可以看到 html 和 canvaskit 單獨(dú)構(gòu)建后的 engine 代碼結(jié)構(gòu)差異性還是很大的。

d6f0defe-fe5e-11ec-ba43-dac502259ad0.png

而如果您在編譯時(shí)默認(rèn)的 auto 模式,就會(huì)看到 html 和 canvaskit 的代碼都會(huì)打包進(jìn)去,所以相對(duì)的 main.dart.js 也會(huì)增加一些。

d700aa46-fe5e-11ec-ba43-dac502259ad0.png

那還有什么可以?xún)?yōu)化的地方嗎?還是有的,通過(guò)外部手段,例如通過(guò)在部署時(shí)開(kāi)啟 gzip 或者 brotli 壓縮,如下圖所示,開(kāi)始 gzip 后大概可以讓 main.dart.js 下降到 400k 左右。

d713c2a2-fe5e-11ec-ba43-dac502259ad0.png

另外也有在 index.html 里增加 loading 效果來(lái)做等待加載過(guò)程的展示,例如:

《!DOCTYPE html》《html》《head》 《meta charset=“UTF-8”》 《title》gsy_flutter_demo《/title》 《style》 .loading { display: flex; justify-content: center; align-items: center; margin: 0; position: absolute; top: 50%; 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=“l(fā)oading”》 《div class=“l(fā)oader”》《/div》 《/div》 《script src=“main.dart.js” type=“application/javascript”》《/script》《/body》《/html》

所以大致上以上這些就是今天關(guān)于 Flutter Web 上產(chǎn)物體積的優(yōu)化,總結(jié)起來(lái)就是:

去除無(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:

d71ed1f6-fe5e-11ec-ba43-dac502259ad0.png

NativeFieldWrapperClass1 也就是它的邏輯是由不同平臺(tái)的 Engine 區(qū)分實(shí)現(xiàn),其中編譯后的 Flutter Web 上的 Canvas 代碼應(yīng)該是繼承如下所示的結(jié)構(gòu):

d73d47e4-fe5e-11ec-ba43-dac502259ad0.png

可以看到在 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ī)則。

d753e698-fe5e-11ec-ba43-dac502259ad0.png

先看例子,如下圖所示,可以看到在 html 渲染模式下,F(xiàn)lutter Web 是有一大堆自定義的 《flt-*》 標(biāo)簽實(shí)現(xiàn)渲染,并且在一個(gè)長(zhǎng)列表中,標(biāo)簽會(huì)被控制在一個(gè)合適的數(shù)量,在滾動(dòng)時(shí)進(jìn)行動(dòng)態(tài)切換渲染。

d75f1d60-fe5e-11ec-ba43-dac502259ad0.jpg

如果這時(shí)候我們放慢去看細(xì)節(jié),如下動(dòng)圖所示,可以看到當(dāng) item 處于不可見(jiàn)時(shí) 《flt-picture》 里其實(shí)并沒(méi)有內(nèi)容,而當(dāng) Item 可見(jiàn)之后,《flt-picture》 下會(huì)有 《canvas》 標(biāo)簽把文字繪制出來(lái)。

d77223f6-fe5e-11ec-ba43-dac502259ad0.gif

看到一個(gè)重點(diǎn)沒(méi)有?在這里的文本為什么是由 《canvas》 標(biāo)簽繪制而不是 《p》 標(biāo)簽之類(lèi)的呢?這就是我們重點(diǎn)要講的 SurfaceCanvas 渲染邏輯。

在 Flutter Web 的 SurfaceCanvas 里,文本繪制一般都會(huì)是以這樣的情況出現(xiàn),基本都是從 picture 開(kāi)始進(jìn)入繪制流程:

d7cb13bc-fe5e-11ec-ba43-dac502259ad0.png

那么在對(duì)應(yīng)的 picture.dart 的代碼實(shí)現(xiàn)里可以看到,如下關(guān)鍵代碼所示,當(dāng) hasArbitraryPaint 為 true 時(shí)就會(huì)進(jìn)入到 BitmapCanvas 的邏輯,不然就會(huì)使用 DomCanvas。

void applyPaint(EngineCanvas? oldCanvas) { if (picture.recordingCanvas!.renderStrategy.hasArbitraryPaint) { _applyBitmapPaint(oldCanvas); } else { _applyDomPaint(oldCanvas); }}

那么這里有兩個(gè)問(wèn)題: BitmapCanvas 和 DomCanvas 的區(qū)別是什么?hasArbitraryPaint 的判斷邏輯是什么?

首先 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 里。

d7db6a5a-fe5e-11ec-ba43-dac502259ad0.png

回到前面的文本問(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)兀?/p>

我們先看如下代碼,運(yùn)行后效果如下圖所示,可以看到此時(shí)的文本是直接使用 canvas 渲染的,這個(gè)結(jié)果符合我們目前的預(yù)期。

Scaffold( body: Container( alignment: Alignment.center, child: Center( child: Container( child: Text( “v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333”, ), ), ), ),)

d7ebba22-fe5e-11ec-ba43-dac502259ad0.png

接下來(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”, ), ), ), ),)

d801c2e0-fe5e-11ec-ba43-dac502259ad0.png

這里就需要先講到 BitmapCanvas 的 drawRect 實(shí)現(xiàn),如下關(guān)鍵代碼所示,在 drawRect 時(shí),如果在滿(mǎn)足 _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 的條件就是滿(mǎn)足其中三大條件之一即可,下述表格里大致描述了每個(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

_contains3dTransformtransformKind == 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)兀?/p>

d82c4d62-fe5e-11ec-ba43-dac502259ad0.png

這是因?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)繪制文本。

@overridevoid 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);

····}

而在 BitmapCanvas 里,有三個(gè)操作會(huì)觸發(fā) _childOverdraw = true 和 _canvasPool Empty:

_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”, ), ), ), ), )

d8418eac-fe5e-11ec-ba43-dac502259ad0.png

結(jié)合前面的流程看這是符合預(yù)期的,因?yàn)榇藭r(shí)帶有 boxShadow 參數(shù),該參數(shù)會(huì)在繪制時(shí)通過(guò) toPaint 方法轉(zhuǎn)化為 maskFilter,所以在 maskFilter != null 的情況下,流程不會(huì)進(jìn)入到 Element 的判斷,所以使用 canvas。

d850dbf0-fe5e-11ec-ba43-dac502259ad0.png

繼續(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”, ), ), ), ), ), )

d86108ae-fe5e-11ec-ba43-dac502259ad0.png

可以看到此時(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。

d8766ce4-fe5e-11ec-ba43-dac502259ad0.png

繼續(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”, ), ), ), ),)

d8883fa0-fe5e-11ec-ba43-dac502259ad0.png

d89c0620-fe5e-11ec-ba43-dac502259ad0.png

最后再來(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), ), ), ), ), );

d8aafa54-fe5e-11ec-ba43-dac502259ad0.png

這是因?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ì)麻煩很多。

d8bbf1f6-fe5e-11ec-ba43-dac502259ad0.png

前面講了那么多例子都是 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)) ], ), ), ), ),)

d8cd51f8-fe5e-11ec-ba43-dac502259ad0.png

所以最后總結(jié)一下: 首先除了下圖所示之外的情況,大部分時(shí)候 Flutter Web 繪制都會(huì)進(jìn)入到 BitmapCanvas。

d8db99de-fe5e-11ec-ba43-dac502259ad0.png

結(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。

d8edc3ca-fe5e-11ec-ba43-dac502259ad0.png

最后針對(duì)文本,在 drawParagraph 時(shí)還有特殊處理,關(guān)于 _childOverdraw 和 !isInsideSvgFilterTree 相關(guān)前面解釋過(guò)了,新增條件是在有 TextDecoration 或者 FontFeatures 時(shí),也會(huì)觸發(fā)文本繪制變?yōu)?Element,也就是 p + span 標(biāo)簽的形式。

d900c420-fe5e-11ec-ba43-dac502259ad0.png

四、最后

雖然本次介紹的東西不少,但是 Flutter Web 在 html 渲染模式下的知識(shí)點(diǎn)遠(yuǎn)不止這些,而由小窺大,以 drawRect 和文本為切入點(diǎn)去了解 SurfaceCanvas 就是很不錯(cuò)的開(kāi)始。

另外可以看到,在 Flutter Web 里有很多的自定義的 《flt-*》 標(biāo)簽,這些標(biāo)簽都是通過(guò)如 html.Element.tag(‘flt-canvas’); 等方式創(chuàng)建,它們和 Flutter 里的對(duì)應(yīng)關(guān)系如下圖所示,如果感興趣可以在 chrome 的 source 里對(duì)應(yīng)的 dart_sdk.js 查看具體實(shí)現(xiàn)。

原文標(biāo)題:帶您了解最全面的 Flutter Web | 開(kāi)發(fā)者說(shuō)·DTalk

文章出處:【微信公眾號(hào):谷歌開(kāi)發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

審核編輯:彭靜
聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀(guān)點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • Android
    +關(guān)注

    關(guān)注

    12

    文章

    3936

    瀏覽量

    127405
  • API
    API
    +關(guān)注

    關(guān)注

    2

    文章

    1501

    瀏覽量

    62018
  • iOS
    iOS
    +關(guān)注

    關(guān)注

    8

    文章

    3395

    瀏覽量

    150610
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4788

    瀏覽量

    68612

原文標(biāo)題:帶您了解最全面的 Flutter Web | 開(kāi)發(fā)者說(shuō)·DTalk

文章出處:【微信號(hào):Google_Developers,微信公眾號(hào):谷歌開(kāi)發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    鴻蒙Flutter實(shí)戰(zhàn):14-現(xiàn)有Flutter 項(xiàng)目支持鴻蒙 II

    引言 在之前的文章鴻蒙Flutter實(shí)戰(zhàn):09-現(xiàn)有Flutter項(xiàng)目支持鴻蒙中,介紹了如何改造項(xiàng)目,適配鴻蒙平臺(tái)。 文中講述了整體的理念和思路,本文更進(jìn)一步,結(jié)合可實(shí)操的項(xiàng)目代碼,詳細(xì)說(shuō)明如何實(shí)施
    發(fā)表于 12-26 14:59

    CCD傳感器與CMOS傳感器的相同之處不同之處

    ? ? ? ?本文介紹了CCD傳感器與CMOS傳感器的相同之處不同之處。 相對(duì)最早發(fā)展起來(lái)的模擬相機(jī),數(shù)字相機(jī)也是一個(gè)很龐大的家族,早在20世紀(jì)70年代,相機(jī)里出現(xiàn)了以CMOS技術(shù)為核心的類(lèi)型分支
    的頭像 發(fā)表于 11-24 10:39 ?882次閱讀

    鴻蒙Flutter實(shí)戰(zhàn):11-使用 Flutter SDK 3.22.0

    # 使用 Flutter SDK 3.22.0 ## SDK 安裝 參考[鴻蒙Flutter實(shí)戰(zhàn):01-搭建開(kāi)發(fā)環(huán)境]文章的說(shuō)明,首先安裝 Flutter SDK 3.22.0。 目前鴻蒙化
    發(fā)表于 11-01 15:03

    NXP MCX N23和MCX N94/54的不同之處

    繼2024年一月份發(fā)布了MCXN94/54系列之后,NXP又在6月份發(fā)布了N系列的第二款產(chǎn)品,MCX N23系列,下面小編就為大家揭開(kāi)它的神秘面紗,來(lái)看看這款產(chǎn)品何特點(diǎn),了解一下它和N94/54又有什么不同之處呢!
    的頭像 發(fā)表于 11-01 12:35 ?426次閱讀
    NXP MCX N23和MCX N94/54的<b class='flag-5'>不同之處</b>

    鴻蒙Flutter實(shí)戰(zhàn):09-現(xiàn)有Flutter項(xiàng)目支持鴻蒙

    # 鴻蒙Flutter實(shí)戰(zhàn):現(xiàn)有Flutter項(xiàng)目支持鴻蒙 ## 背景 原來(lái)使用Flutter開(kāi)發(fā)的項(xiàng)目,需要適配鴻蒙。 ## 環(huán)境搭建 見(jiàn)文章[鴻蒙Flutter適配指南],
    發(fā)表于 10-23 16:36

    鴻蒙Flutter實(shí)戰(zhàn):08-如何調(diào)試代碼

    # 鴻蒙Flutter實(shí)戰(zhàn):如何調(diào)試代碼 ## 1.環(huán)境搭建 參考文章[鴻蒙Flutter實(shí)戰(zhàn):01-搭建開(kāi)發(fā)環(huán)境](https://gitee.com/zacks
    發(fā)表于 10-23 16:29

    鴻蒙Flutter實(shí)戰(zhàn):07混合開(kāi)發(fā)

    # 鴻蒙Flutter實(shí)戰(zhàn):混合開(kāi)發(fā) 鴻蒙Flutter混合開(kāi)發(fā)主要有兩種形式。 ## 1.基于har 將flutter module打包成har包,在原生鴻蒙項(xiàng)目中,以har包的方式引入
    發(fā)表于 10-23 16:00

    鴻蒙Flutter實(shí)戰(zhàn):06-使用ArkTs開(kāi)發(fā)Flutter鴻蒙插件

    來(lái)自 Flutter 的消息調(diào)用,分別實(shí)現(xiàn)了 \'getPrefs\' 和 \'setPrefs\' 兩個(gè)回掉,其中 getPrefs返回值,通過(guò) result.success(val);(見(jiàn)下)異步返回
    發(fā)表于 10-22 21:56

    鴻蒙Flutter實(shí)戰(zhàn):01-搭建開(kāi)發(fā)環(huán)境

    ; 如果要適配ios,需要安裝Xcode Mac 安裝(推薦) 環(huán)境變量配置 # Flutter Mirror export PUB_HOSTED_URL=https
    發(fā)表于 10-21 19:35

    繼電器和接觸器什么不同之處?

    繼電器和接觸器是兩種常用的電氣元件,它們?cè)陔娐分衅鹬浅V匾淖饔谩km然它們?cè)谀承┓矫嬗邢嗨?b class='flag-5'>之處,但它們之間還是存在一些不同之處。以下是對(duì)繼電器和接觸器的詳細(xì)比較: 定義和工作原理 繼電器是一種利用
    的頭像 發(fā)表于 06-21 10:10 ?899次閱讀

    智能制造與傳統(tǒng)制造什么不同之處

    、生產(chǎn)方式、管理模式等方面存在很大的不同,這些不同之處正是智能制造的優(yōu)勢(shì)和特點(diǎn)。 二、設(shè)計(jì)理念的不同 傳統(tǒng)制造設(shè)計(jì)理念 傳統(tǒng)制造的設(shè)計(jì)理念主要側(cè)重于產(chǎn)品的功能性、穩(wěn)定性和成本效益。在設(shè)計(jì)過(guò)程中,設(shè)計(jì)師需要充分考
    的頭像 發(fā)表于 06-07 15:36 ?3320次閱讀

    單片機(jī)和plc什么相同和不同之處

    某些方面具有相似之處,但在許多關(guān)鍵方面也存在顯著差異。本文將詳細(xì)探討單片機(jī)和PLC的相同和不同之處。 一、相同之處 控制功能:?jiǎn)纹瑱C(jī)和PLC都具有控制功能,可以對(duì)各種設(shè)備和系統(tǒng)進(jìn)行控制。它們可以接收輸入信號(hào),處理這些信號(hào),然后輸
    的頭像 發(fā)表于 06-06 14:05 ?1220次閱讀

    AUTOSAR MCAL驅(qū)動(dòng)程序與演示程序中的Libraries中的驅(qū)動(dòng)程序什么不同之處

    1.關(guān)于 AUTOSAR MCAL 驅(qū)動(dòng)程序 與演示程序中的 Libraries 中的驅(qū)動(dòng)程序 什么不同之處? 2.AUTOSAR MCAL 驅(qū)動(dòng)程序中是否包含了 TC397 安全菜單中提及的 SM(安全機(jī)制)的接口? 3.是否
    發(fā)表于 05-17 06:55

    淺談兼容 OpenHarmony 的 Flutter

    OpenHarmony SIG 組織在 Gitee 開(kāi)源了兼容 OpenHarmony 的 Flutter。該組織主要用于孵化 OpenHarmony 相關(guān)的開(kāi)源生態(tài)項(xiàng)目。 ? ? ▲ 倉(cāng)庫(kù)地址
    的頭像 發(fā)表于 02-02 15:22 ?613次閱讀
    淺談兼容 OpenHarmony 的 <b class='flag-5'>Flutter</b>

    光纖和光纜不同之處

    很多人會(huì)有這樣的疑問(wèn),光纖和光纜不同之處?主要是因?yàn)楣饫w和光纜這兩個(gè)名詞容易引起混淆。在嚴(yán)格的定義下,光纖和光纜是兩種不同的東西,然而在現(xiàn)實(shí)生活中,許多人仍然會(huì)混淆這兩者。為了更好地理解光纖和光纜之間的區(qū)別,我們一起來(lái)看一下。
    的頭像 發(fā)表于 01-15 17:01 ?851次閱讀
    主站蜘蛛池模板: 黄色视奸| 欧美性aaa| 综合激情婷婷| 国产美女视频爽爽爽| 亚洲欧洲第一页| 色播丁香| jiuse视频| 757一本到午夜宫| 5252a我爱haose01亚洲| 精品国产成人三级在线观看| 色综合视频在线观看| www.四虎.com| 欧美另类丰满69xxxxx| 午夜女上男下xx00xx00动态| 欧美一级欧美三级| 天天综合网在线| 国产成人影院在线观看| 在线亚洲成人| 国产乱码精品一区二区| 2022国产情侣真实露脸在线| 婷婷99| 大尺度在线| 国产黄网站在线观看| 日本免费成人| 人操人操| 亚洲入口无毒网址你懂的| 丁香六月婷婷精品免费观看| 视频在线观看免费网站| 色婷婷基地| 天天干天天爱天天操| aa三级动态图无遮无挡| 国产成人精品免费视频大全可播放的 | 亚洲午夜精品久久久久久成年| xxx69日本hd| 五月天福利视频| 91精品久久国产青草| 同性同男小说肉黄| 日本精品一卡二卡≡卡四卡| 人人福利| 特级毛片aaa免费版| 国产成人精品视频一区二区不卡|