Flutter 內幕:Flutter 在內部是如何工作的?

發表于 1年以前  | 總閱讀數:2763 次

Flutter 內部是如何工作的?Widgets、Elements、BuildContext 到底是什么東西?為什么 Flutter 可以運行那么快?為什么有時候運行的效果并不符合我們的預期?什么是所謂視圖樹?——本文將一一為你解答。

以下為譯文:Flutter 內部是如何工作的?Widgets 、Elements 、BuildContext、RenderObject 這些都是些什么東西?

簡介

去年,我剛開始使用 Flutter 開發應用程序時,在互聯網上,只能找到極少量Flutter相關文檔。雖然有一些文章在談論 Flutter,但基本沒有文章寫 Flutter 內部的工作原理。

Widgets、Elements、BuildContext 到底是什么東西?為什么 Flutter 可以運行那么快?為什么有時候運行的效果并不符合我們的預期?什么是所謂視圖樹?

在Flutter開發一個應用程序時,有 95% 的需求,都只需要處理 Widgets ,用它來展示 UI 并處理屏幕交互。但你是否考慮過,整個系統是如何工作的,是如何知道要更新哪些 UI 呢?

第一部分:背景

這一部份內容包含一些關鍵概念,了解他們,會有助于理解后面的內容。

硬件

讓我們從最基礎的東西開始說起。

當你在使用你的設備的時候,或者說你在使用某個應用程序的時候,你只會看到一塊屏幕。

事實上,在屏幕上,你看到了一系列的像素點,這些像素點,共同組成了一個二維的圖像。當你觸摸屏幕的時候,屏幕也只識別你手指在屏幕中的位置。

神奇的是,我們的應用程序可以觸發屏幕顯示的圖像更新。在以下情況下,應用程序可以觸發屏幕顯示圖像更新:

  • 屏幕事件 (點擊屏幕 )
  • 網絡事件 (與服務器通信)
  • 時間事件 (動畫)
  • 其它傳感器事件

將圖片渲染到屏幕上是由顯示屏硬件來保證,這些顯示屏都是按固定的間隔時間來刷新屏幕,通常是1秒鐘刷新60次。這個刷新的頻率,我們通常叫作 刷新率,用 HZ 表示。

設備從 GPU 接收到要在屏幕上顯示的數據,渲染顯示在屏幕上(注:GPU 是一種專用的電子電路,經過優化,可以從 polygons 和 textures 獲取數據,并快速生成圖像)。我們將 GPU 在每秒中生成并發送給設備用于顯示的圖像的次數叫做 幀率, 使用 FPS 來做為單位進行計量。

看到這里,你也許會問,在文章的開篇,我們寫到二維圖像與硬件屏幕的渲染,這些又和 Flutter 的 Widgets 有什么關系呢?

答案很簡單,因為 Flutter 的一個核心功能就是合成二維圖像并處理交互。我認為,從這個角度來解讀,可以更好的理解 Flutter 的內部工作原理。

當然,還有一個原因,不管你信或者不信,Flutter 中幾乎所有的事情都是由刷新屏幕的需求來驅動的,我們需要在適當的時候來快速刷新我們的屏幕。

接口設計

不管什么時候,只要你加入Flutter開發的行列中,都會看到下面這張 Flutter 的架構圖。

我們在開發 Flutter 的應用程序的時候,使用 Dart 語言,我們面象的 API 都是 Flutter框架(綠色部分) 這一層提供的。

Flutter 框架通過 Window 這個抽象層與 Flutter引擎(藍色部分)進行通信, Window 中抽象出來了一系列的 API,實現了與硬件通信的接口。

當然,在下面的這些情況中,Flutter引擎也可以通過 Window 來通知 Flutter 框架層來進行事件處理。

  • 設備級別的屬性更改 (設備方向改變,設置修改,內存問題,APP狀態修改等)
  • 屏幕級別的更改(手勢)
  • 平臺渠道發送的數據
  • 在 Flutter 引擎層空閑下來,可以渲染新的幀的時候,會發送通知給 Flutter 框架層。

Flutter Engine 渲染驅動 Flutter 框架

這一部分內容很難以理解,但是這就是真實的邏輯。

除了以下幾種情況, Flutter 框架的代碼執行都是由 Flutter 引擎觸發的。

  • 手勢 (屏幕上的事件)
  • 平臺消息(如 GPS)
  • 硬件消息(如旋轉屏幕, 應用壓后臺,內存不足等)
  • 異步消息( Future API 或者 HTTP 響應)

