Flutter 渲染引擎详解 - iOS GL 篇

时间:2022-07-28
本文章向大家介绍Flutter 渲染引擎详解 - iOS GL 篇,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

作者:易旭昕 原文链接:https://zhuanlan.zhihu.com/p/258492344 本文由作者授权发布。

写作费时,敬请点赞,关注,收藏三连。

Flutter 渲染引擎在 iOS 上支持三种渲染方式,分别是纯软件(CPU),Metal 和 GL。其中纯软件的方式仅限于特定的构建,需要在编译时开启 TARGET_IPHONE_SIMULATOR 宏,应该是用于在模拟器上的测试,实机运行只会使用 Metal 和 GL。Flutter 会在运行时先判断是否能够使用 Metal,如果设备不支持,才会降级到 GL。iOS 10 以上的版本默认使用 Metal,GL 只用于兼容 iOS 9 的老旧设备。

这篇文章的主要内容是讲解在 iOS 上,Flutter 渲染引擎:

  1. 需要的 GL GPU 上下文环境是如何完成初始化;
  2. 目标输出 Surface 的设置过程;
  3. 渲染流水线执行光栅化的调用过程。

上图显示了 Flutter 渲染引擎在 iOS 上主要涉及的对象,绿色背景是 iOS SDK 原生对象,黄色背景是平台相关的适配对象,白色背景是平台无关的通用对象。后面的内容我们会频繁地引用图中的对象,这张图可以方便读者了解它们之间的关系。

GL GPU 上下文环境初始化

上图显示了 iOS 应用在主线程初始化 Flutter Engine 的调用栈。FlutterViewController 在被系统初始化时创建了 FlutterEngine,并请求 engine 创建 Shell 对象,FlutterEngine 在 Shell 对象的创建过程中生成了 PlatformViewIOS 对象并将它传递给 Shell。

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 上下文环境,在使用 GL API 的情况下,创建的实际上是 IOSContextGL 对象。

