Flutter Widget初始化-12

AidenCang發表於2019-12-04

在上一篇FlutterEngine引擎啟動的時候,我們已經分析了,從andorid端架子dart相關的程式碼和資源開發,分析FlutterEngine中的初始化過程,最終呼叫FlutterUI層的入口main()方法,在main()方法呼叫之前,我們就已經在FlutterEngine初始化時,架子了Window.Dart檔案,在執行main(),函式之後,就是把FlutterUI層產生的每一幀新增到FlutterEngine上,接下來我們會進一步分析FlutterEngine架子FlutterUI層程式碼,已經FlutterUI層的初始化邏輯

概要

核心類:

RenderObject:RenderBox實現類

Element:管理widget在樹中的邏輯關係

Wideget:配置每一個節點在的資料

BuildContext:提供的Element樹中進行查詢的使用者介面

Window:FlutterEngine提供給FlutterUI通訊的介面

RenderView:FlutterUI框架的第一個RenderObject,同時和window的Scene進行關聯

Layer:每一個渲染物件對應的一個邏輯表示

Scene:最終FlutterUI層經過一系列佈局、合成、彙總最終生成Scene提供給SkyEngine

RenderObjectToWidgetAdapter:FlutterUI的第一個Widget物件

BuildOwner:記錄Element樹中新增和刪除的節點、並且記錄那些Element需要重新計算

PipelineOwner:記錄RenderObject的在整棵樹中的關係和變化請求,最終通過呼叫PipelineOwner來遍歷整個渲染物件樹進行每一幀的邏輯處理

引擎繪製原理

在分析FlutterUI框架之前,先來分析一下Flutter引擎是怎麼用最少步驟把一幀繪製在螢幕上的

大象裝進冰箱裡(最大顆粒分解動作)

1.Flutter啟動時呼叫scheduleFrame()會觸發ui.window.onBeginFrame()回撥方法呼叫

2.sceneBuilder 生成場景

3.資料回傳到渲染引擎:ui.window.render(scene);

4.手勢回撥事件:ui.window.onPointerDataPacket = handlePointerDataPacket;,通過手勢事件來觸發不間斷的呼叫第三步驟中的方法

5.重新整理一幀:ui.window.scheduleFrame();

通過上面的三個步驟,就可以實現一幀一幀的繪製的螢幕上,主要的一個內容是如何管理Scene物件的資料變化處理邏輯,不斷的更新資料回傳到渲染引擎進行計算

官方Demo

Flutter Widget初始化-12

官方Demo 原始碼

ui.Picture paint(ui.Rect paintBounds) {
  final ui.PictureRecorder recorder = ui.PictureRecorder();
  final ui.Canvas canvas = ui.Canvas(recorder, paintBounds);
  final ui.Size size = paintBounds.size;
  canvas.drawCircle(
    size.center(ui.Offset.zero),
    size.shortestSide * 0.45,
    ui.Paint()..color = color,
  );
  return recorder.endRecording();
}

ui.Scene composite(ui.Picture picture, ui.Rect paintBounds) {
  final double devicePixelRatio = ui.window.devicePixelRatio;
  final Float64List deviceTransform = Float64List(16)
    ..[0] = devicePixelRatio
    ..[5] = devicePixelRatio
    ..[10] = 1.0
    ..[15] = 1.0;
  final ui.SceneBuilder sceneBuilder = ui.SceneBuilder()
    ..pushTransform(deviceTransform)
    ..addPicture(ui.Offset.zero, picture)
    ..pop();
  return sceneBuilder.build();
}

void beginFrame(Duration timeStamp) {
  final ui.Rect paintBounds = ui.Offset.zero & (ui.window.physicalSize / ui.window.devicePixelRatio);
  final ui.Picture picture = paint(paintBounds);
  final ui.Scene scene = composite(picture, paintBounds);
  ui.window.render(scene);
}

void handlePointerDataPacket(ui.PointerDataPacket packet) {
  for (ui.PointerData datum in packet.data) {
    if (datum.change == ui.PointerChange.down) {
      color = const ui.Color(0xFF0000FF);
      ui.window.scheduleFrame();
    } else if (datum.change == ui.PointerChange.up) {
      color = const ui.Color(0xFF00FF00);
      ui.window.scheduleFrame();
    }
  }
}

void main() {
  color = const ui.Color(0xFF00FF00);
  // The engine calls onBeginFrame whenever it wants us to produce a frame.
  ui.window.onBeginFrame = beginFrame;
  // The engine calls onPointerDataPacket whenever it had updated information
  // about the pointers directed at our app.
  ui.window.onPointerDataPacket = handlePointerDataPacket;
  // Here we kick off the whole process by asking the engine to schedule a new
  // frame. The engine will eventually call onBeginFrame when it is time for us
  // to actually produce the frame.
  ui.window.scheduleFrame();
}

複製程式碼

上面的邏輯來看,FlutterUI層最終的目的就是提供一幀內容傳遞個影象渲染引擎,不斷驅動,來修改Widget相關的屬性,從而改動FlutterRenderObject相關的資料,提供給FlutterEngine進行處理,最終渲染到平臺提供的View上

Flutter架構

核心問題點

通過上面的分析,我們知道Flutter引擎對UI層的繪製,最關心的是:

1.輸出的渲染物件

2.渲染物件如何改變

