JSActors

在 Fission 世界中,进程间通信的首选方法是 JSActors。

在撰写本文档时,Fission 提供以下 JSActors:

  • JSProcessActor,用于子进程与其父进程之间的通信;

  • JSWindowActor,用于框架与其父框架之间的通信。

JSProcessActor

什么是 JSProcessActors?

JSProcess 对(见下文)是子进程与其父进程之间通信的首选方法。

在 Fission 世界中,JSProcessActors 替换了 e10s 时代的 *进程脚本*。

JSProcessActor 对的生命周期

JSProcessActors 总是成对存在

  • 一个 JSProcessActorChild 实例,位于子进程中——例如,MyActorChild

  • 一个 JSProcessActorParent 实例,位于父进程中——例如,MyActorParent

该对是在第一次调用 getActor(“MyActor”) 时延迟实例化的(见下文)。请注意,如果父进程有多个子进程,则父进程通常会托管多个 MyActorParent 实例,而子进程将分别托管一个 MyActorChild 实例。

JSProcessActor 原语允许在 *对内* 发送和接收消息。在撰写本文档时,JSProcessActor 并未提供用于广播、枚举等的原语。

当子进程终止时,该对也会终止。

关于 Actor 名称

请注意,名称 MyActorChildMyActorParent 具有意义——后缀 ChildParentgetActor(…) 在 JS 代码中查找要加载的正确类的依据。

JSWindowActor

什么是 JSWindowActors?

JSWindowActor 对(见下文)是框架与其父框架之间通信的首选方法,无论框架和父框架是否位于同一个进程或不同的进程中。

在 Fission 世界中,JSWindowActors 替换了 *framescripts*。Framescripts 是我们用于构建代码以了解父(UI)和子(内容)分离的方式,包括建立两者之间的通信通道(通过框架消息管理器)。

但是,framescripts 无法建立进一步的向下进程分离(即,对于进程外的 iframe)。JSWindowActors 将成为替代方案。

它们的结构如何?

回顾 Fission 之前的消息管理器机制

注意

实际上有几种类型的消息管理器:框架消息管理器、窗口消息管理器、组消息管理器和进程消息管理器。出于本文档的目的,将所有这些机制统称为“消息管理器机制”最简单。本文档中的大多数示例都将基于消息管理器是框架消息管理器的假设,这是最常用的一个。

目前,在 电解项目 之后的 Firefox 代码库中,我们在父进程(UI)中拥有使用纯 JS(.js 文件)或 JS 模块(.jsm 文件)编写的代码。在子进程(托管内容)中,我们使用 framescripts(.js)以及 JS 模块。framescripts 为每个顶级框架实例化一次(或者,简单地说,每个标签页实例化一次)。此代码可以访问 Web 内容中的所有 DOM,包括其中的所有 iframe。

这两个进程通过框架消息管理器 (mm) 使用 sendAsyncMessage / receiveMessage API 进行通信,父进程中的任何代码都可以与子进程中的任何代码通信(反之亦然),只需监听感兴趣的消息即可。

框架消息管理器通信机制遵循类似于 Firefox 中事件工作方式的发布/订阅模式

  1. 某些内容公开了一种订阅通知的机制(对于框架消息管理器,为 addMessageListener,对于事件,为 addEventListener)。

  2. 订阅者负责在不再关注通知时取消订阅(对于框架消息管理器,为 removeMessageListener,对于事件,为 removeEventListener)。

  3. 可以随时附加任意数量的订阅者。

../../_images/Fission-framescripts.png

JSWindowActors 与框架消息管理器的区别

对于 Fission,替换 framescripts 的 JSWindowActors 将成对结构化。将延迟实例化一对 JSWindowActors:一个在父进程中,一个在子进程中,并建立两者之间的直接通信通道。父进程中的 JSWindowActor 必须扩展全局 JSWindowActorParent 类,子进程中的 JSWindowActor 必须扩展全局 JSWindowActorChild 类。

JSWindowActor 机制类似于 Firefox 本机层中 IPC Actor 的工作方式

  1. 每个 Actor 在另一个进程中都有一个对应的 Actor,它们可以直接与其通信。

  2. 每个 Actor 从父类继承通用的通信 API。

  3. 每个 Actor 的名称以 ParentChild 结尾。

  4. 没有内置的订阅消息机制。当一个 JSWindowActor 发送消息时,另一侧对应的 JSWindowActor 将接收它,而无需显式监听它。