注:一般情況下,如果Flutter渲染引擎沒有發出通知, Flutter 框架是不能更新任何UI的。有些時候,在沒有 Flutter 渲染引擎通知的情況下,也可以讓 Flutter 框架更新UI,但是并不建議這么做。

你或許會問我,執行手勢相關的邏輯,會讓 UI 發生變化;使用一個異步任務,或者動畫,也會讓 UI 發生改變,那它們又是如何工作的呢?

如果你想更新UI, 或者說你想在后臺執行代碼邏輯并更新 UI,你需要告訴 Flutter 引擎,這里有一些更改需要被渲染到屏幕上。通常情況下,在屏幕下一次刷新的時候, Flutter引擎會通知 Flutter框架,讓它來提供新場景的圖像來進行渲染顯示。

因此,Flutter 引擎是如何基于渲染編排整個應用程序的行為?從下面的動畫中,我們可以了解到整個內部運行機制:

整個動畫過程解釋如下:

  • 像手勢、http 網絡請求和異步事件,它們都會觸發一個異步任務,當它們引起 UI 的更新。它們會發送一個消息(Schedule Frame)給 Flutter引擎,告訴 Flutter引擎,有新的UI需要被渲染。
  • 當 Flutter引擎準備好,可以更新UI的時候,它會發送 Begin Frame 通知到Flutter框架。
  • Flutter 框架運行著的異步任務,如動畫,它們會攔截掉 Begin Frame 通知。
  • 這些異步任務會根據自身狀態進行判斷是否需要繼續發送請求給 Flutter 引擎,用來觸發后續的UI渲染(例:當一個動畫沒有完成的時候,為了讓動畫可以繼續執行,它會發送一個通知到 Flutter 引擎,然后會等待接收另一個 Begin Frame 的通知)。
  • 緊接著,Flutter 引擎會發出一個 Draw Frame 的通知到 Flutter 框架層。
  • Flutter 框架會攔截 Draw Frame 通知,并根據任務進行布局調整和UI大小計算。
  • 完成這些任務后,它將繼續執行與更新布局有關的繪畫任務。
  • 如果有什么要畫在屏幕上,它會發送一個全新的場景數據到 Flutter 引擎,讓Flutter引擎來更新到屏幕上。
  • 最后,Flutter 框架執行完所有的任務并且在屏幕中渲染完成。
  • 緊接著會繼續一遍又一遍的執行上述流程。

RenderView 和 RenderObject

在討論與事件流相關的細節之前,先來看看視圖樹。

如之前所說, 所有東西最后都會轉換成像素顯示在屏幕中, Flutter 框將我們用于開發程序使用的 Widgets 轉化成可視部分,顯示在屏幕上。

在 Flutter 中,用來與渲染在屏幕上的可見視圖一一對應的對象,我們稱作 RenderObject,它被用來表示:

  • 定義屏幕中的區域,包括 大小,位置, 幾何結構。也可稱其為"渲染內容"。

  • 識別可能受到手勢影響的屏幕區域。

一堆 RenderObject 共同組成了一棵樹,稱之為視圖樹。在視圖樹的最上面,也就是其跟節點,就是RenderView。RenderView 代表了整個輸出的視圖樹,它也是一種特殊的 Renderobject , 如圖所示:

在文章的后面會講解 WidgetsRenderObjects 之前的關系。但在這之前,我們需要更深入的了解 RenderObjects。

Bindings的初始化

當 Flutter 應用程序啟動的時候,系統會執行 main() 方法,它會調用 runApp(Widget app)。

在調用runApp()這個方法的時候,Flutter 會初始化 Flutter 框架Flutter 引擎 之間的接口,它被稱作是 bindings。

Bindings簡介

Bindings 是建立 Flutter 框架Flutter 引擎之間通信的橋梁,Flutter的這兩個部分只能通過它傳遞數據(其中RenderView 是個例外,在后面會講到)。

每個bindings負責處理一組特定的任務、操作或者事件。本文中,作者按其作用域進行了重新分組。

到目前為止, Flutter 框架 提供8個 bindings,本文中,只討論以下四個:

  • SchedulerBinding
  • GestureBinding
  • RendererBinding
  • WidgetsBinding

