作者:易旭昕 原文链接:https://zhuanlan.zhihu.com/p/214099612 本文由作者授权发布。
写作费时,敬请点赞,关注,收藏三连。
Flutter 渲染引擎在 iOS 上支持三种渲染方式,分别是纯软件(CPU),Metal 和 GL。其中纯软件的方式仅限于特定的构建,需要在编译时开启 TARGET_IPHONE_SIMULATOR 宏,应该是用于在模拟器上的测试,实机运行只会使用 Metal 和 GL。Flutter 会在运行时先判断是否能够使用 Metal,如果设备不支持,才会降级到 GL。iOS 10 以上的版本默认使用 Metal,GL 只用于兼容 iOS 9 的老旧设备。
这篇文章的主要内容是讲解在 iOS 上,Flutter 渲染引擎:
- 需要的 Metal GPU 上下文环境是如何完成初始化;
- 目标输出 Surface 的设置过程;
- 渲染流水线执行光栅化的调用过程。
上图显示了 Flutter 渲染引擎在 iOS 上主要涉及的对象,绿色背景是 iOS SDK 原生对象,黄色背景是平台相关的适配对象,白色背景是平台无关的通用对象。后面的内容我们会频繁地引用图中的对象,这张图可以方便读者了解它们之间的关系。
Metal GPU 上下文环境初始化
上图显示了 iOS 应用在主线程初始化 Flutter Engine 的调用栈。FlutterViewController 在被系统初始化时创建了 FlutterEngine,并请求 engine 创建 Shell 对象,FlutterEngine 在 Shell 对象的创建过程中生成了 PlatformViewIOS 对象并将它传递给 Shell。
代码语言:javascript复制std::unique_ptr<IOSContext> IOSContext::Create(IOSRenderingAPI rendering_api) {
switch (rendering_api) {
case IOSRenderingAPI::kOpenGLES:
return std::make_unique<IOSContextGL>();
case IOSRenderingAPI::kSoftware:
return std::make_unique<IOSContextSoftware>();
#if FLUTTER_SHELL_ENABLE_METAL
case IOSRenderingAPI::kMetal:
return std::make_unique<IOSContextMetal>();
#endif // FLUTTER_SHELL_ENABLE_METAL
default:
break;
}
FML_CHECK(false);
return nullptr;
}
PlatformViewIOS 一个主要的职责就是创建 IOSContext 对象,由它来为渲染引擎提供 GPU 上下文环境,在使用 Metal API 的情况下,创建的实际上是 IOSContextMetal 对象。
代码语言:javascript复制IOSContextMetal::IOSContextMetal() {
device_.reset([MTLCreateSystemDefaultDevice() retain]);
main_queue_.reset([device_ newCommandQueue]);
...
main_context_ = GrContext::MakeMetal([device_ retain], [main_queue_ retain]);
resource_context_ = GrContext::MakeMetal([device_ retain], [main_queue_ retain]);
...
}
从上面我们可以看到在 IOSContextMetal 的构造函数里面,它要做的就是:
- 创建或者获取系统默认的 MTLDevice(MTLCreateSystemDefaultDevice);
- 创建 MTLCommandQueue;
- 使用前面创建的 MTLDevice 和 MTLCommandQueue 分别创建两个 Skia GrContext,main context 用于在 raster 线程光栅化,resource context 用于在 io 线程做纹理上传;
sk_sp<GrContext> GrContext::MakeMetal(void* device, void* queue, const GrContextOptions& options) {
sk_sp<GrContext> context(new GrLegacyDirectContext(GrBackendApi::kMetal, options));
context->fGpu = GrMtlTrampoline::MakeGpu(context.get(), options, device, queue);
if (!context->fGpu) {
return nullptr;
}
if (!context->init(context->fGpu->refCaps())) {
return nullptr;
}
return context;
}
Skia 内部创建了 GrLegacyDirectContext 和 GrMtlGpu 对象,在 GrMtlGpu 保持了对传递进来的 MTLDevice 和 MTLCommandQueue 对象的引用,后续它会使用 MTLCommandQueue 对象创建 MTLCommandBuffer 对象用于执行 GPU 绘图指令。
到目前为止,我们已经完成了 Metal GPU 上下文环境的初始化,并创建了两个 Skia GrContext 分别用于后续的 Skia 光栅化和纹理上传。但是为了完成真正的光栅化和屏幕输出,我们还需要获取目标输出的 Surface。
设置目标输出 Surface
当 FlutterViewController 加载 View 结束后被系统回调 viewDidLoad,触发了 PlatformViewIOS::attachView 被调用。
代码语言:javascript复制void PlatformViewIOS::attachView() {
ios_surface_ =
[static_cast<FlutterView*>(owner_controller_.get().view) createSurface:ios_context_];
...
}
PlatformViewIOS::attachView 通过 FlutterViewController 获取 FlutterView,然后调用它的 createSurface 方法创建 IOSSurface,传递 IOSContext 对象作为参数。
代码语言:javascript复制- (std::unique_ptr<flutter::IOSSurface>)createSurface:
(std::shared_ptr<flutter::IOSContext>)ios_context {
return flutter::IOSSurface::Create(
std::move(ios_context), // context
fml::scoped_nsobject<CALayer>{[self.layer retain]}, // layer
[_delegate platformViewsController] // platform views controller
);
}
FlutterView::createSurface 调用 IOSSurface::Create 方法创建 IOSSurface 对象,并传递自己的 layer 对象作为参数,在使用 Metal API 的情况下,layer 对象实际是 CAMetalLayer,创建的 IOSSurface 实际上是 IOSSurfaceMetal。IOSSurfaceMetal 的实现比较简单,它实际就是用来持有 IOSContextMetal 和 CAMetalLayer 用于后续创建 GPUSurfaceMetal,Surface 的子类。
系统调用 FlutterViewController::viewDidLayoutSubviews 通知它 FlutterView 布局计算完成,大小已经确定,会触发 PlatformView::NotifyCreated 被调用。在这里,主线程会同步请求 raster 线程创建 Rendering Surface,实际上就是请求之前创建的 IOSSurfaceMetal 创建 GPUSurfaceMetal。
代码语言:javascript复制GPUSurfaceMetal::GPUSurfaceMetal(GPUSurfaceDelegate* delegate,
fml::scoped_nsobject<CAMetalLayer> layer,
sk_sp<GrContext> context,
fml::scoped_nsprotocol<id<MTLCommandQueue>> command_queue)
: delegate_(delegate),
layer_(std::move(layer)),
context_(std::move(context)),
command_queue_(std::move(command_queue)) {
...
}
GPUSurfaceMetal 实际上就是用来持有 CAMetalLayer 图层对象,IOSContextMetal 创建的光栅化用的 GrContext 对象,和 IOSContextMetal 创建的 MTLCommandQueue 对象。GPUSurfaceMetal 对象最终通过 Shell 传递给 Rasterizer 持有,到这里光栅化器就完成了目标输出 Surface 的设置,现在我们可以开始绘制第一帧了。
光栅化输出
关于 Flutter 渲染流水线比较完整的说明请参考我之前的文章Flutter 渲染流水线浅析,在这里我们只关注光栅化的部分。Flutter 光栅化的过程比较简单:
- 从目标输出的 Surface,也就是 CAMetalLayer,获取一个像素缓冲器( CAMetalDrawable 封装了该缓冲器);
- 将这个像素缓冲器包装成一个 SkSurface 对象,并取得对应的 SkCanvas 对象;
- 将生成的图层树里面的 DisplayList(SkPicture)通过上面的 SkCanvas 逐个绘制到 SkSurface 上,Skia 会先存储经过预处理的 2D 绘图指令;
- Flush SkCanvas,相当于生成相应的 Metal GPU 绘图指令,Encode 到 CommandBuffer,最后请求 Metal 执行;
- 等待执行完毕后,请求提交绘制完成的像素缓冲器,并请求 iOS 重绘 UI,CAMetalLayer 在被绘制的过程中输出新的像素缓冲器到屏幕上;
RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) {
auto frame = surface_->AcquireFrame(layer_tree.frame_size());
SkMatrix root_surface_transformation = surface_->GetRootTransformation();
auto root_surface_canvas = frame->SkiaCanvas();
auto compositor_frame = compositor_context_->AcquireFrame(
surface_->GetContext(), // skia GrContext
root_surface_canvas, // root surface canvas
external_view_embedder, // external view embedder
root_surface_transformation, // root surface transformation
true, // instrumentation enabled
frame->supports_readback(), // surface supports pixel reads
raster_thread_merger_ // thread merger
);
if (compositor_frame) {
RasterStatus raster_status = compositor_frame->Raster(layer_tree, false);
frame->Submit();
return raster_status;
}
return RasterStatus::kFailed;
}
上面的代码显示了一个简化后的光栅化器光栅化图层树的流程(不考虑使用 ExternalViewEmbedder 的场景):
- Rasterizer 首先调用 GPUSurfaceMetal::AcquireFrame 获取一个 SurfaceFrame;
- 然后通过 SurfaceFrame 获取用于绘制目标缓冲器的 SkCanvas(frame->SkiaCanvas());
- 然后将 SkCanvas 包裹成一个 CompositorContext::ScopedFrame 对象,并请求它光栅化图层树(compositor_frame->Raster(layer_tree, false));
- 最后调用 SurfaceFrame::Submit 提交绘制结果(frame->Submit());
std::unique_ptr<SurfaceFrame> GPUSurfaceMetal::AcquireFrame(const SkISize& frame_size) {
auto surface = SkSurface::MakeFromCAMetalLayer(context_.get(), // context
layer_.get(), // layer
kTopLeft_GrSurfaceOrigin, // origin
1, // sample count
kBGRA_8888_SkColorType, // color type
nullptr, // colorspace
nullptr, // surface properties
&next_drawable_ // drawable (transfer out)
);
auto submit_callback = [this](const SurfaceFrame& surface_frame, SkCanvas* canvas) -> bool {
canvas->flush();
auto command_buffer =
fml::scoped_nsprotocol<id<MTLCommandBuffer>>([[command_queue_.get() commandBuffer] retain]);
fml::scoped_nsprotocol<id<CAMetalDrawable>> drawable(
reinterpret_cast<id<CAMetalDrawable>>(next_drawable_));
next_drawable_ = nullptr;
[command_buffer.get() commit];
[command_buffer.get() waitUntilScheduled];
[drawable.get() present];
return true;
};
return std::make_unique<SurfaceFrame>(std::move(surface), true, submit_callback);
}
SkCanvas* SurfaceFrame::SkiaCanvas() {
return surface_ != nullptr ? surface_->getCanvas() : nullptr;
}
上面的代码显示了简化后的 GPUSurfaceMetal::AcquireFrame 处理流程:
- 首先是使用初始化时获得的 GrContext 和 CAMetalLayer 生成一个 SkSurface,并获得 SkSurface 通过 CAMetalLayer 创建的 CAMetalDrawable 对象,Skia 在内部会使用该 CAMetalDrawable 作为 SkSurface 的像素缓冲器;
- 创建供 SurfaceFrame::Submit 调用的回调函数对象;
- 将上面的生成的 SkSurface 和 Submit Callback 封装成 SurfaceFrame 输出;
- SurfaceFrame::SkiaCanvas 返回 SkSurface 对应的 SkCanvas 对象供光栅化器使用;
当 SurfaceFrame::Submit 的时候:
- Flush 绘制图层树完毕的 SkCanvas,相当于请求 GrContext 创建一个 MTLCommandBuffer,然后再创建绑定 SkSurface 包装的 CAMetalDrawable 的 MTLRenderCommandEncoder 对象,根据输入的 2D 绘制指令生成 Metal GPU 绘制指令并 Encode,最后结束 Encode 并 Commit MTLCommandBuffer;
- 然后创建一个新的 MTLCommandBuffer 等待其被调度,这样可以保证前面的 MTLCommandBuffer 被执行完毕;
- 最后调用 CAMetalDrawable::present 方法,提交绘制完成的像素缓冲器,并请求 iOS 重绘;
如果读者对更多的具体细节感兴趣的话,可以去阅读 Skia 内部的实现代码,这部分相对来说就比较复杂了。