【淘系技术】超详解析Flutter渲染引擎_业务想创新,不了解底层原理怎么行?

context.paintChild(child, offset);
}

我们可以具体看看一个节点如何绘制的:

  1. 创建 Canvas。绘制时会通过PaintContex获取的Canvas进行,其内部会去创建一个PictureLayer,并通过ui.PictrureRecorder调用到C++层来创建一个Skia的SkPictureRecorder实例,再通过SkPictureRecorder创建SkCanvas,最后把这个SkCanvas返回给Dart层去使用.

//@rendering/object.dart
@override
Canvas get canvas {
if (_canvas == null)
_startRecording();
return _canvas;
}

void _startRecording() {
assert(!_isRecording);
_currentLayer = PictureLayer(estimatedBounds);
_recorder = ui.PictureRecorder();
_canvas = Canvas(_recorder);
_containerLayer.append(_currentLayer);
}

  1. 通 过Canvas 执行具体绘制。Dart 层拿到绑定了底层 SkCanvas 的对象后,用这 Canvas 进行具体的绘制操作,这些绘制命令会被底层的 SkPictureRecorder 记录下来。

  2. 结束绘制,准备上屏。绘制完毕时,会调 用Canvas 对象的 stopRecordingIfNeeded 函数,它会最后会去调用 到C++ 的 SkPictureRecorder 的 endRecording 接口来生成一个 Picture 对象,存储在 PictureLayer 中。

//@rendering/object.dart
void stopRecordingIfNeeded() {
if (!_isRecording)
return;
_currentLayer.picture = _recorder.endRecording();
_currentLayer = null;
_recorder = null;
_canvas = null;
}

这 Picture 对象对应 Skia 的 SkPicture 对象,存储这所有的绘制指令。有兴趣可以看一下 SkPicture 的官方说明。

所有的 Layer 绘制完成形成 LayerTree,在 renderView.compositeFrame() 中通过 SceneBuilder 把 Dart Layer 映射为 Flutter engine 中的 flow::Layer ,同时也会生成一颗 C++ 的 flow::LayerTree ,存储在 Scene 对象中,最后通过 Window 的 render 接口提交给 Flutter engine。

//@rendering/view.dart
void compositeFrame() {

final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
_window.render(scene);
scene.dispose();
}

在全部绘制操作完成后,在Flutter engine中就形成了一颗flow::LayerTree,应该是像下面的样子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这颗包含了所有绘制信息以及绘制指令的flow::LayerTree会通过window实例调用到Animator::Render后,最后在Shell::OnAnimatorDraw中提交给GPU线程,并进行光栅化操作,代码可以参考:

@shell/common/animator.cc/Animator::Render

@shell/common/shell.cc/Shell::OnAnimatorDraw

这里提一下flow这个模块,flow是一个基于skia的合成器,它可以基于渲染指令来生成像素数据。Flutter基于flow模块来操作Skia,进行光栅化以及合成。

图片纹理

前面讲线程模型的时候,我们提到过IO线程负责图片加载以及解码并且把解码后的数据上传到GPU生成纹理,这个纹理在后面光栅化过程中会用到,我们来看一下这部分的内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

UI 线程加载图片的时候,会在 IO 线程调用 InstantiateImageCodec* 函数调用到C++层来初始化图片解码库,通过 skia 的自带的解码库解码生成 bitmap 数据后,调用SkImage::MakeCrossContextFromPixmap来生成可以在多个线程共享的 SkImage,在 IO 线程中用它来生成 GPU 纹理。

//@flutter/lib/ui/painting/codec.cc
sk_sp MultiFrameCodec::GetNextFrameImage(
fml::WeakPtr resourceContext) {

// 如果resourceContext不为空,就会去创建一个SkImage,
// 并且这个SkImage是在resouceContext中的,
if (resourceContext) {
SkPixmap pixmap(bitmap.info(), bitmap.pixelRef()->pixels(),
bitmap.pixelRef()->rowBytes());
// This indicates that we do not want a “linear blending” decode.
sk_sp dstColorSpace = nullptr;
return SkImage::MakeCrossContextFromPixmap(resourceContext.get(), pixmap,
false, dstColorSpace.get());
} else {
// Defer decoding until time of draw later on the GPU thread. Can happen
// when GL operations are currently forbidden such as in the background
// on iOS.
return SkImage::MakeFromBitmap(bitmap);
}
}