3.如何管理渲染物件的生成

4.如何管理樹中物件的變化

tree.png

Flutter Widget初始化-12

系統層初始化過程

Widget初始化過程

Flutter 層UI初始化過程,是在入口檔案中呼叫runApp把應用Widget和系統的渲染引擎進行關聯

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}
複製程式碼

上一篇文章中,我們已經介紹了Window事件是如何分解到不同的"BaseBind"物件的子類上,並進行初始化的,繼續分析使用者層的widget如何和系統成的Window框架進行關聯的。

attachRootWidget完成了RenderObject和Element進行繫結,也就是和Window物件關聯的第一個渲染物件,同時呼叫BuilderOwer來對螢幕上顯示的區域和那些需要進行重新繪製的Widget計算。BuilderOwer管理樹根到樹葉的Element物件,記錄那些是需要改變的(如果一棵樹有10個節點,那麼在第一次繪製完成只用,如果樹中的有幾個節點有改變,第二次構建渲染物件的過程中,從Root節點開始構建,那邊需要遍歷整棵樹才能找到其中那個是已經改變的節點,在Root渲染物件中使用BuilderOwer儲存每一個節點是否有變動,在進行下一次構建渲染物件的時候:1.管理Widget框架,2.管理沒有inactive的Element,3.管理整棵Widget樹的reassemble命令,build/layout/paint pipeline),分配BuilderOwer給整棵樹,通過RootRenderObjectElement.assignOwner

scheduleWarmUpFrame所有的資料準備好了之後,需要把一幀的資料傳遞到系統FlutterEngine中繼續渲染出來發展螢幕上。pipeline是對已經計算好的資料進行處理提交到FluuterEngine上進行渲染一個管理邏輯

以上邏輯主要做了兩個操作:

1.生成靜態資料的關聯物件:(只是儲存一下邏輯資料)

1:RenderView:對應系統層渲染框架的Surface物件的資料抽象,FlutterUI層的第一個渲染物件

2.Element: Widget的例項物件,Widget儲存了控制自身的資料結構型別,Element物件是對是Widget的一個抽象表示,用在告訴BuilderOwer如何配置Widget,Element的生命週期_ElementLifecycle {initial,active,inactive,defunct,},呼叫`attachRenderObject`把渲染物件掛載到渲染樹中

3.RenderObject:通過子類`RenderBox`對Element進行笛卡爾或者極座標進行邏輯佈局的座標系,提供繪圖層的Layer

4.BuildContext:提供了一個使用者在Widget訪問`RenderObject`的介面類

5.RenderObjectToWidgetAdapter:`RootRenderObjectElement`和`RenderView`管理在一起,根`RootRenderObjectElement`持有`RenderView`物件,那麼在Element和RenderObject物件就構成了一個整體,可以同步更新資料

6.ParentData:用來在`RenderObject`中對儲存資料,Widget是不可以被修改的,在Widget改變的時候,指定UI進行更新,把資料儲存在`RenderObject`中,rebuild之後`RenderObject`物件還存在,除非儲存資料的父節點不移除構建樹中。

7.RenderObject->Constraints描述子wiget能夠讀取的資料,不描述笛卡爾座標系和極座標
複製程式碼

2.動態資料:

1.buildOwner:從`RenderView`開始計算整個UI樹中顯示的是那一部分已經是哪一部分是需要更新,儲存相關的`List<Element>`物件

2.[PipelineOwner](../flutterdev/FlutterWidgetinit/#pipeline):每一個`Element`中都會有一個`RenderObject`,主要使用來處理渲染引擎在FlutterUI層的邏輯資料,上一步已經把需要更新的資料新增到`List<RenderObject>`

3.呼叫`RenderView`的`compositeFrame`方法把場景資料傳遞到系統渲染引擎
複製程式碼

RenderObject和Element關係

通過呼叫runApp中attachRootWidgetElementRenderObject進行關聯 RenderObjectToWidgetAdapterWidget的一個子類,主要實現兩個方法:

1.`createElement`
2.`createRenderObject`
複製程式碼

createElement主要的功能是對Widget進行管理,Element是Widget的一個例項物件,Elment有不同的子型別,Element作為樹中單一節點,管理RenderObject和Widget,Widget提供第一節點的資料配置Element,Element同時還管理過個節點在樹中的邏輯關係,多個Widget通過Element來串聯,構成一棵樹,Element作為一個圖紙的作用,相當於地圖功能,而真正可以提供個汽車同行的路,就是RenderObject的主要功能

createRenderObject主要功能是產生RenderObject的物件,主要功能是完成座標系統的構建,對Element提供配置引數,實現類是RenderBox,多個渲染物件的變化情況通過Pipeline進行記錄管理,在查詢RenderObject需要變化是就可以從Pipeline中進行查詢而不用遍歷整棵樹

每一個Element對應一個RenderObject物件,RenderObject作為Element的成員變數進行使用,多個Element進行關聯形成一棵樹,每一個Element節點上的RenderObject物件通過合成就可以提供到FlutterEngine上的每一幀 Element主要是實現邏輯功能,RenderObject實現真正的資料渲染功能

RenderObjectToWidgetAdapter的建構函式中的引數衝那裡來的?

renderView: WidgetsBinding 繼承 RendererBinding,在RendererBindinginitInstances方法中初始化renderView,系統surface,成員變數RenderBox是整個繪圖的Root 頂層RootRenderObjectElement,只用頂層物件使用BuildOwner

RenderObjectToWidgetAdapter 類作為Flutter系統頂層的UI,作為使用者UI和Flutter框架層的聯合點 在WidgetsBinding中呼叫attachRootWidget進行渲染物件和Element物件進行關聯,RenderView成為RootRenderObjectElement的成員變數,渲染物件和Element物件進行關聯,可以同步更新UI

attachRootWidget 完成三件事:

RenderObjectToWidgetAdapter: 關聯renderView和rootWidget

關聯PipelineOwner

關聯BuildOwner

void attachRootWidget(Widget rootWidget) {
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
  ).attachToRenderTree(buildOwner, renderViewElement);
}
複製程式碼