IOSContextGL::IOSContextGL() {
  resource_context_.reset([[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]);
  if (resource_context_ != nullptr) {
    context_.reset([[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3
                                         sharegroup:resource_context_.get().sharegroup]);
  } else {
    resource_context_.reset([[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]);
    context_.reset([[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2
                                         sharegroup:resource_context_.get().sharegroup]);
  }
}

从上面代码我们可以看到在 IOSContextGL 的构造函数里面,主要就是创建两个 EAGLContext GL 上下文对象,分别在 io 线程用于图片纹理上传(resource_context_)和在 raster 线程用于光栅化(context_),并且 resource_context_ 作为 context_ 的 sharegroup,从而在两个上下文之间共享纹理。

到目前为止,我们已经完成了 GL GPU 上下文环境的初始化,跟 iOS Metal 的实现不同,跟 Android GL 的实现类似,光栅化使用的 Skia GrContext 不是在这里创建,而是延迟到设置目标输出 Surface 时创建。

设置目标输出 Surface

当 FlutterViewController 加载 View 结束后被系统回调 viewDidLoad,触发了 PlatformViewIOS::attachView 被调用。

void PlatformViewIOS::attachView() {
  ios_surface_ =
      [static_cast<FlutterView*>(owner_controller_.get().view) createSurface:ios_context_];
  ...
}

PlatformViewIOS::attachView 通过 FlutterViewController 获取 FlutterView,然后调用它的 createSurface 方法创建 IOSSurface,传递 IOSContext 对象作为参数。

- (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 对象作为参数,在使用 GL API 的情况下,layer 对象实际是 CAEAGLLayer,创建的 IOSSurface 实际上是 IOSSurfaceGL。

IOSSurfaceGL::IOSSurfaceGL(fml::scoped_nsobject<CAEAGLLayer> layer,
                           std::shared_ptr<IOSContext> context,
                           FlutterPlatformViewsController* platform_views_controller)
    : IOSSurface(context, platform_views_controller) {
  render_target_ = CastToGLContext(context)->CreateRenderTarget(std::move(layer));
}

IOSSurfaceGL 主要是调用 IOSContextGL::CreateRenderTarget 方法创建 IOSRenderTargetGL 对象并持有。IOSRenderTargetGL 主要是用来持有 CAEAGLLayer 对象,和从 IOSContextGL 获得的用于光栅化的 EAGLContext 对象,并为 CAEAGLLayer 分配 Framebuffer 和 Renderbuffer GL 对象。

IOSRenderTargetGL::IOSRenderTargetGL(fml::scoped_nsobject<CAEAGLLayer> layer,
                                     fml::scoped_nsobject<EAGLContext> context)
    : layer_(std::move(layer)), context_(context) {
  ...
  auto context_switch = GLContextSwitch(std::make_unique<IOSSwitchableGLContext>(context_.get()));
  bool context_current = context_switch.GetResult();

  // Generate the framebuffer
  glGenFramebuffers(1, &framebuffer_);
  glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);

  // Setup color attachment
  glGenRenderbuffers(1, &colorbuffer_);
  glBindRenderbuffer(GL_RENDERBUFFER, colorbuffer_);
  glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorbuffer_);

  valid_ = true;
}

上面的代码显示了:

  1. IOSSwitchableGLContext 用来设置 EAGLContext 作为调用线程的当前上下文;
  2. 产生 Framebuffer GL 对象;
  3. 产生 Renderbuffer GL 对象并绑定为上面 Framebuffer 的 Color Attachment(像素缓冲区);
  4. CAEAGLLayer 对象跟 Renderbuffer 的绑定会延迟到第一次进行光栅化的时候;

系统调用 FlutterViewController::viewDidLayoutSubviews 通知它 FlutterView 布局计算完成,大小已经确定,会触发 PlatformView::NotifyCreated 被调用。在这里,主线程会同步请求 raster 线程创建 Rendering Surface,实际上就是请求之前创建的 IOSSurfaceGL 创建 GPUSurfaceGL。

GPUSurfaceGL 在构造函数里面会创建光栅化用的 Skia GrContext 对象并持有,该 GrContext 对象对应当前线程的 GL 上下文对象,也就是 IOSRenderTargetGL 持有的光栅化用的 EAGLContext 对象。GPUSurfaceGL 对象最终通过 Shell 传递给 Rasterizer 持有,到这里光栅化器就完成了目标输出 Surface 的设置,现在我们可以开始绘制第一帧了。

光栅化输出

关于 Flutter 渲染流水线比较完整的说明请参考我之前的文章Flutter 渲染流水线浅析,在这里我们只关注光栅化的部分。Flutter 在 iOS GL 上进行光栅化的操作如下:

  1. 将目标输出的 Surface,也就是 CAEAGLLayer,跟为它分配的 Renderbuffer GL 对象绑定,Renderbuffer 作为对应的 Framebuffer 的 Color Attachment,也就是对这个 Framebuffer 写入,光栅化后像素值的结果实际上是写入到对应的 CAEAGLLayer 的内部像素缓冲区里面;
  2. 将上面的 Framebuffer 包装成一个 SkSurface 对象,并取得对应的 SkCanvas 对象;
  3. 将生成的图层树里面的 DisplayList(SkPicture)通过上面的 SkCanvas 逐个绘制到 SkSurface 上,Skia 会先存储经过预处理的 2D 绘图指令;
  4. Flush SkCanvas,相当于生成相应的 GL 指令,执行时将光栅化的结果写入上面的 Framebuffer;
  5. 等待执行完毕后,请求提交绘制完成的像素缓冲器,并请求 iOS 重绘 UI,CAEAGLLayer 在被绘制的过程中输出新的像素缓冲器到屏幕上;
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 的场景):

  1. Rasterizer 首先调用 GPUSurfaceGL::AcquireFrame 获取一个 SurfaceFrame;
  2. 然后通过 SurfaceFrame 获取用于绘制目标缓冲器的 SkCanvas(frame->SkiaCanvas());
  3. 然后将 SkCanvas 包裹成一个 CompositorContext::ScopedFrame 对象,并请求它光栅化图层树(compositor_frame->Raster(layer_tree, false));
  4. 最后调用 SurfaceFrame::Submit 提交绘制结果(frame->Submit());

GPUSurfaceGL::AcquireFrame 需要调用 IOSSurfaceGL::GLContextMakeCurrent 设置当前线程的 GL 上下文,并且调用 IOSRenderTargetGL::UpdateStorageSizeIfNecessary 将 CAEAGLLayer 和 IOSRenderTargetGL 创建时分配的 Renderbuffer 绑定。

当 SurfaceFrame::Submit 的时候,IOSRenderTargetGL::PresentRenderBuffer 会被调用到。

bool IOSRenderTargetGL::PresentRenderBuffer() const {
  const GLenum discards[] = {
      GL_DEPTH_ATTACHMENT,
      GL_STENCIL_ATTACHMENT,
  };

  glDiscardFramebufferEXT(GL_FRAMEBUFFER, sizeof(discards) / sizeof(GLenum), discards);

  glBindRenderbuffer(GL_RENDERBUFFER, colorbuffer_);
  auto current_context = [EAGLContext currentContext];
  FML_DCHECK(current_context != nullptr);
  return [current_context presentRenderbuffer:GL_RENDERBUFFER];
}

IOSRenderTargetGL::PresentRenderBuffer 主要是调用 CAEAGLLayer::presentRenderbuffer 来请求 CAEAGLLayer 提交绘制完毕的像素缓冲区。

如果读者对更多的具体细节感兴趣的话,可以去阅读 Skia 内部的实现代码,这部分相对来说就比较复杂了。