我们知道,OpenGL的环境是线程不安全的,在一个线程生成的图片纹理,在另外一个线程里面是不能直接使用的。但由于上传纹理操作比较耗时,都放在GPU线程操作,会减低渲染性能。目前OpenGL中可以通过share context来支持这种多线程纹理上传的,所以目前flutter中是由IO线程做纹理上传,GPU线程负责使用纹理。

基本的操作就是在GPU线程创建一个EGLContextA,之后把EGLContextA传给IO线程,IO线程在通过EGLCreateContext在创建EGLContextB的时候,把EGLContextA作为shareContext的参数,这样EGLContextA和EGLContextB就可以共享纹理数据了。

具体相关的代码不一一列举了,可以参考:

@shell/platform/android/platformviewandroid.cc/CreateResourceContext

@shell/platform/android/androidsurfacegl.cc/ResourceContextMakeCurrent

@shell/platform/android/androidsurfacegl.cc/AndroidSurfaceGL

@shell/platform/android/androidsurfacegl.cc/SetNativeWindow

关于图片加载相关流程,可以参考这篇文章:TODO

光栅化与合成

把绘制指令转化为像素数据的过程称为光栅化,把各图层光栅化后的数据进行相关的叠加与特效相关的处理成为合成这是渲染后半段的主要工作。

前面也提到过,生成LayerTree后,会通过Window的Render接口把它提交到GPU线程去执行光栅化操作,大体流程如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1-4步,在UI线程执行,主要是通过Animator类把LayerTree提交到Pipeline对象的渲染队列,之后通过Shell把pipeline对象提交给GPU线程进行光栅化,不具体展开,代码在animator.cc&pipeline.h

5-6步,在GPU线程执行具体的光栅化操作。这部分主要分为两大块,一块是Surface的管理。一块是如何把Layer Tree里面的渲染指令绘制到之前创建的Surface中。

可以通过下图了解一下Flutter中的Surface,不同类型的Surface,对应不同的底层渲染API。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们以GPUSurfaceGL为例,在Flutter中,GPUSurfaceGL是对Skia GrContext的一个管理和封装,而GrContext是Skia用来管理GPU绘制的一个上下文,最终都是借助它来操作OpenGL的API进行相关的上屏操作。在引擎初始化时,当FlutterViewAndroid创建后,就会创建GPUSurfaceGL,在其构造函数中会同步创建Skia的GrContext。

光栅化主要是在函数Rasterizer::DrawToSurface中实现的:

//@shell/rasterizer.cc
RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) {
FML_DCHECK(surface_);

if (compositor_frame) {
//1.执行光栅化
RasterStatus raster_status = compositor_frame->Raster(layer_tree, false);
if (raster_status == RasterStatus::kFailed) {
return raster_status;
}
//2.合成
frame->Submit();
if (external_view_embedder != nullptr) {
external_view_embedder->SubmitFrame(surface_->GetContext());
}
//3.上屏
FireNextFrameCallbackIfPresent();

if (surface_->GetContext()) {
surface_->GetContext()->performDeferredCleanup(kSkiaCleanupExpiration);
}

return raster_status;
}

return RasterStatus::kFailed;
}

光栅化完成后,执行frame->Submit()进行合成。这会调用到下面的PresentSurface,来把offscreensurface中的内容转移到onscreencanvas中,最后通过GLContextPresent()上屏。

