您是否希望开发一个具有多进程架构的 macOS 应用程序,其中一个进程负责生成图片,而另一个进程则负责在窗口中渲染这些图片? 如果您的应用需求不仅限于展示静态图片,而是要播放 60fps 的 4k 全屏视频,同时,您还希望在这一过程中不会对用户的 Mac 造成过高的 CPU 负载,如果以上是您所期望的,那么本篇文章就是为你准备的。
我叫 Kyrylo,多年来,我一直致力于开发一个商业化的跨平台库,该库允许将 Chromium 网络浏览器控件集成到 Java 和 .NET 桌面应用程序中。为了将 Chromium 集成到第三方应用程序中,我们不得不面临和克服一系列复杂且具有挑战性的技术难题。
其中,渲染是最重要且最具挑战性的任务之一。其复杂性源于 Chromium 的多进程架构。Chromium 在独立的 GPU 进程中渲染网页内容,而我们需要在另一个 Java 或 .NET 进程中显示这些内容。关于这个问题可以这样描述:在进程 B 中显示进程 A 的图形内容。

与不同进程共享渲染结果
每个支持的平台解决此任务的方式都不同。例如,在 Windows 和 Linux 上,这是通过将 Chromium
窗口嵌入到目标应用程序窗口中来完成的。这是本机
API 的常见用例:Win32 API’ SetParent
或 XLib’ ReparentWindow 函数。
遗憾的是,这种方法在 macOS 上并不适用。macOS 不允许将一个进程的窗口嵌入到另一个进程的窗口中。不过,我们仍有办法解决这一问题。
在本篇文章中,我将展示如何使用
CALayer 共享方法在 macOS
上将在一个进程中创建的内容渲染到另一个进程的窗口中。我将通过一个简单的应用程序来说明这一概念。
Chromium 多进程架构
在本节中,让我们简要回顾一下 Chromium 架构。
Chromium 的主要思想是使用单独的进程来显示网页。每个进程与其他进程和系统其余部分都是隔离的。
在下面的图片中,Browser 代表主进程(顶级窗口)。Renderer 负责解释和布局 HTML(浏览器标签页)。
这是出于安全原因的设计, 使得 Chromium 与操作系统的设计理念相似:即使某个应用程序(如 GPU 或渲染进程,等等)崩溃,也不会导致整个系统崩溃。

Chromium 多进程架构(引自此处)
同样的概念也适用于图形处理。Chromium 为 GPU 相关操作托管了一个单独的进程。

Chromium GPU 进程
纹理共享
让我们回到最初的任务,即从不同的进程中获取像素。在深入研究实现细节之前,了解 macOS 中用于渲染的实体是至关重要的。

简化的 CoreAnimation 模型
在上面的图片中,您可以看到用于显示图形内容的主要对象及它们之间的关系:
NSWindow表示屏幕上的窗口。该类为嵌入其他视图提供了一个区域,接受并分发由用户通过鼠标和键盘引起的交互事件;UIView— 渲染其内容边界内内容的基本构建块。这是诸如标签、按钮、滑块等 UI 控件的基类;CALayer— 用于为视图提供后备存储的对象。即使没有父视图,也可以利用CALayer实例来显示视觉内容。图层用于视图的自定义,例如添加半径、阴影、高亮等。
我们之所以对 CALayer 感兴趣,是因为它为内容显示提供了更多的灵活性。确切地说,我们将使用其子类 — CALayerHost, 这个子类对于纹理共享至关重要。那么,“纹理共享” 是什么呢?
纹理共享指的是一个进程创建的图形内容能够被另一个进程所使用的过程。这一过程正是本文所阐述的多进程架构概念的具体实践。

在一个进程中创建图形内容 — 在另一个进程中显示
IOSurface
苹果公司为实现上述目的提供了方法,即 IOSurface 框架。该 API 基于
IOSurface 可以通过名为 mach_ports 的 macOS 低级内核原语从远程进程访问。Chromium
使用了 IOSurface 方法,我们在项目中也用了很长一段时间。它具有良好的性能和稳定的
API。然而,过了一段时间后,我们就遇到了问题,即在 Chromium 中启用 IOSurface
渲染流程时,一些网站无法正确显示。这促使我们调查了 macOS 上
Chromium 的默认方法 — CALayerHost
CALayerHost
正如我前面提到的,CALayerHost 是 CALayer 的子类,用于渲染另一个图层的渲染上下文。通过
CALayerHost 进行的纹理共享涉及以下实体:
CAContext— CoreAnimation 对象, 用于表示有关环境的信息,以及用于在进程之间共享CALayer。CALayerHost—CALayer的子类,可以渲染远程图层的内容;CAContextID— 全局唯一标识符, 用于标识CAContext。这个 token 可以直接在不同的进程之间传递,而mach_port的传递则需要额外的操作。

渲染共享所需的类
示例
我将用一个简单的应用程序来说明纹理共享的概念。这个应用程序将类似于 Chromium 的结构,即为不同的目标设置单独的进程。
GPU 进程将使用 CALayer 作为后备缓冲区来完成所有的绘制工作。绘制完成后,此 CALayer
的 CAContextID 将被发送到主进程。主进程将使用 CAContextID 来创建 CALayerHost
对象,以便在创建的 NSWindow 对象上显示内容。
实施细节
这个应用程序由两个进程组成:
- Renderer App(渲染应用程序) — 负责创建要共享的纹理;
- Host App(宿主应用程序) — 显示在 Renderer App 中创建的内容。
进程之间的通信是通过一个简单的进程间通信(IPC)库来实现的。