JSWindowActors 与消息管理器/framescripts 的其他显著差异

  1. 每个 JSWindowActor 对都与特定的框架关联。例如,给定以下 DOM 层次结构

    <browser src="https://www.example.com">
      <iframe src="https://www.a.com" />
      <iframe src="https://www.b.com" />
    

    为任一 iframe 实例化的 JSWindowActorParent / JSWindowActorChild 对仅会向该 iframe 发送和接收消息。

  2. 每个 Actor 类型,每个框架只有一个对。

    例如,假设我们有一个 ContextMenu Actor。父进程最多可以有 N 个 ContextMenuParent Actor 实例,其中 N 是当前加载的框架数量。但是,对于任何单个框架,都只有一个 ContextMenuChild 与该框架关联。

  3. 我们不能再假设对框架树具有完全的同步访问权限,即使在内容进程中也是如此。

    这是将框架拆分为进程外运行的自然结果。

  4. JSWindowActorChild 与其关联的 WindowGlobalChild 生命周期相同。

如果在前面提到的 DOM 层次结构中,其中一个 <iframe> 卸载,则任何关联的 JSWindowActor 对都将被拆除。

JSWindowActors 由 WindowGlobal IPC Actor “管理”,并作为 JS 类(JSWindowActorParentJSWindowActorChild 的子类)实现,并在为任何特定窗口请求时实例化。与框架消息管理器一样,它们最终使用 IPC Actor 在后台进行通信。

提示

../../_images/Fission-actors-diagram.png

注意

与消息管理器一样,JSWindowActors 同时实现了进程内和进程外框架通信。这意味着可以立即移植到 JSWindowActors,而无需等待启用进程外 iframe。

与 Actor 通信

发送消息

JSActor 基类公开了两种发送消息的方法。这两种方法都是异步的。使用 JSActor **无法同步发送消息**。

sendAsyncMessage

sendAsyncMessage(“SomeMessage”, value[, transferables]);

value 可以是任何可以使用结构化克隆算法序列化的内容。此外,可以发送 nsIPrincipal,而无需手动序列化和反序列化它。

transferables 参数是一个可选的 可传输 对象数组。请注意,诸如 ArrayBuffers 之类的可传输对象无法跨进程传输,它们的内容将被复制到序列化数据中。但是,transferables 对于诸如 MessageChannel 端口之类的对象仍然有用,因为这些对象可以跨进程边界传输。

注意

跨进程对象包装器 (CPOW) 无法通过 JSWindowActors 发送。

sendQuery

Promise<any> sendQuery(“SomeMessage”, value);

sendQuerysendAsyncMessage 的基础上进行了改进,它返回一个 Promise。然后,消息的接收方必须返回一个 Promise,该 Promise 最终可以解析为一个值——此时,sendQueryPromise 将使用该值解析。

sendQuery 方法参数遵循与 sendAsyncMessage 相同的约定,第二个参数为结构化克隆。

接收消息

receiveMessage

要接收消息,您需要实现

receiveMessage(value)

该方法接收一个参数,该参数是通过 sendAsyncMessagesendQuery 发送的反序列化参数。

注意

如果 receiveMessage 正在响应 sendQuery,则**必须**为该消息返回一个 Promise

JSWindowActors 由 WindowGlobal IPC Actor “管理”,并作为 JS 类(JSWindowActorParentJSWindowActorChild 的子类)实现,并在为任何特定窗口请求时实例化。与框架消息管理器一样,它们最终使用 IPC Actor 在后台进行通信。

使用 sendQueryreceiveMessage 能够立即返回值?尝试使用 Promise.resolve(value); 返回 value,或者也可以将您的 receiveMessage 方法设为异步函数,假设它处理的其他消息不需要获取非 Promise 返回值。

其他可以覆盖的方法

constructor()

如果在 JSActor 实例化后立即需要执行某些操作,则 constructor 函数是执行此操作的理想位置。

注意

此时,发送消息的基础设施尚未准备好,并且 managerbrowsingContext 等对象不可用。

observe(subject, topic, data)

如果注册 Actor 以监听 nsIObserver 通知,请实现具有上述签名的 observe 方法以处理该通知。

handleEvent(event)

如果您注册您的 Actor 以监听内容事件,请实现一个具有上述签名的 handleEvent 方法来处理该事件。

注意

只有 JSWindowActor 可以注册以监听内容事件。

actorCreated

此方法在子 Actor 创建并初始化之后立即调用。与 Actor 的构造函数不同,可以执行诸如访问 Actor 的内容窗口和从此回调发送消息等操作。

didDestroy

