深入學習Flutter的運行機制

關注個人簡介入撒,面試不迷路~

main入口啟動

  • Flutter的主入口在"lib/main.dart"的main()函數(shù)中轻黑。在Flutter應用中藐窄,main()函數(shù)最簡單的實現(xiàn)如下:
    void main() {
      runApp(MyApp());
    }
    
  • 可以看到main()函數(shù)只調(diào)用了一個runApp()方法遮精,runApp()方法中都做了什么:
    void runApp(Widget app) {
      //初始化操作
      WidgetsFlutterBinding.ensureInitialized()
        //頁面渲染
        ..attachRootWidget(app)
        ..scheduleWarmUpFrame();
    }
    
*   參數(shù)`app`是一個Widget移迫,是Flutter應用啟動后要展示的第一個Widget旺嬉。
*   `WidgetsFlutterBinding`正是綁定widget 框架和Flutter engine的橋梁。
  • ensureInitialized()方法
    class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
      static WidgetsBinding ensureInitialized() {
        if (WidgetsBinding.instance == null)
          WidgetsFlutterBinding();
        return WidgetsBinding.instance;
      }
    }
    
*   可以看到`WidgetsFlutterBinding`繼承自`BindingBase` 并混入了很多`Binding`厨埋,在介紹`Binding`之前先介紹一下`Window`邪媳,`Window`的官方解釋:The most basic interface to the host operating system's user interface.
  • Window正是Flutter Framework連接宿主操作系統(tǒng)的接口〉聪荩看一下Window類的部分定義:
    class Window {
      // 當前設備的DPI雨效,即一個邏輯像素顯示多少物理像素,數(shù)字越大废赞,顯示效果就越精細保真徽龟。
      // DPI是設備屏幕的固件屬性,如Nexus 6的屏幕DPI為3.5 
      double get devicePixelRatio => _devicePixelRatio;
      // 繪制回調(diào)  
      VoidCallback get onDrawFrame => _onDrawFrame;
      // 發(fā)送平臺消息
      void sendPlatformMessage(String name,
                               ByteData data,
                               PlatformMessageResponseCallback callback) ;
      ... //其它屬性及回調(diào)
    }
    
*   `Window`類包含了當前設備和系統(tǒng)的一些信息以及Flutter Engine的一些回調(diào)“Φ兀現(xiàn)在回來看看`WidgetsFlutterBinding`混入的各種Binding据悔。通過查看這些 Binding的源碼,可以發(fā)現(xiàn)這些Binding中基本都是監(jiān)聽并處理`Window`對象的一些事件耘沼,然后將這些事件按照Framework的模型包裝极颓、抽象然后分發(fā)∪亨停可以看到`WidgetsFlutterBinding`正是粘連Flutter engine與上層Framework的“膠水”菠隆。
  • 看看attachToRenderTree的源碼實現(xiàn):
    RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
      if (element == null) {
        ...// 代碼處理
      } else {
        ...// 代碼處理
      }
      return element;
    }
    
  • 該方法負責創(chuàng)建根element,即 RenderObjectToWidgetElement,并且將element與widget 進行關聯(lián)骇径,即創(chuàng)建出 widget樹對應的element樹躯肌。
    • 如果element 已經(jīng)創(chuàng)建過了,則將根element 中關聯(lián)的widget 設為新的既峡,由此可以看出element 只會創(chuàng)建一次羡榴,后面會進行復用。那么BuildOwner是什么呢运敢?其實他就是widget framework的管理類校仑,它跟蹤哪些widget需要重新構建。

頁面渲染

  • 回到runApp的實現(xiàn)中传惠,當調(diào)用完attachRootWidget后迄沫,最后一行會調(diào)用 WidgetsFlutterBinding 實例的 scheduleWarmUpFrame() 方法,該方法的實現(xiàn)在SchedulerBinding 中卦方,它被調(diào)用后會立即進行一次繪制(而不是等待"vsync"信號)羊瘩,在此次繪制結(jié)束前,該方法會鎖定事件分發(fā)盼砍,也就是說在本次繪制結(jié)束完成之前Flutter將不會響應各種事件尘吗,這可以保證在繪制過程中不會再觸發(fā)新的重繪。
  • 下面是scheduleWarmUpFrame() 方法的部分實現(xiàn)(省略了無關代碼):
    void scheduleWarmUpFrame() {
      Timer.run(() {
        handleBeginFrame(null); 
      });
      Timer.run(() {
        handleDrawFrame();  
        resetEpoch();
      });
      // 鎖定事件
      lockEvents(() async {
        await endOfFrame;
        Timeline.finishSync();
      });
     ...
    }
    