為了保證完整性,我列出剩下四個 bindings:

  • ServicesBinding :處理不同平臺發過來的消息
  • PaintingBinding:處理圖片緩存
  • SemanticsBinding:保留到以后實現所有與語義相關的內容
  • TestWidgetsFlutterBinding:組件測試使用的

當然,我也注意到了 WidgetsFlutterBinding ,但這個不是真正的 bindings, 而是一種 bindings 初始化工具。

有關 bindings 與 Flutter 引擎的交互邏輯,見下圖所示:

下面,我們分別來看一下這幾個 bindings。

SchedulerBinding

它有兩個主要功能:

  • 第一個是告訴 Flutter 引擎:“我現在已經準備好了,在你不忙的時候,把我喚醒,告訴我要渲染的內容,我會開始工作?!?/li>
  • 第二個是監聽并響應一些事件,如喚醒事件。

當SchedulerBinding接受到喚醒事件 的時候,要做些什么呢?

  • 需要 Ticker 來控制的時候

舉個例子,假設您有一個動畫,并且已經開始執行了。這個動畫是由Ticker進行控制的,它需要以固定時間間隔觸發回調。要讓這樣的回調運行,我們需要告訴Flutter 引擎在下次刷新時喚醒我們(發送Begin Frame),觸發回調,執行動畫任務。在該動畫任務結束時,動畫還需要繼續,它將再次調用SchedulerBinding來調度另一幀。

  • 更改布局

當你響應導致視覺變化的事件(例如,更新屏幕的一部分的顏色,滾動,向屏幕中添加/從屏幕中刪除某些內容)時,我們需要采取必要的步驟來保證它可以正常的顯示在屏幕上。在這種情況下,Flutter框架將調用SchedulerBinding來告訴Flutter Engine去調度另一幀。

GestureBinding

這個 binding 處理手勢事件,并與 Flutter 引擎進行通信。它負責接受與手指有關的數據,并確定屏幕的哪一部分受到手勢的影響。然后,它會通知這些部分,來響應事件。

RendererBinding

它是 Flutter 引擎與視圖樹之前的橋梁,它有兩個不同的功能:

  • 第一個是監聽Flutter引擎發出的消息,當設備設置發生更改,它會告知用戶受到影響的視覺效果/語義。
  • 第二個是為 Flutter 引擎提供要顯示在屏幕上的數據。

為了提供要在屏幕上呈現的修改,它負責驅動PipelineOwner并初始化RenderView。

PipelineOwner是一種協調器,它知道哪個RenderObject需要做一些與布局有關的事情并協調這些動作。

WidgetsBinding它用于監聽用戶設置的更改,如語言的修改。不僅如此, WidgetsBinding 否是 Widgets 與 Flutter 引擎之間通信的橋梁,有兩個主要的功能:

  • 第一個是負責處理Widgets結構變更的過程;
  • 第二個是觸發渲染事件。

一些小組件的結構更改是 BuildOwner 來完成的,它跟蹤需要重建的小部件,并處理應用于整個小部件結構的其他任務。

第二部分:Widgets 轉換成像素

基礎的內部原理講解完成,下面我們來看看 Widgets。

在所有有關 Flutter 的文檔中,你能看到這樣子的描述:在Fluter中,所有的對象都是 Widgets 。這樣子說雖然沒錯,但要說得更精確一些,我覺得應該是:

開發人員的角度來看,在布局和交互方面,與用戶界面相關的所有內容均通過Widgets完成。

為啥要如此精確?Widget允許開發人員根據尺寸,內容,布局和交互性來定義屏幕的一部分。不僅這些,還有更多的東西也是通過它來定義。那么,什么是Widgets呢?

不變的屬性

在讀 Flutter 的源碼的時候,你可以在 Widget 類中看到如下定義:

1@immutable
2abstract class Widget extends DiagnosticableTree {
3  const Widget({ this.key });
4
5  final Key key;
6
7  ...
8}

這是什么意思?

@immutable 這個注解告訴我們,在 Widget 類中定義的所有變量都是 FINAL 的。換句話說,它們只能被定義一次。因此,一但初始化完成,這個 Widget 的內部變量將不能被修改。

Widgets 結構

當你在使用用Flutter的時候,你寫一個頁面,會用到 Widget,像下面的代碼中那樣,定義出要顯示的UI:

1Widget build(BuildContext context){
 2    return SafeArea(
 3        child: Scaffold(
 4            appBar: AppBar(
 5                title: Text('My title'),
 6            ),
 7            body: Container(
 8                child: Center(
 9                    child: Text('Centered Text'),
10                ),
11            ),
12        ),
13    );
14}

上述例子中,一共使用了7個 Widgets,它們組成了一個樹狀結構。一個非常簡單的結構,根據代碼,可以畫出如下結構圖:

正如你看到的,他像一個樹, SafeArea 是這個樹的根節點。

復雜的 Widget 結構

正如你知道的那樣, Widget 可以將很多其它的Widget 聚合在一起。舉個例子,我可以使用如下代碼替換上面的代碼:

1Widget build(BuildContext context){
2    return MyOwnWidget();
3}

我們假設 MyOwnWidget 會自己去渲染 SafeArea, Scaffold 等 Widget。這個例子,要表達的意思是:

Widget 可能是一個頁子節點,也可能是一顆樹。

元素

為什么我會提到這個,正如我們將在后面看到的那樣,為了能夠生成可以在設備上渲染的圖像的像素,Flutter需要詳細了解組成屏幕的所有Widget,并確定所有部分, 按要求生成 Widget 。

為了說明這一點,你可以想象一下俄羅斯套娃,最開始的時候,你只能看到一個玩偶,但是這個玩偶中,包含了另一個,然后依次包含另一個,以此內推。

Flutter 生成所有的 Widgets 時,就像獲得所有的俄羅斯套娃一樣。

下圖展示了如何顯示 Widget 的所有部分,圖中,黃色部分表示你在代碼中寫到的部分,你可以在不同的組件中使用:

注:在這里我們使用了 "Widget 樹",這只是為了更好的理解邏輯,在 Flutter中并沒有這個概念。

現在我們來介紹前面提到的元素。

每個小部件都對應一個元素。元素彼此鏈接并形成一棵樹。因此,元素是樹中某個節點的引用。

首先,將元素作為一個節點,它有父結點,也有子結點。然后通過父子關系將它們鏈接在一起,就可以得到一個樹形結構。

如圖中看到的, 元素可以對應一個 Widget ,也可以對應一個 RenderObject。

總結一下:

  • 沒有 Widgets 樹,但是有元素樹;
  • Widgets 創建 Elements;
  • Element 指向創建它的 Widget;
  • Elements 使用父子關系進行關聯;
  • Elements 有一個或多個子節點;
  • Elements 也可以指向一個 RenderObject。

Elements 定義了視圖部分的鏈接關系。

為了可以更好的表達 element 的概念,讓我們來看看下圖:

圖中所示, 元素樹連接了 Widgets 和 RenderObjects。

但是,為什么 Widget 會創建 Element?

三種 Widgets

在 Flutter , Widgets 可以拆分成三類(僅僅是我個人為了組織它們,而進行的分類),如下:

  • 代理類

這類 Widget 主要作用是用來保存一些數據信息的,并且做為樹結構的根結點。點型的例子是 InheritedWidgetLayoutId這些 Widgets 不會展現出任何的用戶頁面,但是它們會用來為其它的Widgets提供數據。

  • 渲染類

這類 Widget 直接或間接的用于屏幕布局:

  • 大小尺寸
  • UI位置
  • 布局/渲染方式

典型例子:Row,Column,Stack、Padding、Align、Opacity、RawImage 等。

  • 組件類

剩下的這一類 Widget 不能直接用于大小、位置、布局等設置,它們只是用來展示最終的數據信息。這些 Widget 通常被稱之為組件。

典型例子:RaisedButton, Scaffold, Text, GestureDetector, Container。

Widgets 分類

為什么拆分顯得如此重要?因為根據 Widget 的分類,會關聯不同的 Element。

Element 分類

下圖中展示了不同的 Element 分類:

內部Element 分類

Element 主要分為兩大類:

  • 組件,此分類不直接用于任何視覺渲染的部分。
  • 渲染,此分類是直接用于屏幕渲染。

到目前為止,出現了很多的概念,它們是如何關聯在一起?

Widgets 和 Elements 是如何在一起工作的?

在Flutter中,整個系統都依賴 Widget / RenderObject 的狀態。