这是在 Actor 被销毁之前清理 Actor 的另一个时机,但在这一点上,无法与另一端进行通信。

注意

此方法不能是异步的。

注意

由于 JSProcessActorChild 在其进程死亡时被销毁,因此 JSProcessActorChild 永远不会收到此调用。

JSWindowActorParent 上公开的其他内容

CanonicalBrowsingContext

Getter: this.browsingcontext

WindowGlobalParent

待办事项

JSWindowActorChild 上公开的其他内容

BrowsingContext

待办事项

WindowGlobalChild

待办事项

有用的 Getter

JSWindowActorChild 上存在许多有用的 Getter,包括

this.document

与该 JSWindowActorChild 关联的框架中当前加载的文档。

this.contentWindow

与该 JSWindowActorChild 关联的框架的外部窗口。

this.docShell

与该 JSWindowActorChild 关联的框架的 nsIDocShell

有关 JSWindowActorParentJSWindowActorChild 实现上到底公开了什么内容的更多详细信息,请参阅 JSWindowActor.webidl

如何从消息管理器和框架脚本移植到 JSWindowActor

消息管理器 Actor

在设计和开发 JSWindowActor 机制期间,我们的框架脚本的大部分内容已转换为“Actor 样式”模式,以便将来更容易移植到 JSWindowActor。这些 Actor 在后台使用消息管理器,但使我们更容易缩减框架脚本,并且还允许我们通过延迟实例化 Actor 来获得显着的内存节省。

您可以在 BrowserGlue.sys.mjsActorManagerParent.sys.mjs 中的 LEGACY_ACTORS 列表中找到消息管理器 Actor(或“旧版 Actor”)的列表。

注意

BrowserGlueActorManagerParent 之间定义的消息管理器 Actor 的拆分主要是为了将 Firefox 桌面特定的 Actor 与可以(理论上)为非桌面浏览器(如 Fennec 和基于 GeckoView 的浏览器)实例化的 Actor 分开。Firefox 桌面特定的 Actor 应在 BrowserGlue 中注册。共享的“工具包”Actor 应进入 ActorManagerParent

“移植”这些 Actor 通常意味着执行必要的操作,以便将它们的注册条目从 LEGACY_ACTORS 移动到 JSWINDOWACTORS 列表中。

确定新的 Actor 对的生命周期

在旧模型中,框架脚本尽快由顶级框架加载和执行。在 JSWindowActor 模型中,Actor 更加延迟,并且仅在以下情况下实例化

  1. 通过在 WindowGlobal 上调用 getActor 并传入 Actor 的名称来显式实例化它们。

  2. 向它们发送消息。

  3. 预定义的 nsIObserver 观察者通知触发,通知主题对应于内部或外部窗口。

  4. 预定义的内容事件触发。

像这样使 Actor 延迟可以节省处理时间以使框架准备好加载网页,以及将 Actor 加载到内存中的开销。

将框架脚本移植到 JSWindowActor 时,通常要问的第一个问题是:入口点是什么?Actor 应该在什么时间点实例化并变得活跃?

例如,在将 Firefox 的内容区域上下文菜单移植时,注意到内容中触发的 contextmenu 事件是等待实例化 Actor 对的自然事件。一旦 ContextMenuChild 实例化,handleEvent 方法用于检查事件并准备要发送到 ContextMenuParent 的消息。可以通过查看 上下文菜单 Fission 移植 的补丁来找到此示例。

使用 ContentDOMReference 而不是 CPOW

尽管作为同步访问其他进程中对象属性的方式被禁止,但 CPOW 最终被用作在进程之间传递 DOM 元素句柄的方式。

但是,CPOW 消息无法通过 JSWindowActor 通信管道发送,因此这种方便的机制将不再起作用。

相反,创建了一个名为 ContentDOMReference.sys.mjs 的新模块,它提供了相同的功能。有关文档,请参阅该文件。

如何开始将父进程浏览器代码移植以使用 JSWindowActor

消息管理器 Actor 的工作使得从框架脚本迁移到类似于 JSWindowActors 的内容变得更加容易。但是,它并没有实质性地改变父进程如何与这些框架脚本交互。

因此,在将代码移植以使用 JSWindowActors 时,我们发现这通常是花费时间的地方——重构父进程浏览器代码以适应新的 JSWindowActor 模型。

通常,首先要做的是为您的 Actor 对找到一个合理的名称,并注册它们(请参阅 使用 ContentDOMReference 而不是 CPOW),即使 Actor 本身只是 JSWindowActorParentJSWindowActorChild 的未修改子类。

