微信交流群

作者:易旭昕

原文链接: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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

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]);
  }
}
1
2
3
4
5
6
7
8
9
10
11

从上面代码我们可以看到在 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_];
  ...
}
1
2
3
4
5

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
  );
}
1
2
3
4
5
6
7
8

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));
}
1
2
3
4
5
6

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上面的代码显示了:

  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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

上面的代码显示了一个简化后的光栅化器光栅化图层树的流程(不考虑使用 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];
}
1
2
3
4
5
6
7
8
9
10
11
12
13

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

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