更新 element 的狀態的兩種不同方式:

  • 使用 setState 方法,這個方法可以用于所有的 StatefulElement (注意,我這里面說的不是 StatefulWidget)。
  • 使用通知,基于 ProxyElement來實現狀態更新(如:InheritedWidget)。

Elements 的狀態變更是需要Elements的引用放入了臟元素列表中。使RenderObject無效意味著沒有任何更改應用于元素的結構,但是發生了renderObject級別的修改:

  • 大小、位置等修改;
  • 需要重繪,如背景顏色修改、字體樣式修改。

RenderObject 的狀態變更是需要將 RenderObject 的引用放在重繪/重建列表中。

不管是什么級別的變更,只要有變更發生,SchedulerBinding 就會發送一個消息到 Flutter 引擎,開始新的一次UI渲染。當 Flutter 引擎喚醒 SchedulerBinding 的時候,所有的變更都將生效,像魔法一樣。onDrawFrame()在前面,我們提到了 SchedulerBinding 兩個主要的功能,其中一個就是處理由 Flutter 引擎發出的與幀視圖重建相關的請求。下面,我們來看看有關它的詳細細節。當 SchedulerBinding 收到 Flutter 引擎發出的 onDrawFrame 時,執行的流程圖如下所示:

流程圖

  • 第1步,元素

WidgetsBinding被執行的時候, Flutter引擎首先考慮的是元素的變化。因為由建造者自己管理元素樹,所以綁定控件的時候,會調用建造者的 buildScope 方法。這個方法中,會將要更改的元素存起來,稍后觸發他們進行重建。

rebuild() 主要的原則如下:

  • 大部分時候,觸發元素的重建,會調用控件的 build()方法(Widget build(BuildContext context) {….}),這個方法會返回一個新的控件。

  • 如果元素沒有子節點,這個元素就被創建完成,反之,會先創建子節點。

  • 將新控件與元素引用的子控件進行比較:

  • 如果可以被替換, 則更新,并保留子控件;

  • 如果不可以被替換,子控件會被移除,并創建一個新的。

創建一個新的控件會創建一個與之對應的新的元素。并且會把元素插入到元素樹中去。下圖展示了這個過程:

當控件被創建的時候,需要創建一個元素與之進行關聯。

控件與元素的對應關系如下:

舉個例子:

StatefulElement 在初始化的時候會執行 widget.createState() 方法,創建并關聯對應的狀態。RenderObjectElement 在元素被加載的時候,會創建一個 RenderObject, 并且會將這個對象加入到渲染樹。

  • 第2步,渲染對象

一旦完成了臟元素有關的所有動作,元素樹就變成了一個穩定結構,是時候考慮渲染到屏幕上了。

渲染綁定用來處理渲染樹,控件綁定會調用渲染綁定的 drawFrame 方法。

下圖展示了 drawFrame() 的整個調用流程:

這個過程中,主要執行以下的幾個事件:

  • 為每一個標記為的渲染對象計算新的布局(計算大小和幾何形狀);
  • 使用渲染層,將所有需要重繪的對象重畫出來;
  • 將生成的場景數據發送給 Flutter 引擎,然后在屏幕中顯示出來;
  • 最后,Semantics 被發送更新到 Flutter 引擎。

在流程的最后,設備屏幕顯示的圖像將會被更新。

手勢處理

手指在屏幕上點擊或移動,觸發手勢,這些會被 GestureBinding 處理響應。

當 Flutter 引擎通過 window.onPointerDataPacket 方法發送出手勢相關的事件, GestureBinding 會攔截并處理:

  • Flutter 引擎將屏幕位置轉換成對應的坐標;
  • 拿到坐標上所有渲染出來的View對應的 RenderObject;
  • 然后遍歷所有的RenderObject ,并把對應事件分發給他們;
  • RenderObject 會等待它能處理的時間并處理它。

動畫

最后一部分,我們將聚焦到動畫和 Ticker。

當你初始化一個動畫的時候,你通常會創建一個 AnimationController 或者類似的控件或者組件。

在Flutter中,與動畫相關的所有內容均為Ticker。

Tikcer 只做一件事情,它在 SchedulerBinding 上注冊一個回調,當Flutter引擎在下一次可用的時候,會將它喚醒。

當 Flutter 引擎可用,它會觸發 SchedulerBinding 的 "onBeginFrame" 通知。SchedulerBinding 會遍歷并執行所有的 Ticker 的回調。