renderView物件

renderView物件是在RendererBinding中呼叫initRenderView物件進行初始化的,RenderView整合RenderObject實現RenderObjectWithChildMixin

RenderBox:是真正的渲染物件 RenderObject: 1.主要是儲存RenderBox的資料實際資料和child的Constraints關係,同時儲存對RenderBox操作的介面

2.儲存和Child相關關聯的資料ParentData

/// Creates a [RenderView] object to be the root of the
/// [RenderObject] rendering tree, and initializes it so that it
/// will be rendered when the engine is next ready to display a
/// frame.
///
/// Called automatically when the binding is created.
void initRenderView() {
  assert(renderView == null);
  renderView = RenderView(configuration: createViewConfiguration(), window: window);
  renderView.scheduleInitialFrame();
}
複製程式碼

ViewConfiguration

配置FlutterUI框架第一個渲染物件顯示在螢幕上的大小

1.裝置畫素縮放比例

2.裝置的時間物理畫素

ViewConfiguration createViewConfiguration() {
  final double devicePixelRatio = window.devicePixelRatio;
  return ViewConfiguration(
    size: window.physicalSize / devicePixelRatio,
    devicePixelRatio: devicePixelRatio,
  );
}
複製程式碼

Window

BindingBase 物件初始化時,獲取ui.window物件最為和Flutter通訊的物件

ui.Window get window => ui.window;

scheduleInitialFrame

RenderView初始化完成之後,初始化標記當前的渲染物件的狀態

1.新增當前物件到Pipline中,標記為需要佈局_nodesNeedingLayout

2.初始化Layer物件,所有的RenderObject物件最終會合併到當前物件,並且傳遞個Window.render上

3.通知Pipline進行視覺化更新

void scheduleInitialFrame() {
  assert(owner != null);
  assert(_rootTransform == null);
  scheduleInitialLayout();
  scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
  assert(_rootTransform != null);
  owner.requestVisualUpdate();
}
複製程式碼

Layer

Layer 是所有RenderObject物件合成在一起最終傳遞到Window.render中

1.初始化一個Layer物件

2.繫結RootLayer和當前的渲染物件rootLayer.attach(this);,每一個renderObject物件持有RootLayer物件的引用

Layer _updateMatricesAndCreateNewRootLayer() {
  _rootTransform = configuration.toMatrix();
  final ContainerLayer rootLayer = TransformLayer(transform: _rootTransform);
  rootLayer.attach(this);
  assert(_rootTransform != null);
  return rootLayer;
}
複製程式碼

通過上面的初始化過程已經把渲染物件初始化完成,並且關聯自己到PipelineOwner物件中,詳細的資訊在後續介紹(owner._nodesNeedingLayout.add(this);),RenderView初始化話過去設定當前FlutterUI顯示在螢幕上的大小,和FlutterEngine層的物件進行關聯,可以把一幀的資料傳遞到Window物件的本地方法中

小結

RenderView物件進行初始化過程:

1.在RendererBinding物件初始化時呼叫initRenderView()初始化RootRenderView

2.獲取裝置的ViewConfiguration物件來配置FlutterUI在視窗上顯示的大小

3.獲取ui.window物件引用,FlutterUI層的幀可以合成到window.render方法上

4.建立一個Layer物件作為RootLayer,並且呼叫attch方法傳遞到每一個節點上的RenderObject物件上

使用者層的Widget初始化過程

初始化Widget

上面介紹了初始化系統框架成的渲染物件renderView,和記錄Widget變化資訊的PipelineOwner,真正執行測量、佈局、繪製、的整個過程,並且已經初始化不同的BaseBind物件的子類,分解了Window物件的事件

RenderObjectToWidgetAdapter.attachToRenderTree對Element進行建立 將這個Widget進行填充,並實際將結果[renderobject]設定為[容器]的子級。如果element為空,則此函式將建立一個新元素。否則,給定元素將有一個計劃好的更新以切換到此小部件。由[runapp]用於引導應用程式。 Only one [buildScope] can be active at a time.

呼叫attach物件是renderViewElement物件為null(可以自己打斷點除錯檢視),遞迴從樹的根節點進行遍歷,所有的樹中的Widget進行填充,父子、兄弟節點的依賴關係通過Element物件來儲存,每一個節點的配置資訊是通過Widget介面給提供操作整棵樹的介面,渲染RenderObject到RenderObjectToWidgetElement

初始化Element

