「本文已參與好文召集令活動,點選檢視:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!」
Widget 渲染過程
Flutter 把檢視資料的組織和渲染抽象為三部分,即 Widget,Element 和 RenderObject。
Flutter 渲染過程,可以分為這麼三步:首先,通過 Widget 樹生成對應的 Element 樹;然後,建立相應的 RenderObject 並關聯到 Element.renderObject
屬性上;最後,構建成 RenderObject 樹,以完成最終的渲染。
螢幕每秒重新整理60幀,Flutter 第一次將頁面繪製到螢幕上,它需要找出螢幕上每個元素的位置、顏色、文字等。也就是說,在第一次渲染中,需要配置螢幕上的每一個畫素。
對於後續的螢幕重新整理和繪製,如果沒有任何更改,Flutter 會使用上一次的繪製資訊,並快速的在螢幕上繪製出來。而當每次螢幕重新整理都需要計算螢幕上的所有內容時,才會出現掉幀。
Widget
Widget 是對檢視的一種結構化描述,裡面儲存的是有關檢視渲染的配置資訊,包括佈局、渲染屬性、事件響應資訊等。
Widget 設計成不可變的,所以當檢視渲染的配置資訊發生變化時,Flutter 會選擇重建 Widget 樹的方式進行資料更新,以資料驅動 UI 構建的方式簡單高效。雖然重建涉及到大量物件的銷燬和重建,會對垃圾回收造成壓力,不過,Widget 本身並不涉及實際渲染點陣圖,所以它只是一份輕量級的資料結構,重建的成本很低。另外由於 Widget 的不可變性,可以以較低成本進行渲染節點複用,一個Widget物件可以對應多個 Element 物件。
Element
Element 是 Widget 的一個例項化物件,它承載了檢視構建的上下文資料,是連線結構化的配置資訊到完成最終渲染的橋樑。
Element 同時持有 Widget 和 RenderObject。而無論是 Widget 還是 Element,其實都不負責最後的渲染,只負責發號施令,真正去幹活兒的只有 RenderObject。
因為 Widget 具有不可變性,但 Element 卻是可變的。實際上,Element 樹這一層將 Widget 樹的變化(類似 React 虛擬 DOM diff)做了抽象,可以只將真正需要修改的部分同步到真實的 RenderObject 樹中,最大程度降低對真實渲染檢視的修改,提高渲染效率,而不是銷燬整個渲染檢視樹重建。這,就是 Element 樹存在的意義。
BuildContext 就是 Widget 對應的 Element。
Element 樹不會在每次呼叫build(){...}
方法時重建。
RenderObject
從其名字,我們就可以很直觀地知道,RenderObject 是主要負責實現檢視渲染的物件。
Flutter 通過控制元件樹(Widget 樹)中的每個控制元件(Widget)建立不同型別的渲染物件,組成渲染物件樹。而渲染物件樹在 Flutter 的展示過程分為四個階段,即佈局、繪製、合成和渲染。 其中,佈局和繪製在 RenderObject 中完成,Flutter 採用深度優先機制遍歷渲染物件樹,確定樹中各個物件的位置和尺寸,並把它們繪製到不同的圖層上。繪製完畢後,合成和渲染的工作則交給 Skia 搞定。
- 每當 Flutter 遇到一個之前沒有被渲染的元素時,它就會通過 Widget 樹中的配置,在元素樹中建立一個元素。
- 渲染樹也不會經常重建。
- 除了佈局、繪製、合成和渲染階段,它還有另一個階段,將監聽器附加到 Widget 上,這樣我們就可以監聽事件。
build()
的構建過程
每當狀態發生變化時,Flutter 就會呼叫方法build()
。一般有兩種出發重建的條件:
-
有狀態的 Widget 中呼叫
setState(){...}
方法。會導致build(){...}
方法的呼叫。 -
其次,每當有
MediaQuery
呼叫或Theme.of(...)...
呼叫、軟鍵盤出現/消失等,只要這些資料發生變化,就會自動觸發build(){...}
方法。
呼叫setState(){...}
將相應的元素標記為 dirty 。對於下一次重新整理(每秒發生60次),Flutter 會將build(){...}
方法建立的新配置進行分析,然後更新螢幕。
反最佳實踐的例子
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
debugPrint('=======_MyHomePageState.build:');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
Text('我是第一行'),
Text('我是第二行'),
Text('我是第三行'),
],
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
複製程式碼
使用 Flutter 建立的計數器專案,然後新增幾個text
,通常為了避免地獄回撥,我們通常會把巢狀的元件抽離成一個方法, Flutter Outline 也有快捷操作:
_buildRow(),
// Row(
// children: <Widget>[
// Text('我是第一行'),
// Text('我是第二行'),
// Text('我是第三行'),
// ],
// ),
複製程式碼
Row _buildRow() {
return Row(
children: <Widget>[
Text('我是第一行'),
Text('我是第二行'),
Text('我是第三行'),
],
);
}
複製程式碼
看著真棒,去掉了地域巢狀,方法行數減少,結構清晰。說真的,過去的一年多我都是這麼幹的。
問題
直到看到這個:
Wm 是 Flutter 的開發人員倡導者。
每當值發生變化時,Flutter 都會呼叫setState()
。這觸發了 Widget 呼叫build()
方法重建。然後呼叫_buildRow()
重建這個方法返回的控制元件。_incrementCounter``build``_buildRow()``_counter
。
前面渲染流程我們說過,build()
的時候 Element 不一定重建。但是因為這個方法,會導致每次都會重建它。重建不需要重建的東西時浪費了寶貴的CPU週期。
解決方案
解決方案很簡單:不是將構建方法拆分為更小的方法,而是將其拆分為小部件 - 無狀態小部件。
上面的 Demo 最終是這樣:
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
debugPrint('=======_MyHomePageState.build:');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_NonsenseWidget(),
// _buildRow(),
// Row(
// children: <Widget>[
// Text('我是第一行'),
// Text('我是第二行'),
// Text('我是第三行'),
// ],
// ),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
Row _buildRow() {
return Row(
children: <Widget>[
Text('我是第一行'),
Text('我是第二行'),
Text('我是第三行'),
],
);
}
}
class _NonsenseWidget extends StatelessWidget {
const _NonsenseWidget();
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Text('我是第一行'),
Text('我是第二行'),
Text('我是第三行'),
],
);
}
}
複製程式碼
當是一個 Widget 時,Flutter 會對比是否需要重建,因為是 相同型別的 StatelessWidget,所以會複用。避免了重建。