示例应用程序架构
上述架构采用了传统的客户端-服务器模式, 其中 Host App 向 Renderer App 发送相应的请求。
让我们快速浏览一下应用程序的主要部分。
主函数随后启动这两个进程。要渲染的纹理数量由常量变量配置:
int main(int argc, char* argv[]) {
constexpr int kNumberWindows = 2;
if (ipc::Ipc::instance().fork_process()) {
HostApp server_app;
server_app.run(kNumberWindows);
} else {
RendererApp client_app;
client_app.run(kNumberWindows);
}
}
声明必要的 API
CALayerHost 类以及其他用于图层共享的对象都是运行时
API。因此,它们必须在我们的代码中显式声明。
// CGSConnectionID 用于在一个进程中创建 CAContext,
// 该进程将要共享其渲染的 CALayer 给另一个进程
// 以供显示。
extern "C" {
typedef uint32_t CGSConnectionID;
CGSConnectionID CGSMainConnectionID(void);
};
// CAContextID 类型用于跨进程标识 CAContext。
// 这是一个 token,
// 从共享正在渲染 CALayer 的进程
// 传递到将要显示该 CALayer 的进程。
typedef uint32_t CAContextID;
// CAContext 具有静态的 CAContextID,可以将其发送到另一个进程。
// 当在另一个进程中使用该 CAContextID 创建
// CALayerHost 时,该 CALayerHost 显示的内容将是设置为
// CAContext 上 // layer| 属性的 CALayer 的内容。
@interface CAContext : NSObject
+ (id)contextWithCGSConnection:(CAContextID)contextId options:
(NSDictionary*)optionsDict;
@property(readonly) CAContextID contextId;
@property(retain) CALayer *layer;
@end
// CALayerHost 是在将显示另一个进程渲染的内容的进程中创建的。
// 在这个类的对象上设置 |contextId| 属性后,
// 该层将展示在图层共享过程中
// 与具有该 CAContextID 的 CAContext
// 相关联的 CALayer 的内容。
@interface CALayerHost : CALayer
@property CAContextID contextId;
@end
Renderer App (渲染应用程序)
Renderer App 主要完成两项任务:
- 通过 OpenGL API 进行内容渲染。 这是在
ClientGlLayer类中完成的,该类继承自CAOpenGLLayer - 通过
CAContext和 IPC 库,将新创建的图层暴露给 Host App。
void RendererApp::exportLayer(CALayer* gl_layer) {
NSDictionary* dict = [[NSDictionary alloc] init];
CGSConnectionID connection_id = CGSMainConnectionID();
CAContext* remoteContext = [CAContext
contextWithCGSConnection:connection_id options:dict];
printf("Renderer: 将 CAContext 的图层设置为要导出的 CALayer。\n");
[remoteContext setLayer:gl_layer];
printf("Renderer: 将上下文 ID 发送回服务器。\n");
CAContextID contextId = [remoteContext contextId];
// 将 contextId 发送到 HostApp。
ipc::Ipc::instance().write_data(&contextId);
}
共享 CALayer 的代码很简单:
- 初始化
CAContext; - 通过
CAContext::setLayer()方法将图层暴露以供导出; - 通过 IPC 库将
CAContext的标识符传递给 Renderer App。在那里,它将用于创建CALayerHost实例。
Host App(宿主应用程序)
Host App 的主要目标是显示 Renderer App 中渲染的内容。每个图层都由一个单独的
NSWindow 服务。首先,我们需要使用从 Renderer App 进程接收到的 CAContextID
来初始化一个 CALayerHost:
CALayerHost*HostApp::getLayerHost() {
if (context_id_ == 0) {
ipc::Ipc::instance().read_data(&context_id_);
}
CALayerHost* layer_host = [[CALayerHost alloc] init];
[layer_host setContextId:context_id_];
return layer_host;
}
为了方便起见,我们将接收到的 id 保存到 context_id 字段中。接着,我们需要将
CALayerHost 嵌入到新创建的窗口的 NSView 中:
[window setTitle:@"CARemoteLayer Example"];
[window makeKeyAndOrderFront:nil];
NSView* view = [window contentView];
[view setWantsLayer:YES];
CALayerHost* layer_host = getLayerHost();
[[[window contentView] layer] addSublayer:layer_host];
[layer_host setPosition:CGPointMake(240, 240)];
printf("Host: 已将图层添加到视图层次结构中。\n");
重要提示: 必须将 setWantsLayer 属性设置为 YES, 这样视图才能通过
CALayer 来管理其渲染的内容。
下面您可以看到两个窗口的应用程序结果。 请注意,这两个窗口在 Render App
中使用的是同一个 CALayer。

运行具有两个窗口的应用程序
您可以在此处找到示例应用程序的完整源代码,以及构建说明。
结论
将 GPU 渲染置于单独的进程中是 Chromium
等大型项目中常用的做法。这种做法不仅提高了产品的整体安全性,还增强了代码的可维护性。在本文中, 我们测试了基于
CALayer 共享的方法。其核心思想在于,有一个进程专门负责在 CALayer
上进行绘制,并将最终图层共享给另一个或多个进程,这些进程随后可以在目标视图中展示这些已渲染的图层。
发送中。。。
您的个人 JxBrowser 试用密钥和快速入门指南将在几分钟内发送至您的电子邮箱。