在繫結渲染物件和跟Element之後進行,開始初始化Element物件

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
  if (element == null) {///頂級原生出手動呼叫createment(); RenderObjectToWidgetElement
    owner.lockState(() {
      element = createElement();
      assert(element != null);
      ///只用根節點會呼叫該方法,其他節點的Element不會為空
      element.assignOwner(owner);
    });
    owner.buildScope(element, () {
      element.mount(null, null);  ///開始遍歷樹處理每一個Widget
    });
  } else {
    element._newWidget = this;
    element.markNeedsBuild();
  }
  return element;
}

複製程式碼

createElement 方法建立rootElement物件,RenderObjectToWidgetElement,Element物件中有四個成員變數:

1.renderObject:當前Widget物件的RenderObject物件

2._newWidget:提供給開發者控制RenderObject的介面

3._child:如果當前Element物件有子節點,那麼使用當前變數儲存當前子節點引用

Element.mount,建立RenderObject

呼叫createElement()建立一個Element物件的時候,Element物件是孤立存在的,父Element物件是不負責處理掛載,通過子節點的Element物件mount方法執行兩個操作:

1.呼叫super.mount(),通知父Element把自己新增在整棵樹中,並且建立渲染物件

2.更新子節點的Element物件,不斷重複上述過程

RenderObjectElement.mount()

在建立RenderObjectToWidgetElement物件的時候,構造方法中呼叫 RenderObjectToWidgetElement(RenderObjectToWidgetAdapter widget) : super(widget);,傳遞當前節點的widget物件,在呼叫mount方法時進行初始化操作,呼叫createRenderObject方法建立渲染物件,並且attachRenderObject(newSlot);物件到渲染樹中,Element已經建立完成,明確通過mount掛載自己到整個Element樹中,同時建立RenderObject物件,並且新增到渲染樹中,完成了當前節點的初始化,如果當前節點都有子節點,那麼就會初始化當前的節點的子節點。

@override
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  assert(() { _debugUpdateRenderObjectOwner(); return true; }());
  assert(_slot == newSlot);
  attachRenderObject(newSlot);
  _dirty = false;
}

複製程式碼

updateChild

從根節點開始,不斷的建立子Element物件,不斷的通過mount方法新增到Element樹中,通過updateChild初始化自己的子節點,在更新子節點是有一條件就是,當前的子節點是否要更新,是建立一個新的還是使用當前的節點複用

是否建立節點的核心演算法:

/// The following table summarizes the above:
///
/// |                     | **newWidget == null**  | **newWidget != null**   |
/// | :-----------------: | :--------------------- | :---------------------- |
/// |  **child == null**  |  Returns null.         |  Returns new [Element]. |
/// |  **child != null**  |  Old child is removed, returns null. | Old child updated if possible, returns child or new [Element]. |
複製程式碼
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  assert(() {
    if (newWidget != null && newWidget.key is GlobalKey) {
      final GlobalKey key = newWidget.key;
      key._debugReserveFor(this);
    }
    return true;
  }());
  if (newWidget == null) {
    if (child != null)
      deactivateChild(child);
    return null;
  }
  if (child != null) {
    if (child.widget == newWidget) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      return child;
    }
    if (Widget.canUpdate(child.widget, newWidget)) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      assert(child.widget == newWidget);
      assert(() {
        child.owner._debugElementWasRebuilt(child);
        return true;
      }());
      return child;
    }
    deactivateChild(child);
    assert(child._parent == null);
  }
  return inflateWidget(newWidget, newSlot);
}
複製程式碼

inflateWidget 真正建立Element的地方

為給定的小部件建立一個元素,並將其新增為給定槽中的元素。此方法通常由[updatechild]呼叫,但可以呼叫直接通過需要對建立進行更細粒度控制的子類元素。 如果給定的小部件有一個全域性鍵並且已經存在一個元素具有具有該全域性鍵的小部件,此函式將重用該元素可能從樹上的另一個位置嫁接或重新啟用(it from the list of inactive elements)而不是建立一個新元素。 此函式返回的元素已經被裝入將處於活動生命週期狀態。注:以上是對UI架構中使用到的邏輯繼續分析,主要是把整個UI框架的資料表示和關聯進行拆分和組合,能夠提供UI變化是資料的跟蹤和記錄(靜態資料儲存邏輯)

final Element newChild = newWidget.createElement();遍歷樹,為每一個Widget生成例項物件Element,呼叫mount方法把Elment新增到樹中

@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {
  assert(newWidget != null);
  final Key key = newWidget.key;
  if (key is GlobalKey) {
    final Element newChild = _retakeInactiveElement(key, newWidget);
    if (newChild != null) {
      assert(newChild._parent == null);
      assert(() { _debugCheckForCycles(newChild); return true; }());
      newChild._activateWithParent(this, newSlot);
      final Element updatedChild = updateChild(newChild, newWidget, newSlot);
      assert(newChild == updatedChild);
      return updatedChild;
    }
  }
  final Element newChild = newWidget.createElement();
  assert(() { _debugCheckForCycles(newChild); return true; }());
  newChild.mount(this, newSlot);
  assert(newChild._debugLifecycleState == _ElementLifecycle.active);
  return newChild;
}

複製程式碼

BuildOwner:

widgets框架的manager類。此類跟蹤需要重建的小部件,並處理其他任務作為一個整體應用於小部件樹,例如管理非活動元素列出樹並在必要時觸發“重新組合”命令除錯時熱重新載入。

  1. 通常由[widgetsbinding]擁有,並且是與build/layout/paint管道。可以生成其他生成所有者來管理螢幕外小部件樹。若要將生成所有者分配給樹

  2. 請使用的根元素上的[rootrenderobjectelement.assignowner]方法。

BuildOwner: 主要是記錄Element樹中的那些需要改變的渲染物件進行渲染,查詢有哪些Element是汙染了,需要進行從新佈局和繪製的,通過檢視BuildOwner物件的成員變數:

1.記錄整棵樹中新增或移除的資訊記錄

final _InactiveElements _inactiveElements = _InactiveElements();

2.記錄以及新增到樹中的Element物件那些Widget物件以及有改變了

final List _dirtyElements = [];

reassemble:自有在hotreload上呼叫

使根在給定[element]的整個子樹完全重建。當應用程式程式碼已更改並正在熱重新載入,以使小部件樹獲取任何更改了實現。這很昂貴,除非在開發期間,否則不應該呼叫它。 RendererBinding的initInstances方法中呼叫drawFrame

buildScope:

查詢在螢幕上顯示的區域的Element,確定哪些元素是要進行處理,畫布是一個沒有邊界的區域,所以在在繪製之前是需要進行確認哪些區域是需要進行處理的

void drawFrame() {
  .....

  try {
    if (renderViewElement != null)
    ///開始計算在螢幕上顯示一幀的範圍
      buildOwner.buildScope(renderViewElement);
    super.drawFrame();
    buildOwner.finalizeTree();
  } finally {
    。。。。。。
  }
  .......
}
複製程式碼

小結Widget初始化過程

通過上面的我們知道整個Widget初始化過程:

1.呼叫Widget物件的createElement方法建立一個當前節點的Element物件

2.呼叫Element物件的mount方法,

1.呼叫父類的mount方法,把自己新增到整棵Element物件中

2.在父類的mount方法中呼叫widget.createRenderObject建立當前節點的RenderOb物件

3.呼叫updateChild方法,遍歷所有的子節點的Widget物件,新增到Element樹中和RenderObject樹中
複製程式碼

3.WidgetsBinding的成員變數BuildOwner來記錄整個Element物件樹新增和移除,記錄已新增的Element有改變的資訊(提高遍歷效率,主要處理以及改變的物件)

到目前為止整個widget樹已經初始化完成,整個樹的配置資訊已經初始化完成,這一部分屬於靜態資料,在螢幕上看到的每一幀的資料是從這些靜態資料中收集計算出來的。

繪製第一幀

在呼叫main()時,初始化runApp(),載入整個FlutterUI框架,渲染當前的RenderObject到FlutterEngine中

  1. attachRootWidget:建立Element樹和RenderObject樹,同時建立了BuildOwner來跟蹤整個Element物件的變化情況,Pipline記錄RenderObject變化情況

2.RenderObject物件初始化完成,需要主動彙總在螢幕上scheduleWarmUpFrame,接下來分析一下第一幀是怎麼顯示在螢幕上的

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}

複製程式碼

觸發第一幀儘快執行,而不是等待引擎請求幀以響應系統“vsync”訊號。這在應用程式啟動期間使用,以便第一個幀(即可能很貴)得到一些額外的毫秒來執行。鎖定事件排程,直到排程的幀完成。 如果已經用[scheduleFrame]計劃了一個幀,或者[scheduleForcedFrame],此呼叫可能會延遲該幀。 如果任何計劃的幀已經開始,或者如果另一個已經呼叫了[schedulewarmupframe],此呼叫將被忽略。prefer[scheduleframe]在正常操作中更新顯示。 呼叫scheduleFrame觸發window物件處理第一幀資料window.scheduleFrame();,將會觸發window.onBeginFrame,開始執行,SchedulerBinding負責處理window物件的frame回撥事件

void scheduleWarmUpFrame() {
  if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle)
    return;
  _warmUpFrame = true;
  Timeline.startSync('Warm-up frame');
  final bool hadScheduledFrame = _hasScheduledFrame;
  Timer.run(() {
    assert(_warmUpFrame);
    handleBeginFrame(null);
  });
  Timer.run(() {
    assert(_warmUpFrame);
    handleDrawFrame();
    resetEpoch();
    _warmUpFrame = false;
    if (hadScheduledFrame)
      scheduleFrame();
  });
  lockEvents(() async {
    await endOfFrame;
    Timeline.finishSync();
  });
}
複製程式碼

handleBeginFrame

RendererBinding初始化initInstances時,addPersistentFrameCallback新增處理一幀的回撥函式_handlePersistentFrameCallback,上一步中呼叫scheduleFrame()觸發window.onDrawFrame回撥函式執行

handleBeginFrame方法預處理一幀到FlutterEngine中,

void handleBeginFrame(Duration rawTimeStamp) {
  Timeline.startSync('Frame', arguments: timelineWhitelistArguments);
  _firstRawTimeStampInEpoch ??= rawTimeStamp;
  _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);
  if (rawTimeStamp != null)
    _lastRawTimeStamp = rawTimeStamp;

