作者:騰訊NOW直播 - levinyang(楊亦偉)
前言
Flutter是谷歌的移動UI框架,可以快速在iOS和Android上構建高質量的原生使用者介面。 Flutter可以與現有的程式碼一起工作。在全世界,Flutter正在被越來越多的開發者和組織使用,並且Flutter是完全免費、開源的。本文主要講述Flutter整個渲染流程,重點關注在渲染過程中Framework層,從setState到向Engine提交Layer整個過程是怎麼樣實現的,讓讀者更加深入理解Flutter的渲染過程。
渲染框架
Flutter的框架分為Framework和Engine兩層,應用是基於Framework層開發的,Framework負責渲染中的Build,Layout,Paint,生成Layer等環節。Engine層是C++實現的渲染引擎,負責把Framework生成的Layer組合,生成紋理,然後通過Open GL介面向GPU提交渲染資料。
渲染過程
當需要更新UI的時候,Framework通知Engine,Engine會等到下個Vsync訊號到達的時候,會通知Framework,然後Framework會進行animations, build,layout,compositing,paint,最後生成layer提交給Engine。Engine會把layer進行組合,生成紋理,最後通過Open Gl介面提交資料給GPU, GPU經過處理後在顯示器上面顯示。整個流程如下圖:
從流程圖可以看出來,只有當有UI更新的才需要重新渲染,當然程式啟動的是預設去渲染的。渲染觸發
接下來我們先分析一下當有UI需要更新的時候,是怎麼樣觸發渲染,從應用到Framework,再到Engine這個過程是怎麼樣的。在Flutter開發應用的時候,當需要更新的UI的時候,需要呼叫一下setState方法,然後就可以實現了UI的更新,我們接下來分析一下該方法做哪些事情。
void setState(VoidCallback fn) {
...
_element.markNeedsBuild(); //通過相應的element來實現更新,關於element,widget,renderOjbect這裡不展開討論
}
void markNeedsBuild() {
...
if (dirty)
return;
_dirty = true;
owner.scheduleBuildFor(this);
}
void scheduleBuildFor(Element element) {
...
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled(); //這是一個callback,呼叫的方法是下面的_handleBuildScheduled
}
_dirtyElements.add(element); //把當前element新增到_dirtyElements陣列裡面,後面重新build會遍歷這個陣列
element._inDirtyList = true;
}
void _handleBuildScheduled() {
...
ensureVisualUpdate();
}
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks:
return;
}
}
void scheduleFrame() {
if (_hasScheduledFrame || !_framesEnabled)
return;
...
ui.window.scheduleFrame();
_hasScheduledFrame = true;
}
void scheduleFrame() native 'Window_scheduleFrame';//這個方法是Engine實現的,把介面暴露給Framework,呼叫這個方法通知引擎,需要更新UI,引擎會在下一個vSync的到達的時候通知Framework
複製程式碼
渲染過程
當應用呼叫setState後,經過Framework一連串的呼叫後,最終呼叫scheduleFrame來通知Engine需要更新UI,Engine就會在下個vSync到達的時候通過呼叫_drawFrame來通知Framework,然後Framework就會通過BuildOwner進行Build和PipelineOwner進行Layout,Paint,最後把生成Layer,組合成Scene提交給Engine。接下來我們從程式碼中分析一下,這些環節具體是怎麼樣實現的。首先從Engine回撥Framework的入口開始。
void _drawFrame() { //Engine回撥Framework入口
_invoke(window.onDrawFrame, window._onDrawFrameZone);
}
//初始化的時候把onDrawFrame設定為_handleDrawFrame
void initInstances() {
super.initInstances();
_instance = this;
ui.window.onBeginFrame = _handleBeginFrame;
ui.window.onDrawFrame = _handleDrawFrame;
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
}
void _handleDrawFrame() {
if (_ignoreNextEngineDrawFrame) {
_ignoreNextEngineDrawFrame = false;
return;
}
handleDrawFrame();
}
void handleDrawFrame() {
_schedulerPhase = SchedulerPhase.persistentCallbacks;//記錄當前更新UI的狀態
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
}
}
void initInstances() {
....
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
}
void drawFrame() {
...
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement); //先重新build widget
super.drawFrame();
buildOwner.finalizeTree();
}
void drawFrame() { //這個方法完成Layout,CompositingBits,Paint,生成Layer和提交給Engine的工作
assert(renderView != null);
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame(); //生成Layer並提交給Engine
pipelineOwner.flushSemantics();
}
複製程式碼
從上面程式碼分析得知,從Engine回撥,Framework會build,Layout,Paint,生成Layer等環節。接下來具體分析一下,這些環節是怎麼實現的。
Build
在Flutter應用開發中,無狀態的widget是通過StatelessWidget的build方法構建UI,有狀態的widget是通過State的build方法構建UI。現在具體分析一下從setState呼叫後到呼叫自定義State的build的流程是怎樣的(現在只分析有狀態的widget渲染過程)。
//這是官方的demo
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
//這裡就是構建UI,當呼叫setState後就會呼叫到這裡,重新生成新的widget
@override
Widget build(BuildContext context) {
return new Scaffold(
...
);
}
}
//從上面程式碼的分析到,在呼叫了setState後,最終會呼叫到buildScope來build
void buildScope(Element context, [VoidCallback callback]) {
...
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
...
_dirtyElements[index].rebuild();
index += 1;
}
for (Element element in _dirtyElements) {
assert(element._inDirtyList);
element._inDirtyList = false;
}
_dirtyElements.clear();
}
void rebuild() {
...
if (!_active || !_dirty)
return;
performRebuild();
}
void performRebuild() {
...
built = build();
...
}
Widget build() => widget.build(this);
複製程式碼
從上面可以看出,buildScope會遍歷_dirtyElements,對每個在陣列裡面的每個element呼叫rebuild,最終就是呼叫到相應的widget的build方法。 其實當setState的時候會把相應的element新增到_dirtyElements陣列裡,並且element標識dirty狀態。
Layout
在Flutter中應用中,是使用支援layout的widget來實現佈局的,支援layout的wiget有Container,Padding,Align等等,強大又簡易。在渲染流程中,在widget build後會進入layout環節,下面具體分析一下layout的實現,layout入口是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)) {//這裡是按照在node tree中的深度順序遍歷_nodesNeedingLayout,RenderObject的markNeedsLayout方法會把自己新增到_nodesNeedingLayout
if (node._needsLayout && node.owner == this)//對於需要layout的RenderObject進行layout
node._layoutWithoutResize();
}
}
...
}
void _layoutWithoutResize() {
...
performLayout(); //這個方法是計算layout的實現,不同layout widget有不同的實現
markNeedsSemanticsUpdate();
...
_needsLayout = false;
markNeedsPaint();
}
//這裡就是列出來RenderView的計算佈局的實現方式,這個比較簡單,就是讀取配置裡面的大小,然後呼叫child的layout,其他widget layout的計算佈局的方式是非常繁瑣複雜的,可以自行分析程式碼
void performLayout() {
assert(_rootTransform != null);
_size = configuration.size;
assert(_size.isFinite);
if (child != null)
child.layout(new BoxConstraints.tight(_size));//呼叫child的layout
}
//這個方法parent呼叫child的layout的入口,parent會把限制傳給child,child根據限制來layout
void layout(Constraints constraints, { bool parentUsesSize: false }) {
...
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
_constraints = constraints;
_relayoutBoundary = relayoutBoundary;
if (sizedByParent) {
performResize();
}
RenderObject debugPreviousActiveLayout;
performLayout();//實際計算layout的實現
markNeedsSemanticsUpdate();
_needsLayout = false;
markNeedsPaint();
}
void performResize() {
...
size = constraints.biggest;
switch (axis) {
case Axis.vertical:
offset.applyViewportDimension(size.height);
break;
case Axis.horizontal:
offset.applyViewportDimension(size.width);
break;
}
}
//這是標記為layout為dirty,把自己新增到渲染管道(PipelineOwner)裡面
void markNeedsLayout() {
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
return true;
}());
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
複製程式碼
從上面分析出來,layout的整個過程,首先是當RenderOjbect需要重新layout的時候,把自己新增到渲染管道里面,然後再觸發渲染到了layout環節,先從渲染管道里面遍歷找出需要渲染的RenderObject,然後呼叫performLayout進行計算layout,而且不同的物件實現不同的performLayout方法,計算layout的方式也不一樣,然後再呼叫child 的layout入口,同時把parent的限制也傳給child,child呼叫自己的performLayout。
Paint
當需要描繪自定義的影象的時候,可以通過繼承CustomPainter,實現paint方法,然後在paint方法裡面使用Flutter提供介面可以實現複雜的影象。 下面具體分析一下paint流程是怎麼實現的。
//這是官方的paint demo
class Sky extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
var rect = Offset.zero & size;
var gradient = new RadialGradient(
center: const Alignment(0.7, -0.6),
radius: 0.2,
colors: [const Color(0xFFFFFF00), const Color(0xFF0099FF)],
stops: [0.4, 1.0],
);
canvas.drawRect(
rect,
new Paint()..shader = gradient.createShader(rect),
);
}
@override
bool shouldRepaint(Sky oldDelegate) => false;
}
//這是在渲染管道中paint的入口
void flushPaint() {
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)) { //這是實現的方式和layout過程基本類似,不過排序是反序的
assert(node._layer != null);
if (node._needsPaint && node.owner == this) {
if (node._layer.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
}
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent: false }) {
...
if (child._layer == null) {
child._layer = new OffsetLayer();
} else {
child._layer.removeAllChildren();
}
final PaintingContext childContext = new PaintingContext._(child._layer, child.paintBounds); //通過layer生成 painting context
child._paintWithContext(childContext, Offset.zero);
childContext._stopRecordingIfNeeded();
}
void _paintWithContext(PaintingContext context, Offset offset) {
...
paint(context, offset);
...
}
void paint(PaintingContext context, Offset offset) {
if (_painter != null) { //只有持有CustomPainter情況下,才繼續往下呼叫自定義的CustomPainter的paint方法,把canvas傳過去
_paintWithPainter(context.canvas, offset, _painter);
_setRasterCacheHints(context);
}
super.paint(context, offset); //呼叫父類的paint的方法
if (_foregroundPainter != null) {
_paintWithPainter(context.canvas, offset, _foregroundPainter);
_setRasterCacheHints(context);
}
}
//super paint 在父類的paint裡面繼續呼叫child的paint,實現父子遍歷
void paint(PaintingContext context, Offset offset) {
if (child != null){
context.paintChild(child, offset);
}
void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) {
int debugPreviousCanvasSaveCount;
canvas.save();
if (offset != Offset.zero)
canvas.translate(offset.dx, offset.dy);
painter.paint(canvas, size);//,在呼叫paint的時候,經過一串的轉換後,layer->PaintingContext->Canvas,最終paint就是描繪在Canvas上
...
canvas.restore();
}
複製程式碼
總結來說,paint過程中,渲染管道中首先找出需要重繪的RenderObject,然後如果有實現了CustomPainter,就是呼叫CustomPainter paint方法,再去呼叫child的paint方法。
Composited Layer
void compositeFrame() {
Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);
try {
final ui.SceneBuilder builder = new ui.SceneBuilder();
layer.addToScene(builder, Offset.zero);
final ui.Scene scene = builder.build();
ui.window.render(scene);
scene.dispose();
assert(() {
if (debugRepaintRainbowEnabled || debugRepaintTextRainbowEnabled)
debugCurrentRepaintColor = debugCurrentRepaintColor.withHue(debugCurrentRepaintColor.hue + 2.0);
return true;
}());
} finally {
Timeline.finishSync();
}
}
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
addChildrenToScene(builder, offset + layerOffset);
}
void addChildrenToScene(ui.SceneBuilder builder, Offset childOffset) {
Layer child = firstChild;
while (child != null) {
child.addToScene(builder, childOffset);
child = child.nextSibling;
}
}
複製程式碼
Composited Layer就是把所有layer組合成Scene,然後通過ui.window.render方法,把scene提交給Engine,到這一步,Framework向Engine提交資料基本完成了。Engine會把所有的layer根據大小,層級,透明度計算出最終的顯示效果,通過Openg Gl介面渲染到螢幕上。
總結
本文結合Flutter的官方描繪的框架和渲染流程,簡要介紹了渲染的過程實現方式,讓讀者對Flutter在渲染方面有基本的理解,便於以後的開發和探索。Now直播終端團隊致力於為Flutter生態作出一點自己的貢獻,期待Flutter越來越好!