復(fù)雜業(yè)務(wù)如何保證Flutter的高性能高流暢度

Flutter渲染原理簡介

優(yōu)化之前我們先來介紹下Flutter的渲染原理,通過這部分基礎(chǔ)了解渲染流程以及主要耗時花費

flutter視圖樹包含了三顆樹:Widget、Element、RenderObject

  • Widget: 存放渲染內(nèi)容,它只是一個配置數(shù)據(jù)結(jié)構(gòu),創(chuàng)建是非常輕量的,在頁面刷新的過程中隨時會重建

  • Element: 同時持有WidgetRenderObject,存放上下文信息,通過它來遍歷視圖樹,支撐UI結(jié)構(gòu)

  • RenderObject: 根據(jù)Widget的布局屬性進(jìn)行layoutpaint ,負(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)如下圖

image

了解了這三棵樹,我們再來看下頁面刷新的時候具體做了哪些操作

當(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)過處理后在顯示器上面顯示,如下圖所示:

image

結(jié)合前面的例子,如果text文本或者image內(nèi)容發(fā)生變化會觸發(fā)哪些操作呢?

Widget不可改變,需要重新創(chuàng)建一顆新樹,build開始,然后對上一幀的Element樹做遍歷,調(diào)用他的updateChild,看子節(jié)點類型跟之前是不是一樣,不一樣的話就把子節(jié)點扔掉,創(chuàng)造一個新的,一樣的話就做內(nèi)容更新。對renderObjectupdateRenderObject操作,updateRenderObject內(nèi)部實現(xiàn)會判斷現(xiàn)在的節(jié)點跟上一幀是不是有改動,有改動才會標(biāo)記dirty,重新layout、paint,再生成新的layer交給GPU,流程如下圖:

image

性能分析工具及方法

下面來看下性能分析工具,注意,統(tǒng)計性能數(shù)據(jù)一定要在真機(jī)+profile模式下運行,拿到最接近真實的體驗數(shù)據(jù)。

performance overlay

平時常用的性能分析工具有performance overlay,通過它可以直觀看到當(dāng)前幀的耗時,但是它是UI線程GPU線程``分開展示的,UI Task RunnerFlutter Engine用于執(zhí)行Dart root isolate代碼,GPU Task Runner被用于執(zhí)行設(shè)備GPU的相關(guān)調(diào)用。綠色的線表示當(dāng)前幀,出現(xiàn)紅色則表示耗時超過16.6ms,也就是發(fā)生丟幀現(xiàn)象

image
Dart DevTool

另一個工具是Dart DevTool ,就是早期的Observatory,官方提供的性能檢測工具。它的 timeline 界面可以讓逐幀分析應(yīng)用的 UI 性能。但是目前還是預(yù)覽版,存在一些問題。

profile模式下運行起來,點擊android studio底部的菜單按鈕,會彈出一個網(wǎng)頁

image

點擊頂部的Timeline菜單

image

這個時候滑動頁面,每一幀的耗時會以柱形bar的形式顯示在頁面上,每條bar代表一個frame,同時用不同顏色區(qū)分UI/GPU線程耗時,這個時候我們要分析卡頓的場景就需要選中一條紅色的bar(總耗時超過16.6ms),中間區(qū)域的Frame events chart顯示了當(dāng)前選中的frame的事件跟蹤UIGPU事件是獨立的事件流,但它們共享一個公共的時間軸。

選中Frame events chart中的某個事件,以上圖為例Layout耗時最長,我們選中它,會在底部Flame chart區(qū)域顯示一個自頂向下堆棧跟蹤,每個堆棧幀的寬度表示它消耗CPU的時長,消耗大量CPU時長的堆棧是我們首要分析的重點,后面就是具體分析堆棧,定位卡頓問題。

debug調(diào)試工具

另外還有一些debug調(diào)試工具可以輔助查看更多信息,注意,只能在debug模式下使用分析,拿到的數(shù)據(jù)不能作為性能標(biāo)準(zhǔn)

  • debugProfileBuildsEnabled:向 Timeline 事件中添加每個widgetbuild 信息

  • debugProfilePaintsEnabled: 向 timeline 事件中添加每個renderObjectpaint 信息

  • debugPaintLayerBordersEnabled:每個layer會出現(xiàn)一個邊框,幫助區(qū)分layer層級

  • debugPrintRebuildDirtyWidgets:打印標(biāo)記dirtywidgets

  • debugPrintLayouts:打印標(biāo)記dirtyrenderObjects

  • debugPrintBeginFrameBanner/debugPrintEndFrameBanner:打印每幀開始結(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;
  }
}
image

大部分widget都是靜態(tài)的,只有黃色Container中包含一個內(nèi)容一直刷新的Text,這個時候我們打開debugProfileBuildsEnabled,用Timeline分析下它的渲染耗時,可以通過Frame events chart看到顯示的build層級非常深

image

結(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顯示如下圖:

image

可以看到build層級明顯減少,總耗時也明顯降低

接下來分析下Paint過程有沒有可以優(yōu)化的部分,我們打開debugProfilePaintsEnabled變量分析可以看到Timeline顯示的paint層級

image
image

通過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)化后的效果如下:

image
image

可以看到我們?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每一幀都會被重建,可以用 AnimatedOpacityFadeInImage 進(jìn)行代替

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容