。。。。。。。。

  assert(schedulerPhase == SchedulerPhase.idle);
  _hasScheduledFrame = false;
  try {
    // TRANSIENT FRAME CALLBACKS
    Timeline.startSync('Animate', arguments: timelineWhitelistArguments);
    _schedulerPhase = SchedulerPhase.transientCallbacks;
    final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
    _transientCallbacks = <int, _FrameCallbackEntry>{};
    callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
      if (!_removedIds.contains(id))
        _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
    });
    _removedIds.clear();
  } finally {
    _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
  }
}
複製程式碼

handleDrawFrame先呼叫一下FlutterUI層的變化資訊,在RendererBinding初始化是新增回撥方法到持久化回撥集合中

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

複製程式碼

drawFrame()

在下面的方法中呼叫回撥幀的方法,就是呼叫了drawFrame()方法繪製第一幀,上面分析的過程中,分析過PipLine是儲存了RenderObject物件的變化資訊,接下來就是呼叫PipLine進行RenderObject的查詢,佈局、合成、繪製等一系列操作,最終這邊好一幀,然後通知FlutterEngine已經準備好資料幀可以渲染了。。。。。

/// Called by the engine to produce a new frame.
///
/// This method is called immediately after [handleBeginFrame]. It calls all
/// the callbacks registered by [addPersistentFrameCallback], which typically
/// drive the rendering pipeline, and then calls the callbacks registered by
/// [addPostFrameCallback].
///
/// See [handleBeginFrame] for a discussion about debugging hooks that may be
/// useful when working with frame callbacks.
void handleDrawFrame() {
  assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
  Timeline.finishSync(); // end the "Animate" phase
  try {
    // PERSISTENT FRAME CALLBACKS
    _schedulerPhase = SchedulerPhase.persistentCallbacks;
    for (FrameCallback callback in _persistentCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp);

    // POST-FRAME CALLBACKS
    _schedulerPhase = SchedulerPhase.postFrameCallbacks;
    final List<FrameCallback> localPostFrameCallbacks =
        List<FrameCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();
    for (FrameCallback callback in localPostFrameCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp);
  } finally {
    _schedulerPhase = SchedulerPhase.idle;
    Timeline.finishSync(); // end the Frame
    assert(() {
      if (debugPrintEndFrameBanner)
        debugPrint('▀' * _debugBanner.length);
      _debugBanner = null;
      return true;
    }());
    _currentFrameTimeStamp = null;
  }
}
複製程式碼

Pipeline

PipelineOwner提供了一個介面,用於驅動PipelineOwner並儲存已請求訪問的渲染物件的狀態在管道的每個階段。要重新整理PipelineOwner,請呼叫以下命令函式順序:

1.[flushlayout]更新任何需要計算其佈局。在此階段中,每個渲染的大小和位置計算物件。渲染物件可能會弄髒其繪畫或此階段的合成狀態。

2.[flushcompositingbits]更新任何已髒的渲染物件合成位。在此階段中,每個渲染物件都將學習它的任何子級都需要合成。此資訊用於選擇如何實現視覺效果時的繪製階段,例如剪輯。如果渲染物件有合成子物件,則需要使用[層]建立剪輯以便剪輯應用於composited child(將被繪製到它自己的[layer])。

3.[flushpaint]訪問任何需要繪製的渲染物件。在此期間階段,渲染物件有機會將繪製命令記錄到[PictureLayer]和構造其他合成的[Layer]s。

4。最後,如果啟用了語義,[flushssemantics]將編譯呈現物件的語義。此語義資訊由提高渲染樹可訪問性的輔助技術。[renderBinding]儲存渲染物件的管道所有者在螢幕上可見。您可以建立其他管道所有者來管理off-screen物件,它可以獨立於螢幕上的渲染物件。

pipeline是在什麼時候開始呼叫的??

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}
複製程式碼

此方法由handleDrawFrame呼叫,它本身被呼叫在佈局和繪製框架時由引擎自動執行。每個幀由以下階段組成:

1.動畫階段:註冊的handlebeginframe方法window.onbeginframe,呼叫所有臨時幀回撥按註冊順序向scheduleFrameCallback註冊。這個包含正在驅動的所有例項objects,這意味著所有活動的animation物件點。

2.Microtasks:在handlebeginframe返回後,任何由臨時幀回撥安排的執行。這通常包括ticker和animationcontroller的期貨回撥完成此幀。在handlebeginframe之後,handledrawframe,註冊於呼叫了window.onDrawFrame,它呼叫所有的持久幀callbacks,其中最值得注意的是這個方法,drawFrame,它

3.佈局階段:系統中所有髒的renderobject都被放置out(參見renderObject.performlayout)。參見renderobject.markneedslayout有關將物件標記為髒的佈局的詳細資訊。

4.合成位階段:任何髒檔案上的合成位renderObject物件已更新。renderObject.markneedscompositingbitsupdate。

5.繪製階段:系統中所有髒的renderobject都是重新繪製(參見renderobject.paint)。這將生成Layer樹。renderbject.markneedspaint瞭解有關標記物件的詳細資訊油漆髒了。

6.合成階段:圖層樹變成一個場景傳送到GPU。7。語義階段:系統中所有髒的renderobject都有它們的語義已更新(請參見renderobject.semanticsannotator)。這個生成semanticsnode樹。湖renderbject.markneedssemanticsupdate瞭解有關標記語義的髒物件。有關步驟3-7的更多詳細資訊,請參見PipelineOwner。

