GeckoView 线程拓扑结构

与传统的 Gecko 不同,GeckoView 在父进程中使用了两个主线程。一个名为“UI”线程,运行原生 Java 事件循环;另一个名为“Gecko”线程,生成一个 Gecko 实例。请注意,这个“Gecko”线程在传统的 Gecko 中被认为是主线程,并且在整个源代码中都被这样称呼。例如,NS_IsMainThread() 在 Gecko 线程上调用时返回 true

Android 的辅助功能 API 的入口点是 UI 线程。辅助功能服务查询和交互 Gecko 的最常用方法具有返回值,因此必须同步执行。例如,performAction()createAccessibilityNodeInfo()

线程安全问题

根据规则,Gecko 中的每个数据结构和方法都应被视为在 GeckoView 的 UI 线程中访问是不安全的。例如,默认的引用计数实现不是线程安全的,不应在 Gecko 线程之外使用。跨进程消息传递也必须从 Gecko 线程进行。

由于辅助功能使用者位于 UI 线程,因此我们需要在阻塞 UI 线程的同时调用 Gecko 线程,或者使辅助功能 API 的一部分子集线程安全。我们针对不同类型的內容采用了这两种方法。

进程内内容

在顶级进程中呈现的内容很少,主要包括提供浏览器内部信息(例如 about:support)的“关于”页面。

由于我们的 Gecko 辅助功能 API 查询 DOM 和布局,并且包含许多复杂且线程不安全的数据结构,因此我们调用 Gecko 线程来检索所需内容,并阻塞 UI 直到我们从 Gecko 线程收到响应。例如,要检索为特定辅助功能计算的“类名”枚举,该方法将类似于以下内容

int SessionAccessibility::GetNodeClassName(int32_t aID) {
  int32_t classNameEnum = java::SessionAccessibility::CLASSNAME_VIEW;
  RefPtr<SessionAccessibility> self(this);
  nsAppShell::SyncRunEvent([this, self, aID, &classNameEnum] {
    if (Accessible* acc = GetAccessibleByID(aID)) {
      classNameEnum = AccessibleWrap::AndroidClass(acc);
    }
  });

  return classNameEnum;
}

进程外内容

大多数 Web 内容将在子进程中呈现。在历史上,我们会将辅助功能树层次结构和对象角色缓存到父进程中,并使用同步 IPC 来查询各个对象的更多属性。我们正在转向一种完全缓存的方法,其中所有字段都存储或计算在父进程中的远程代理对象中。

在我们定义中,“缓存”是指与远程 Accessible 或其子类(例如 RemoteAccessibleBase 中的 mParentmChildrenmCachedFields,或 DocAccessibleParent 中的 mAccessibles)关联的任何数据成员。除了所有这些成员外,缓存还包括 SessionAccessibility 中的辅助功能/ID 表。

缓存的初始化和修改完全由来自子进程到父进程的消息完成。由于缓存仅限于我们的模块并且范围相对较好,因此可以同步对它的访问,并允许 UI 线程使用它。

全局辅助功能线程监视器

我们有一个全局线程监视器,可以通过调用 nsAccessibilityService::GetAndroidMonitor() 来获取。例如,如果我们要检索类名枚举,我们将使用 RAII 包装的监视器来独占访问缓存

int SessionAccessibility::GetNodeClassName(int32_t aID) {
  MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
  if (Accessible* acc = GetAccessibleByID(aID)) {
    return AccessibleWrap::AndroidClass(acc);
  }
}

Gecko 线程访问

根据规则,所有传入的 IPC 消息(即所有 DocAccessibleParent::Recv 方法)都应持有全局监视器。这是因为任何 Recv 方法都可能间接修改缓存,例如,某个事件可能会使缓存的属性失效或更新。目前没有因 Recv 方法而发生的延迟任务,因此在这些方法的开头持有监视器将确保以同步方式读取或写入缓存。

UI 线程访问

通过 JNI 调用的所有方法都应假定位于 UI 线程中,并应获取监视器,除非它们使用 @WrapForJNI(dispatchTo = "gecko") 进行注释。这确保了它们与 Gecko 线程中发生的任何潜在缓存修改同步。