*   可以看到該方法中主要調(diào)用了`handleBeginFrame()` 和 `handleDrawFrame()` 兩個方法浇坐,在看這兩個方法之前我們首先了解一下Frame 和 FrameCallback 的概念:
*   Frame: 一次繪制過程睬捶,我們稱其為一幀。Flutter engine受顯示器垂直同步信號"VSync"的驅(qū)使不斷的觸發(fā)繪制近刘。我們之前說的Flutter可以實現(xiàn)60fps(Frame Per-Second)擒贸,就是指一秒鐘可以觸發(fā)60次重繪,F(xiàn)PS值越大觉渴,界面就越流暢介劫。
*   FrameCallback:`SchedulerBinding` 類中有三個FrameCallback回調(diào)隊列, 在一次繪制過程中案淋,這三個回調(diào)隊列會放在不同時機被執(zhí)行:
    *   1.  `transientCallbacks`:用于存放一些臨時回調(diào)座韵,一般存放動畫回調(diào)√呔可以通過`SchedulerBinding.instance.scheduleFrameCallback` 添加回調(diào)回右。
    *   2.  `persistentCallbacks`:用于存放一些持久的回調(diào),不能在此類回調(diào)中再請求新的繪制幀漱挚,持久回調(diào)一經(jīng)注冊則不能移除。`SchedulerBinding.instance.addPersitentFrameCallback()`渺氧,這個回調(diào)中處理了布局與繪制工作旨涝。
    *   3.  `postFrameCallbacks`:在Frame結(jié)束時只會被調(diào)用一次,調(diào)用后會被系統(tǒng)移除,可由 `SchedulerBinding.instance.addPostFrameCallback()` 注冊白华,注意慨默,不要在此類回調(diào)中再觸發(fā)新的Frame,這可以會導致循環(huán)刷新弧腥。
*   自行查看`handleBeginFrame()` 和 `handleDrawFrame()` 兩個方法的源碼厦取,可以發(fā)現(xiàn)前者主要是執(zhí)行了`transientCallbacks`隊列,而后者執(zhí)行了 `persistentCallbacks` 和 `postFrameCallbacks` 隊列管搪。

頁面繪制

  • 渲染和繪制邏輯在RendererBinding中實現(xiàn)虾攻,查看其源碼,發(fā)現(xiàn)在其initInstances()方法中有如下代碼:
    void initInstances() {
      ... //省略無關代碼
          
      //監(jiān)聽Window對象的事件  
      ui.window
        ..onMetricsChanged = handleMetricsChanged
        ..onTextScaleFactorChanged = handleTextScaleFactorChanged
        ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
        ..onSemanticsAction = _handleSemanticsAction;
       
      //添加PersistentFrameCallback    
      addPersistentFrameCallback(_handlePersistentFrameCallback);
    }
    
*   看最后一行更鲁,通過`addPersistentFrameCallback` 向`persistentCallbacks`隊列添加了一個回調(diào) `_handlePersistentFrameCallback`:

```
void _handlePersistentFrameCallback(Duration timeStamp) {
  drawFrame();
}

```
  • 該方法直接調(diào)用了RendererBindingdrawFrame()方法

flushLayout()

  • 代碼如下所示
    void flushLayout() {
       ...
        while (_nodesNeedingLayout.isNotEmpty) {
          final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
          _nodesNeedingLayout = <RenderObject>[];
          for (RenderObject node in 
               dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
            if (node._needsLayout && node.owner == this)
              node._layoutWithoutResize();
          }
        }
      } 
    }
    
  • 源碼很簡單霎箍,該方法主要任務是更新了所有被標記為“dirty”的RenderObject的布局信息。主要的動作發(fā)生在node._layoutWithoutResize()方法中澡为,該方法中會調(diào)用performLayout()進行重新布局漂坏。

flushCompositingBits()

  • 代碼如下所示
    void flushCompositingBits() {
      _nodesNeedingCompositingBitsUpdate.sort(
          (RenderObject a, RenderObject b) => a.depth - b.depth
      );
      for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
        if (node._needsCompositingBitsUpdate && node.owner == this)
          node._updateCompositingBits(); //更新RenderObject.needsCompositing屬性值
      }
      _nodesNeedingCompositingBitsUpdate.clear();
    }
    
  • 檢查RenderObject是否需要重繪,然后更新RenderObject.needsCompositing屬性媒至,如果該屬性值被標記為true則需要重繪顶别。

