前言
要解答這個問題,首先需要認識到 Flutter 中有三棵樹:Widget
樹,Element
樹和 RenderObject
樹。
當應用啟動時 Flutter 會遍歷並建立所有的 Widget
形成 Widget Tree
,同時與 Widget Tree
相對應,通過呼叫 Widget
上的 createElement()
方法建立每個 Element
物件,形成 Element Tree
。
最後呼叫 Element
的 createRenderObject()
方法建立每個渲染物件,形成一個 Render Tree
。
然後需要知道 Widget
,Element
和 RenderObject
到底是啥以及它們是幹什麼的。
什麼是 Widget
Widget
是 Flutter 的核心部分,是使用者介面的不可變描述資訊。正如 Flutter 的口號 Everything’s a widget
, 用 Flutter 開發應用就是在寫 Widget
?。
Flutter 的 Widget
不只表示 UI 控制元件,還表示一些功能性的元件,如路由跳轉 Navigator
,手勢檢測 GestureDetector
元件等。
@immutable
abstract class Widget extends DiagnosticableTree {
/// Initializes [key] for subclasses.
const Widget({ this.key });
final Key key;
/// ...
@protected
Element createElement();
/// ...
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
複製程式碼
Widget
的 canUpdate
方法通過比較新部件和舊部件的 runtimeType
和 key
屬性是否相同來決定更新部件對應的 Element
。
什麼是 Element
Element
是例項化的 Widget
物件,通過 Widget
的 createElement()
方法,在特定位置使用 Widget
配置資料生成。
Element
用於管理應用 UI 的更新和更改,管理部件的生命週期,每個 Element
都包含對 Widget
和 RenderObject
的引用。
當 Widget
變化時,如果兩個 Widget
的 runtimeType
和 key
屬性相同的,那麼新的 Element
會通過 Element.update()
更新舊的 Element
,否則舊的 Element
會被刪除,新生成的 Element
插入到樹中。
abstract class Element extends DiagnosticableTree implements BuildContext {
/// Creates an element that uses the given widget as its configuration.
///
/// Typically called by an override of [Widget.createElement].
Element(Widget widget)
: assert(widget != null),
_widget = widget;
/// Change the widget used to configure this element.
///
/// The framework calls this function when the parent wishes to use a
/// different widget to configure this element. The new widget is guaranteed
/// to have the same [runtimeType] as the old widget.
///
/// This function is called only during the "active" lifecycle state.
@mustCallSuper
void update(covariant Widget newWidget) {
/// ...
}
/// Creates an instance of the [RenderObject] class that this
/// [RenderObjectWidget] represents, using the configuration described by this
/// [RenderObjectWidget].
///
/// This method should not do anything with the children of the render object.
/// That should instead be handled by the method that overrides
/// [RenderObjectElement.mount] in the object rendered by this object's
/// [createElement] method. See, for example,
/// [SingleChildRenderObjectElement.mount].
@protected
RenderObject createRenderObject(BuildContext context);
}
複製程式碼
什麼是 RenderObject
RenderObject
用於應用介面的佈局和繪製,儲存了元素的大小,佈局等資訊,例項化一個 RenderObject
是非常耗能的。
當應用執行時 Flutter 使用 RenderObject
的資料繪製應用介面,最終形成一個 Render Tree
。
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
/// Initializes internal fields for subclasses.
RenderObject() {
_needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
}
/// The render object at (or below) this location in the tree.
///
/// If this object is a [RenderObjectElement], the render object is the one at
/// this location in the tree. Otherwise, this getter will walk down the tree
/// until it finds a [RenderObjectElement].
RenderObject get renderObject {
RenderObject result;
void visit(Element element) {
assert(result == null); // this verifies that there's only one child
if (element is RenderObjectElement)
result = element.renderObject;
else
element.visitChildren(visit);
}
visit(this);
return result;
}
void layout(Constraints constraints, { bool parentUsesSize = false }) {
/// ...
}
/// ...
void paint(PaintingContext context, Offset offset) {
/// ...
}
}
複製程式碼
為什麼需要三棵樹
使用三棵樹的目的是儘可能複用 Element
。
複用 Element
對效能非常重要,因為 Element
擁有兩份關鍵資料:Stateful widget
的狀態物件及底層的
RenderObject
。
當應用的結構很簡單時,或許體現不出這種優勢,一旦應用複雜起來,構成頁面的元素越來越多,重新建立 3 棵樹的代價是很高的,所以需要最小化更新操作。
當 Flutter 能夠複用 Element
時,使用者介面的邏輯狀態資訊是不變的,並且可以重用之前計算的佈局資訊,避免遍歷整棵樹。
舉個例子說明
建立一個簡單的 Flutter 應用,程式碼如下
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
color: Colors.white,
debugShowCheckedModeBanner: false,
builder: (context, child) => HomePage(),
),
);
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool _isWorld = true;
Widget _buildWorld() {
return RichText(
text: TextSpan(
text: 'Hello world',
style: TextStyle(color: Colors.black),
),
);
}
Widget _buildFlutter() {
return RichText(
text: TextSpan(
text: 'Hello flutter',
style: TextStyle(color: Colors.black),
),
);
}
void changeText() {
setState(() {
_isWorld = !_isWorld;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: _isWorld ? _buildWorld() : _buildFlutter(),
),
SizedBox(height: 20.0),
// Padding(padding: EdgeInsets.only(top: 20.0)),
IconButton(icon: Icon(Icons.refresh), onPressed: changeText)
],
),
);
}
}
複製程式碼
顯示效果
開啟 Dart DevTools,可以看到應用的 Widget Tree
,此時 RichText
控制元件的 RenderObject
的 ID 是 #6276a
點選圖示將文字變成 Hello flutter
時
重新整理瀏覽器頁面再次檢視 RichText
的 RenderObject
的 ID 依然是 #6276a
可以發現 Flutter 只是更新了文字資料,複用了 RichText
對應的 Element
和 RenderObject
。
而使用 SizedBox
部件取代 Padding
部件時。
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: RichText(
text: TextSpan(
text: 'Hello $text',
style: TextStyle(color: Colors.black),
),
),
),
SizedBox(height: 20.0),
// Padding(padding: EdgeInsets.only(top: 20.0)),
IconButton(icon: Icon(Icons.refresh), onPressed: changeText)
],
),
);
}
複製程式碼
Padding
部件對應的 Element
和 RenderObject
都會被從樹中移除,使用 SizedBox
新生成的替代。
總結
Widget
是應用介面的宣告資訊。
Element
連結 Widget
和 RenderObject
,管理介面的更新和修改。
RenderObject
儲存具體的佈局資訊,負責繪製 UI。
參考
How Flutter renders Widgets (Video)