8.定稿階段:在DrawFrame返回後,HandleDrawFrame然後呼叫後幀回撥(使用addPostFrameCallback註冊)。一些繫結(例如widgetsbinding)為此新增了額外的步驟list(例如,請參見widgetsbinding.drawframe)。

@protected
void drawFrame() {
  assert(renderView != null);
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  renderView.compositeFrame(); // this sends the bits to the GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
複製程式碼

flushLayout

PipelineOwner在執行的過程中把需要會在的RenderObject物件查詢出來儲存在_nodesNeedingLayout列表中,通過樹的深度來進行排序遍歷,呼叫_layoutWithoutResize方法進行佈局大小的改變,同時把需要繪製的RenderObject新增到_nodesNeedingPaint集合中便於下一個階段進行繪製,改變_needsPaint為true

void flushLayout() {
....
  try {
    // TODO(ianh): assert that we're not allowing previously dirty nodes to redirty themselves
    第一次建立RenderObject物件是,都是需要Layout的
    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();
      }
    }
  } finally {
  ...
  }
}

複製程式碼

在上面的分析過程中,我們已經知道,FlutterUI的第一個渲染物件是RenderView,那麼當佈局開始時當然是第一個呼叫的渲染物件(可以自己斷點除錯一下),RenderView物件繼承RenderView物件,執行RenderObject物件的_layoutWithoutResize呼叫RenderView的performLayout方法,開始從RootRenderObject開始繪製整個螢幕的渲染物件。

void _layoutWithoutResize() {
  assert(_relayoutBoundary == this);
  RenderObject debugPreviousActiveLayout;
  assert(!_debugMutationsLocked);
  assert(!_doingThisLayoutWithCallback);
  assert(_debugCanParentUseSize != null);
  assert(() {
    _debugMutationsLocked = true;
    _debugDoingThisLayout = true;
    debugPreviousActiveLayout = _debugActiveLayout;
    _debugActiveLayout = this;
    if (debugPrintLayouts)
      debugPrint('Laying out (without resize) $this');
    return true;
  }());
  try {
    performLayout();
    markNeedsSemanticsUpdate();
  } catch (e, stack) {
    _debugReportException('performLayout', e, stack);
  }
  assert(() {
    _debugActiveLayout = debugPreviousActiveLayout;
    _debugDoingThisLayout = false;
    _debugMutationsLocked = false;
    return true;
  }());
  _needsLayout = false;
  markNeedsPaint();
}

複製程式碼

如何計算渲染物件的擺放位置和自定義控制元件RenderView

計算此渲染物件的佈局。此方法是父控制元件要求子控制元件執行的主要入口點更新他們的佈局資訊。父物件傳遞約束物件它通知子級哪些佈局是允許的。這子控制元件是必須遵守給定的約束。如果父級讀取子級佈局期間計算的資訊,則“parentUseSSize”的父級必須傳遞true。在這種情況下,父母將:只要子項被標記為需要佈局,就被標記為需要佈局因為父級的佈局資訊依賴於子級的佈局資訊。如果父項使用預設值(false)“parentUseSSize”,子項可以更改其佈局資訊(根據給定的約束)而不通知父級。子類不應直接重寫[Layout]。相反,他們應該重寫[PerformResize]和/或[performLayout]。delegates將實際工作委託給[performResize]和[performLayout]。父級的[Performance Layout]方法應呼叫其所有無條件的子項。這是[佈局]方法的責任在此處實現)如果子項不需要執行任何操作,則提前返回更新其佈局資訊。

flushLayout呼叫node._layoutWithoutResize();進行渲染物件的位置計算RenderView中的performLayout進行layout進行處理

@override
  void performLayout() {
    assert(_rootTransform != null);
    _size = configuration.size;
    assert(_size.isFinite);

    if (child != null)
      child.layout(BoxConstraints.tight(_size));
  }
複製程式碼

flushCompositingBits

上一步中把相關的位置大小確認好之後,如果有多個控制元件有重疊的部分,需要進行合併,如何呼叫markNeedsPaint準備後續需要彙總的內容


void flushCompositingBits() {
  if (!kReleaseMode) {
    Timeline.startSync('Compositing bits');
  }
  _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
  for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
    if (node._needsCompositingBitsUpdate && node.owner == this)
      node._updateCompositingBits();
  }
  _nodesNeedingCompositingBitsUpdate.clear();
  if (!kReleaseMode) {
    Timeline.finishSync();
  }
}
複製程式碼

遍歷Layout的時候標記需要合成的渲染物件。呼叫渲染自身的引數來判斷是否呀合成,在下面的處理邏輯是有一個優化過程,在螢幕上擺放的Widget,有的是會有重疊和覆蓋,在覆蓋的情況下,如果被完全覆蓋掉,就沒有必要合成這些RenderObject物件,標記當前的渲染物件是需要繪製的。

void _updateCompositingBits() {
  if (!_needsCompositingBitsUpdate)
    return;
  final bool oldNeedsCompositing = _needsCompositing;
  _needsCompositing = false;
  visitChildren((RenderObject child) {
    child._updateCompositingBits();
    if (child.needsCompositing)
      _needsCompositing = true;
  });
  if (isRepaintBoundary || alwaysNeedsCompositing)
    _needsCompositing = true;
  if (oldNeedsCompositing != _needsCompositing)
    markNeedsPaint();
  _needsCompositingBitsUpdate = false;
}
複製程式碼