flushPaint()

  • 代碼如下所示
    void flushPaint() {
     ...
      try {
        final List<RenderObject> dirtyNodes = _nodesNeedingPaint; 
        _nodesNeedingPaint = <RenderObject>[];
        // 反向遍歷需要重繪的RenderObject
        for (RenderObject node in 
             dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
          if (node._needsPaint && node.owner == this) {
            if (node._layer.attached) {
              // 真正的繪制邏輯  
              PaintingContext.repaintCompositedChild(node);
            } else {
              node._skippedPaintingOnLayer();
            }
          }
        }
      } 
    }
    
*   該方法進行了最終的繪制,可以看出它不是重繪了所有 `RenderObject`拒啰,而是只重繪了需要重繪的 `RenderObject`驯绎。真正的繪制是通過`PaintingContext.repaintCompositedChild()`來繪制的,該方法最終會調(diào)用Flutter engine提供的Canvas API來完成繪制图呢。

compositeFrame()

  • 代碼如下所示
    void compositeFrame() {
      ...
      try {
        final ui.SceneBuilder builder = ui.SceneBuilder();
        final ui.Scene scene = layer.buildScene(builder);
        if (automaticSystemUiAdjustment)
          _updateSystemChrome();
        ui.window.render(scene); //調(diào)用Flutter engine的渲染API
        scene.dispose(); 
      } finally {
        Timeline.finishSync();
      }
    }
    
  • 這個方法中有一個Scene對象条篷,Scene對象是一個數(shù)據(jù)結(jié)構,保存最終渲染后的像素信息蛤织。
    • 這個方法將Canvas畫好的Scene傳給window.render()方法赴叹,該方法會直接將scene信息發(fā)送給Flutter engine,最終由engine將圖像畫在設備屏幕上指蚜。

最后

  • 需要注意:由于RendererBinding只是一個mixin乞巧,而with它的是WidgetsBinding,所以需要看看WidgetsBinding中是否重寫該方法摊鸡,查看WidgetsBindingdrawFrame()方法源碼:
    @override
    void drawFrame() {
     ...//省略無關代碼
      try {
        if (renderViewElement != null)
          buildOwner.buildScope(renderViewElement); 
        super.drawFrame(); //調(diào)用RendererBinding的drawFrame()方法
        buildOwner.finalizeTree();
      } 
    }
    
  • 發(fā)現(xiàn)在調(diào)用RendererBinding.drawFrame()方法前會調(diào)用 buildOwner.buildScope() (非首次繪制)绽媒,該方法會將被標記為“dirty” 的 element 進行 rebuild()
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末免猾,一起剝皮案震驚了整個濱河市是辕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌猎提,老刑警劉巖获三,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡疙教,警方通過查閱死者的電腦和手機棺聊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贞谓,“玉大人限佩,你說我怎么就攤上這事÷阆遥” “怎么了祟同?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長烁兰。 經(jīng)常有香客問我耐亏,道長,這世上最難降的妖魔是什么沪斟? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任广辰,我火速辦了婚禮,結(jié)果婚禮上主之,老公的妹妹穿的比我還像新娘择吊。我一直安慰自己,他們只是感情好槽奕,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布几睛。 她就那樣靜靜地躺著,像睡著了一般粤攒。 火紅的嫁衣襯著肌膚如雪所森。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天夯接,我揣著相機與錄音焕济,去河邊找鬼。 笑死盔几,一個胖子當著我的面吹牛晴弃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逊拍,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼上鞠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了芯丧?” 一聲冷哼從身側(cè)響起芍阎,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缨恒,沒想到半個月后谴咸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體度硝,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年寿冕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片椒袍。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡驼唱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出驹暑,到底是詐尸還是另有隱情玫恳,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布优俘,位于F島的核電站京办,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏帆焕。R本人自食惡果不足惜惭婿,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叶雹。 院中可真熱鬧财饥,春花似錦、人聲如沸折晦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽满着。三九已至谦炒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間风喇,已是汗流浹背宁改。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留响驴,地道東北人透且。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像豁鲤,于是被迫代替她去往敵國和親秽誊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

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