//@shell/GPU/gpu_surface_gl.cc
bool GPUSurfaceGL::PresentSurface(SkCanvas* canvas) {

if (offscreen_surface_ != nullptr) {
SkPaint paint;
SkCanvas* onscreen_canvas = onscreen_surface_->getCanvas();
onscreen_canvas->clear(SK_ColorTRANSPARENT);
// 1.转移offscreen surface的内容到onscreen canvas中
onscreen_canvas->drawImage(offscreen_surface_->makeImageSnapshot(), 0, 0,
&paint);
}
{
//2. flush 所有绘制命令
onscreen_surface_->getCanvas()->flush();
}
//3 上屏
if (!delegate_->GLContextPresent()) {
return false;
}

return true;
}

GLContextPresent接口代码如下,实际上是调用的EGL的eglSwapBuffers接口去显示图形缓冲区的内容。

//@shell/platform/android/android_surface_gl.cc
bool AndroidSurfaceGL::GLContextPresent() {
FML_DCHECK(onscreen_context_ && onscreen_context_->IsValid());
return onscreen_context_->SwapBuffers();
}

上面代码段中的onscreen_context是Flutter引擎初始化的时候,通过setNativeWindow获得。主要是把一个Android的SurfaceView组件对应的ANativeWindow指针传给EGL,EGL根据这个窗口,调用eglCreateWindowSurface和显示系统建立关联,之后通过这个窗口把渲染内容显示到屏幕上。

代码可以参考:

@shell/platform/android/androidsurfacegl.cc/AndroidSurfaceGL::SetNativeWindow

总结以上渲染后半段流程,就可以看到LayerTree中的渲染指令被光栅化,并绘制到SkSurface对应的Surface中。这个Surface是由AndroidSurfaceGL创建的一个offscreensurface。再通过PresentSurface操作,把offscreensurface的内容,交换到onscreen_surface中去,之后调用eglSwapSurfaces上屏,结束一帧的渲染。

探索

深入了解了Flutter引擎的渲染机制后,基于业务的诉求,我们也做了一些相关的探索,这里简单分享一下。

小程序渲染引擎

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基于Flutter engine,我们去除了原生的dart引擎,引入js引擎,用C++重写了Flutter Framework中的rendering,painting以及widget的核心逻辑,继续向上封装基础组件,实现cssom以及C++版的响应式框架,对外提供统一的JS Binding API,再向上对接小程序的DSL,供小程序业务方使用。对于性能要求比较高的小程序,可以选择使用这条链路进行渲染,线下我们跑通了星巴克小程序的UI渲染,并具备了很好的性能体验。
小程序互动渲染引擎

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
受限于小程序worker/render的架构,互动业务中频繁的绘制操作需要经过序列化/反序列化并把消息从worker发送到render去执行渲染命令。基于flutter engine,我们提供了一套独立的2d渲染引擎,引入canvas的渲染管线,提供标准的canvas API供业务直接在worker线程中使用,缩短渲染链路,提高性能。目前已经支持了相关的互动业务在线上运行,性能和稳定性表现很好。
总结与思考

本文着重分析了flutter engine的渲染流水线及其相关概念并简单分享了我们的一些探索。熟悉和了解渲染引擎的工作原来可以帮助我们在Android和IOS双端快速去构建一个差异化高效的渲染链路。这在目前双端主要以web作为跨平台渲染的主要形式下,提供了一个更容易定制和优化的方案。
关注「淘系技术」微信公众号,一个有内容有温度的技术社区。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

尾声

面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Android核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、热修复设计、插件化框架解读、组件化框架设计、图片加载框架、网络、设计模式、设计思想与代码质量优化、程序性能优化、开发效率优化、设计模式、负载均衡、算法、数据结构、高级UI晋升、Framework内核解析、Android组件内核等。

不仅有学习文档,视频+笔记提高学习效率,还能稳固你的知识,形成良好的系统的知识体系。这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

Android进阶学习资料库

一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

image

大厂面试真题

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《2017-2021字节跳动Android面试历年真题解析》

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

ndroid源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-msUoe95I-1712686647044)]

《2017-2021字节跳动Android面试历年真题解析》

[外链图片转存中…(img-IF9vCkiG-1712686647045)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 15
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值