flushPaint

更新所有渲染物件的顯示列表。此函式是渲染管道的核心階段之一。在佈局之後和重新放置場景之前進行繪製,以便場景與每個渲染物件的最新顯示列表合成。有關如何使用此函式的示例,請參見[RenderBinding]。 通過佈局和合成兩步的計算操作,已經把需要彙總的資料計算出來,提供給畫筆物件把RenderObject物件繪製在layer上,Window物件中render(Scene scene)就是在為此函式準備資料


void flushPaint() {
  .....
  try {
    final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
    _nodesNeedingPaint = <RenderObject>[];
    // Sort the dirty nodes in reverse order (deepest first).
    ///從樹葉開始計算
    for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
      assert(node._layer != null);
      if (node._needsPaint && node.owner == this) {
        if (node._layer.attached) {
          ///此步驟是彙總的詳細過程,放在後面在進行解析
          PaintingContext.repaintCompositedChild(node);
        } else {
          node._skippedPaintingOnLayer();
        }
      }
    }
    assert(_nodesNeedingPaint.isEmpty);
  } finally {
......
  }
}
複製程式碼

Layer

每一個RenderObject物件在Layer對應,需要

static void _repaintCompositedChild(
  RenderObject child, {
  bool debugAlsoPaintedParent = false,
  PaintingContext childContext,
}) {
  OffsetLayer childLayer = child._layer;
  if (childLayer == null) {
    child._layer = childLayer = OffsetLayer();
  } else {
    childLayer.removeAllChildren();
  }
  childContext ??= PaintingContext(child._layer, child.paintBounds);
  child._paintWithContext(childContext, Offset.zero);
  childContext.stopRecordingIfNeeded();
}
複製程式碼

下圖是每一個渲染物件的一個抽象顯示,真正顯示的是需要從當前的的層合併為一幀,解析來就可以開始開始合併,每一個RenderObject物件的資料,把一個立體的連結串列拼接成為一個平面。

layer:三維展示

frame:二維資料 layer.png

Flutter Widget初始化-12

renderView.compositeFrame()

上一步中已經把需要的RenderObject進行了繪製,每一個RenderObject中都有一個ContainerLayer物件,作為每一個渲染物件的Layer,現在需要處理的是在Flutter已經做好了邏輯操作,需要把Dart物件轉換為渲染引擎能夠識別的物件:

Window物件中render(Scene scene)就是在為此函式準備資料,通過Navtive呼叫把資料傳遞到底層繪製

1.使用SceneBuilder物件來建立場景

2.把RenderObject物件中資料取出來構建Scene的原始資料

3.呼叫_window.render(scene);實現真正的資料傳遞到渲染引擎,呼叫sky引擎進行渲染

/// Uploads the composited layer tree to the engine.
///
/// Actually causes the output of the rendering pipeline to appear on screen.
void compositeFrame() {
  try {
    final ui.SceneBuilder builder = ui.SceneBuilder();
    final ui.Scene scene = layer.buildScene(builder);
    if (automaticSystemUiAdjustment)
      _updateSystemChrome();
    _window.render(scene);
    scene.dispose();
    assert(() {
      if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled)
        debugCurrentRepaintColor = debugCurrentRepaintColor.withHue((debugCurrentRepaintColor.hue + 2.0) % 360.0);
      return true;
    }());
  } finally {
  }
}
複製程式碼

scheduleFrame

上面分析了整個Scene資料的準備過程,在接下來的就是開始執行FlutterEngine進行渲染到平臺的UI上

void scheduleFrame() {
  if (_hasScheduledFrame || !_framesEnabled)
    return;
  assert(() {
    if (debugPrintScheduleFrameStacks)
      debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
    return true;
  }());
  window.scheduleFrame();
  _hasScheduledFrame = true;
}
複製程式碼

總結

1.通過上面的分析我們知道,Widget出現在螢幕上執行執行四個步驟就可以顯示。FlutterUI框架中對Widow類進行拆分,對不同的回撥事件進行不同的mixin類來進行處理

2.通過runApp把開發的Widget和系統框架層關聯:

1.Element和RenderObject物件進行繫結。

2.觸發系統繪製一幀

3.分別處理不同的實際功能BindingBaseGestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding

4.通過RenderObjectToWidgetAdapter物件來進行Element、RenderObject物件進行關聯,主要是處理Widget的資料和RenderView中的資料

5.Element管理Widget資料

6.RenderObject面向Flutter引擎介面,提供繪製UI資料的Layters

7.BuildContext提供了Widget中訪問RenderObject物件的介面

8.BuildOwer主要是Element查詢出那些Element是需要重新進行繪製的

9.Pipeline查詢RenderObject物件,處理那些事渲染物件是需要在Layer進行Layout、填充、繪製的

10.呼叫RenderView中的方法,把生成的資料呼叫window.render(scene)傳遞到渲染引擎

11.RenderObject是如何擺放在正確的位置上的

上面的分析過程是接著上一篇進行分析的,上一篇分析的是FlutterEngine:Run呼叫的過程,呼叫main()從而進入FlutterUI層開始初始化,接著是把UI層彙總好的一幀回傳到FlutterEngine中進行處理,下一篇我們接接著分析一下FlutterUI層是怎麼把一個UI介面傳遞到Android的SurfaceView進行顯示的。

···

相關文章