Ticker 會被對此事件感興趣的攔截攔截,并對其進行處理。當動畫執行結束, Ticker會被標識為“不可用”。因此,Ticker 會告知 SchedulerBinding 去執行另一個回調。

總流程圖

現在,我們已經知道了 Flutter 內部的工作原理,來看看整體的流程圖:

總結構圖

BuildContext最后,如果你還記得不同元素類型的圖,你可能已經注意到了元素的簽名:dart abstract class Element extends DiagnosticableTree implements BuildContext { ...}

什么是 BuildContext ?

BuildContext是一個接口,它定義一系列元素要實現的方法。

特別的是, BuildCotext 中的 build() 方法在 StatelessWidgetStatefulWidget 中被使用,還有狀態對象在 StatefulWidget 中被使用。

除了一下的兩種情況下,其他時候 BuildContext 是沒有任何用處的:

  • 控件被重建的時候;
  • 在StatefulWidget鏈接到你引用的上下文變量的狀態。

這就意味著,大部分時候,我們并不需要知道它。

BuildContext 可以用來干什么?

由于BuildContext既與控件相關的元素相對應,也與空間所在樹中的位置相對應,因此該BuildContext對以下情況非常有用:

  • 獲得對應于控件的渲染對象的基準;
  • 獲取RenderObject的大??;
  • 訪問樹——這是實際使用的所有小部件通常實施該方法的(例如MediaQuery.of(Context),Theme.of(Context))。

小例子

我們知道 BulidContext 也是一個元素,我給你展示一種有關 BuildContext 的使用方法。下面的代碼可以使 StatelessWidget 更新,但是并不使用 setState 方法,而是使用 BuildContext:

1   void main(){
 2       runApp(MaterialApp(home: TestPage(),));
 3   }
 4
 5   class TestPage extends StatelessWidget {
 6       // final because a Widget is immutable (remember?)
 7       final bag = {"first": true};
 8
 9       @override
10       Widget build(BuildContext context){
11           return Scaffold(
12               appBar: AppBar(title: Text('Stateless ??')),
13               body: Container(
14                   child: Center(
15                       child: GestureDetector(
16                           child: Container(
17                               width: 50.0,
18                               height: 50.0,
19                               color: bag["first"] ? Colors.red : Colors.blue,
20                           ),
21                           onTap: (){
22                               bag["first"] = !bag["first"];
23                               //
24                               // This is the trick
25                               //
26                               (context as Element).markNeedsBuild();
27                           }
28                       ),
29                   ),
30               ),
31           );
32       }
33   }

與執行 setState 方法相同,其核心都是執行 _element.markNeedsBuild() 方法。

結語

我認為了解Flutter的架構是很有趣的,所有東西都被設計為高效,可擴展且對將來的擴展開放。而且,諸如Widget,Element,BuildContext,RenderObject之類的關鍵概念并不總是顯而易見。

我希望本文對你有用。


https://mp.weixin.qq.com/s/pKfz5tRITnpkzNM_GU6r-w

最多閱讀

如何有效定位Flutter內存問題? 2年以前  |  13461次閱讀
Flutter的手勢GestureDetector分析詳解 3年以前  |  9679次閱讀
Flutter插件詳解及其發布插件 3年以前  |  8728次閱讀
在Flutter中添加資源和圖片 4年以前  |  6561次閱讀
發布Flutter開發的iOS程序 4年以前  |  5762次閱讀
Flutter 狀態管理指南之 Provider 3年以前  |  5706次閱讀
Flutter for Web詳細介紹 3年以前  |  5493次閱讀
在Flutter中發起HTTP網絡請求 4年以前  |  5200次閱讀
使用Inspector檢查用戶界面 4年以前  |  5081次閱讀
Flutter路由詳解 3年以前  |  4748次閱讀
為Flutter應用程序添加交互 4年以前  |  4109次閱讀
Flutter Widget框架概述 4年以前  |  4089次閱讀
JSON和序列化 4年以前  |  3976次閱讀
推薦5個Flutter重磅開源項目! 2年以前  |  3868次閱讀
Flutter框架概覽 4年以前  |  3784次閱讀
處理文本輸入 4年以前  |  3707次閱讀
使用自定義字體 4年以前  |  3667次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝,女生把筷子放屁眼里,国产精品久久久,国产日产欧洲无码视频