接下来,通常很有帮助的是找到并记录所有使用 sendAsyncMessage 通过旧消息管理器接口发送消息到您正在移植的组件的位置,以及定义任何消息侦听器的位置。

让我们看一个假设的例子。假设我们正在移植页面信息对话框的一部分,该对话框扫描每个框架以获取有用的信息以在对话框中显示。给定如下代码块

// This is some hypothetical Page Info dialog code.

let mm = browser.messageManager;
mm.sendAsyncMessage("PageInfo:getInfoFromAllFrames", { someArgument: 123 });

// ... and then later on

mm.addMessageListener("PageInfo:info", async function onmessage(message) {
  // ...
});

如果注册了 PageInfoJSWindowActor 对,则可能很想简单地将第一部分替换为

let actor = browser.browsingContext.currentWindowGlobal.getActor("PageInfo");
actor.sendAsyncMessage("PageInfo:getInfoFromAllFrames", { someArgument: 123 });

但是,如果页面上的任何框架在其自己的进程中运行,它们将不会收到该 PageInfo:getInfoFromAllFrames 消息。相反,在这种情况下,我们应该遍历 BrowsingContext 树,并为每个全局实例化一个 PageInfo Actor,并分别发送一条消息以获取每个框架的信息。也许是这样的

let contextsToVisit = [browser.browsingContext];
while (contextsToVisit.length) {
  let currentContext = contextsToVisit.pop();
  let global = currentContext.currentWindowGlobal;

  if (!global) {
    continue;
  }

  let actor = global.getActor("PageInfo");
  actor.sendAsyncMessage("PageInfo:getInfoForFrame", { someArgument: 123 });

  contextsToVisit.push(...currentContext.children);
}

原始的 "PageInfo:info" 消息侦听器也需要更新。来自 PageInfoChild Actor 的任何响应最终将传递到 PageInfoParent Actor 的 receiveMessage 方法。需要将这些信息传递给相关方(在本例中,是显示有趣页面信息表的对话框代码)。

可能需要重构或重新架构消息管理器消息的原始发送方和接收方,以适应 JSWindowActor 模型。有时,拥有一个管理所有 JSWindowActorParent 实例并在其结果上执行某些操作的单例管理对象也很有帮助。

在何处存储状态

JSWindowActorChild 中存储任何希望在其 BrowsingContext 生命周期之外持续存在的状态不是一个好主意。进程外的 <iframe> 可以随时关闭,如果它是特定内容进程的唯一一个,那么该内容进程很快就会关闭,并且您可能存储在那里的任何状态都将消失。

存储状态的最佳方法是在父进程中。

JSWindowActors 由 WindowGlobal IPC Actor “管理”,并作为 JS 类(JSWindowActorParentJSWindowActorChild 的子类)实现,并在为任何特定窗口请求时实例化。与框架消息管理器一样,它们最终使用 IPC Actor 在后台进行通信。

如果每个单独的框架都需要状态,请考虑在父进程中使用 WeakMap,将 CanonicalBrowsingContext 与该状态映射。这样,如果关联的框架消失,您就不必自己进行任何清理。

如果您有希望多个 JSWindowActorParent 能够访问的状态,请考虑在同一个 .jsm 文件中拥有这些 JSWindowActorParent 的“管理器”来保存该状态。

注册新的 Actor

ChromeUtils 公开了用于注册 Actor 的 API,但 BrowserGlueActorManagerParent 都是注册发生的主要入口点。如果您想注册 Actor,则应将其添加到 JSPROCESSACTORSJSWINDOWACTORS 中的这两个文件中的任何一个。

JS*ACTORS 对象中,每个键都是 Actor 对的名称(例如:ContextMenu),关联的值是注册参数的 Object

完整的注册参数列表可以在以下位置找到

  • 对于 JSProcessActor,在文件 JSProcessActor.webidl 中作为 WindowActorOptionsProcessActorSidedOptionsProcessActorChildOptions

  • 对于 JSWindowActor,在文件 JSWindowActor.webidl 中作为 WindowActorOptionsWindowActorSidedOptionsWindowActorChildOptions

这是一个从 BrowserGlue.sys.mjs 中提取的 JSWindowActor 注册示例

Plugin: {
   kind: "JSWindowActor",
   parent: {
     esModuleURI: "resource:///actors/PluginParent.sys.mjs",
   },
   child: {
     esModuleURI: "resource:///actors/PluginChild.sys.mjs",
     events: {
       PluginCrashed: { capture: true },
     },

     observers: ["decoder-doctor-notification"],
   },

   allFrames: true,
 },

