Flutter渲染原理簡介
優(yōu)化之前我們先來介紹下Flutter的渲染原理,通過這部分基礎(chǔ)了解渲染流程以及主要耗時花費
flutter視圖樹包含了三顆樹:Widget、Element、RenderObject
Widget
:存放渲染內(nèi)容
,它只是一個配置數(shù)據(jù)結(jié)構(gòu),創(chuàng)建是非常輕量的,在頁面刷新的過程中隨時會重建Element
: 同時持有Widget
和RenderObject
,存放上下文信息
,通過它來遍歷視圖樹,支撐UI結(jié)構(gòu)RenderObject
: 根據(jù)Widget的布局屬性進(jìn)行layout
,paint
,負(fù)責(zé)真正的渲染
從創(chuàng)建到渲染的大體流程是:根據(jù)Widget生成Element
,然后創(chuàng)建
相應(yīng)的RenderObject
并關(guān)聯(lián)
到Element.renderObject
屬性上,最后再通過RenderObject
來完成布局排列和繪制
。
例如下面這段布局代碼
Container(
color: Colors.blue,
child: Row(
children: <Widget>[
Image.asset('image'),
Text('text'),
],
),
);
對應(yīng)三棵樹的結(jié)構(gòu)如下圖
了解了這三棵樹,我們再來看下頁面刷新的時候具體做了哪些操作
當(dāng)需要更新UI
的時候,Framework通知Engine
,Engine會等到下個Vsync信號
到達(dá)的時候,會通知Framework進(jìn)行animate、build、layout、paint
,最后生成layer
提交給Engine
。Engine會把layer進(jìn)行組合,生成紋理
,最后通過Open GL
接口提交數(shù)據(jù)給GPU
, GPU經(jīng)過處理后在顯示器上面顯示,如下圖所示:
結(jié)合前面的例子,如果text文本或者image內(nèi)容發(fā)生變化會觸發(fā)哪些操作呢?
Widget
是不可改變
,需要重新創(chuàng)建一顆新樹
,build開始,然后對上一幀的Element樹
做遍歷,調(diào)用他的updateChild
,看子節(jié)點類型跟之前是不是一樣,不一樣
的話就把子節(jié)點扔掉
,創(chuàng)造
一個新的
,一樣
的話就做內(nèi)容更新
。對renderObject
做updateRenderObject
操作,updateRenderObject
內(nèi)部實現(xiàn)會判斷現(xiàn)在的節(jié)點跟上一幀是不是有改動
,有改動才會標(biāo)記dirty
,重新layout、paint
,再生成新的layer
交給GPU
,流程如下圖:
性能分析工具及方法
下面來看下性能分析工具,注意,統(tǒng)計性能數(shù)據(jù)一定要在真機(jī)+profile模式
下運行,拿到最接近真實的體驗數(shù)據(jù)。
performance overlay
平時常用的性能分析工具有performance overlay
,通過它可以直觀看到當(dāng)前幀的耗時,但是它是UI線程
和GPU線程``分開展示
的,UI Task Runner
是Flutter Engine
用于執(zhí)行Dart root isolate
代碼,GPU Task Runner
被用于執(zhí)行設(shè)備GPU
的相關(guān)調(diào)用。綠色
的線表示當(dāng)前幀
,出現(xiàn)紅色
則表示耗時超過16.6ms
,也就是發(fā)生丟幀
現(xiàn)象
Dart DevTool
另一個工具是Dart DevTool ,就是早期的Observatory,官方提供的性能檢測工具。它的 timeline 界面可以讓逐幀分析應(yīng)用的 UI 性能。但是目前還是預(yù)覽版,存在一些問題。
profile模式下運行起來,點擊android studio底部的菜單按鈕,會彈出一個網(wǎng)頁
點擊頂部的Timeline菜單
這個時候滑動頁面,每一幀的耗時會以柱形bar
的形式顯示在頁面上,每條bar
代表一個frame
,同時用不同顏色區(qū)分UI/GPU
線程耗時,這個時候我們要分析卡頓的場景就需要選中一條紅色的bar(總耗時超過16.6ms)
,中間區(qū)域的Frame events chart
顯示了當(dāng)前選中的frame的事件跟蹤
,UI
和GPU
事件是獨立
的事件流,但它們共享
一個公共的時間軸
。
選中Frame events chart
中的某個事件,以上圖為例Layout耗時最長,我們選中它,會在底部Flame chart
區(qū)域顯示一個自頂向下
的堆棧跟蹤
,每個堆棧幀的寬度
表示它消耗CPU的時長
,消耗大量CPU時長的堆棧是我們首要分析的重點,后面就是具體分析堆棧,定位卡頓問題。
debug調(diào)試工具
另外還有一些debug調(diào)試工具
可以輔助查看更多信息,注意,只能在debug模式
下使用分析,拿到的數(shù)據(jù)不能作為性能標(biāo)準(zhǔn)
debugProfileBuildsEnabled
:向 Timeline 事件中添加
每個widget
的build 信息
debugProfilePaintsEnabled
: 向 timeline 事件中添加
每個renderObject
的paint 信息
debugPaintLayerBordersEnabled
:每個layer
會出現(xiàn)一個邊框
,幫助區(qū)分layer層級
debugPrintRebuildDirtyWidgets
:打印標(biāo)記
為dirty
的widgets
debugPrintLayouts
:打印標(biāo)記
為dirty
的renderObjects
debugPrintBeginFrameBanner/debugPrintEndFrameBanne
r:打印每幀開始
和結(jié)束
實例分析
了解這些工具下面我們來看個簡單的demo具體分析下,一個由Column、Container、ListView嵌套的布局,其中有個定時器控制Text中顯示的文本實時更新
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class TestDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _TestDemoState();
}
}
class _TestDemoState extends State<TestDemo> {
int _count = 0;
Timer _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(milliseconds: 1000), (t) {
setState(() {
_count++;
});
});
}
@override
void dispose() {
if (_timer != null) {
if (_timer.isActive) {
_timer.cancel();
}
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Test Demo"),
),
body: content()
);
}
Widget content(){
Widget result = Column(
children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(10,10,10,5),
height: 100,
color: Color(0xff1fbfbf),
),
Container(
margin: EdgeInsets.fromLTRB(10,5,10,10),
height: 100,
color: Color(0xff1b8bdf),
),
Container(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 5,
itemBuilder: (context, index) {
return Container(
width: 70,
height: 70,
child: Image.asset(
'common.png',
width: 50,
height: 50,
),
);
}),
),
Container(
margin: EdgeInsets.fromLTRB(10,20,10,10),
height: 100,
width: 350,
color: Colors.yellow,
child: Center(
child:
Text(
_count.toString(),
style: TextStyle(fontSize: 18, fontWeight:FontWeight.bold),
),
)
),
],
);
return result;
}
}
大部分widget都是靜態(tài)的,只有黃色Container中包含一個內(nèi)容一直刷新的Text,這個時候我們打開debugProfileBuildsEnabled,用Timeline分析下它的渲染耗時,可以通過Frame events chart看到顯示的build層級非常深
結(jié)合第一部分渲染原理我們了解到,每次定時器刷新text數(shù)字
的時候,整個頁面widget樹
都會重新build
,但其實只有最底層Container中的Text內(nèi)容在改變,沒有必要刷新整顆樹,所以這里我們的優(yōu)化方案是提高build效率
,降低Widget tree遍歷的出發(fā)點
,將setState
刷新數(shù)據(jù)盡量下發(fā)到底層節(jié)點
,所以將Text單獨
抽取成獨立的Widget
,setState下發(fā)到抽取出的Widget內(nèi)部
class _TestDemoState extends State<TestDemo> {
...
Widget content(){
Widget result = Column(
children: <Widget>[
...
Container(
margin: EdgeInsets.fromLTRB(10,20,10,10),
height: 100,
width: 350,
color: Colors.yellow,
child: Center(
child:
CountText()
)
),
],
);
return result;
}
}
class CountText extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _CountTextState();
}
}
class _CountTextState extends State<CountText> {
int _count = 0;
Timer _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(milliseconds: 1000), (t) {
setState(() {
_count++;
});
});
}
@override
void dispose() {
if (_timer != null) {
if (_timer.isActive) {
_timer.cancel();
}
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text(
_count.toString(),
style: TextStyle(fontSize: 18, fontWeight:FontWeight.bold),
);
}
}
修改后的Timeline顯示如下圖:
可以看到build層級
明顯減少
,總耗時
也明顯降低
接下來分析下Paint過程
有沒有可以優(yōu)化的部分,我們打開debugProfilePaintsEnabled
變量分析可以看到Timeline顯示的paint層級
通過debugPaintLayerBordersEnabled = true;
顯示layer邊框可以看到不斷變化的Text和其他Widget都是在同一個layer中的,這里我們想到的優(yōu)化點是利用RepaintBoundary提高paint效率,它為經(jīng)常發(fā)生顯示變化的內(nèi)容提供一個新的隔離layer,新的layer paint不會影響到其他layer
RepaintBoundary(
child: Container(
margin: EdgeInsets.fromLTRB(10,20,10,10),
height: 100,
width: 350,
color: Colors.yellow,
child: Center(
child: CountText()
)
),
)
優(yōu)化后的效果如下:
可以看到我們?yōu)辄S色的Container建立了單獨的layer,并且paint的層級減少很多。
總結(jié)常見問題
提高build效率
,setState刷新數(shù)據(jù)盡量下發(fā)到底層節(jié)點提高paint效率
,RepaintBoundry創(chuàng)建單獨layer,減少重繪區(qū)域減少build中邏輯處理
,因為widget在頁面刷新的過程中隨時會通過build重建,build調(diào)用頻繁,我們應(yīng)該只處理跟UI相關(guān)的邏輯減少saveLayer(ShaderMask、ColorFilter、Text Overflow)、clipPath的使用
,saveLayer會在GPU中分配一塊新的繪圖緩沖區(qū),切換繪圖目標(biāo),這個操作是在GPU中非常耗時的,clipPath會影響每個繪圖指令,將相交操作之外的部分剔除掉,所以這也是個耗時操作減少Opacity Widget 使用
,尤其是在動畫中,因為他會導(dǎo)致widget每一幀都會被重建,可以用AnimatedOpacity
或FadeInImage
進(jìn)行代替