此示例适用于 GMP 崩溃报告的 JSWindowActor 实现。

让我们检查一下父注册

parent: {
  esModuleURI: "resource:///actors/PluginParent.sys.mjs",
},

在这里,我们声明类 PluginParent(此处,JSWindowActorParent 的子类)已定义并从模块 PluginParent.sys.mjs 导出。对于父(主进程)方面,这就是我们要说的全部。

注意

仅将新的 .jsm 文件添加到 actors 子目录是不够的。您还需要更新同一目录中的 moz.build 文件以正确设置 resource:// 链接。

让我们看一下第二块

  child: {
    esModuleURI: "resource:///actors/PluginChild.sys.mjs",
    events: {
      PluginCrashed: { capture: true },
    },

    observers: ["decoder-doctor-notification"],
  },

  allFrames: true,
},

我们同样声明了可以在哪里找到继承 JSWindowActorChildPluginChild

接下来,我们声明内容事件,当这些事件在窗口中触发时,将导致JSWindowActorChild实例化(如果它尚不存在),然后在PluginChild实例上调用handleEvent。对于每个事件名称,可以传递一个事件侦听器选项对象。您可以使用与addEventListener接受的相同的事件侦听器选项。如果事件侦听器在 actor 尚未创建时没有产生任何有用的效果,也可以指定createActor: false 以避免在不需要时创建 actor。

注意

内容事件对于JSWindowActorChild具有内容)是有意义的,但对于JSProcessActorChild(没有内容)会被忽略。

接下来,我们声明PluginChild应该观察decoder-doctor-notification nsIObserver 通知。当该观察者通知触发时,将为对应于作为观察者通知主题参数的内部或外部窗口的BrowsingContext实例化PluginChild actor,并且将调用该PluginChild实现上的observe方法。如果您需要此功能与其他主题一起使用,请提交错误报告。

注意

JSWindowActorChild子类不同,为JSProcessActorChild子类指定的观察者主题将导致创建这些子 actor 实例并调用其observe方法,无论观察者的主题参数是什么。

最后,我们说PluginChild actor 应该应用于allFrames。这意味着PluginChild允许加载到任何子框架中。如果allFrames设置为 false(默认值),则 actor 仅会在顶级框架中加载。

添加新 actor 时的设计注意事项

在添加您自己的 actor 注册时,需要注意以下几点

  • 您注册的任何childparent端**必须**具有moduleURI属性。

  • 您不需要同时拥有childparent模块,并且应该避免拥有除了发送消息之外什么都不做的 actor 端。没有定义模块的进程仍然会获得一个 actor,并且您可以从该端发送消息,但不能通过receiveMessage接收它们。请注意,您**也可以**从该端使用sendQuery,使您能够处理来自其他进程的响应,即使没有receiveMessage方法。

  • 如果您正在编写 JSWindowActor,请考虑您是否真的需要allFrames - 如果我们不需要为子框架实例化 actor,它将节省内存和 CPU 时间。

  • 在复制/移动“旧版”消息管理器 Actor时,请删除其messages属性。它们不再需要。

最小示例 Actor

获取 JSWindowActor

定义一个 Actor

// resource://testing-common/TestWindowParent.jsm
var EXPORTED_SYMBOLS = ["TestWindowParent"];
class TestParent extends JSWindowActorParent {
  ...
}
// resource://testing-common/TestWindowChild.jsm
var EXPORTED_SYMBOLS = ["TestWindowChild"];
class TestChild extends JSWindowActorChild {
  ...
}

获取特定窗口的 JS 窗口 actor

// get parent side actor
let parentActor = this.browser.browsingContext.currentWindowGlobal.getActor("TestWindow");

// get child side actor
let childActor = content.windowGlobalChild.getActor("TestWindow");

获取 JSProcessActor

定义一个 Actor

// resource://testing-common/TestProcessParent.jsm
var EXPORTED_SYMBOLS = ["TestProcessParent"];
class TestParent extends JSProcessActorParent {
  ...
}
// resource://testing-common/TestProcessChild.jsm
var EXPORTED_SYMBOLS = ["TestProcessChild"];
class TestChild extends JSProcessActorChild {
  ...
}

获取特定进程的 JS 进程 actor

// get parent side actor
let parentActor = this.browser
  .browsingContext
  .currentWindowGlobal
  .domProcess
  .getActor("TestProcess");

// get child side actor
let childActor = ChromeUtils.domProcessChild
  .getActor("TestProcess");

以及更多