异步平移和缩放

本文档尚未完善。某些信息可能缺失或不完整。

../_images/AsyncPanZoomArchitecture.png

目标

我们需要能够以最小的延迟提供对用户输入的视觉响应。特别是对于具有触摸输入的设备,内容在平移时必须精确地跟踪手指,否则用户体验会非常糟糕。根据 UX 团队的说法,用户输入和响应之间 120 毫秒的延迟是可以接受的。

上下文和周围架构

我们试图用异步平移和缩放 (APZ) 代码解决的基本问题是响应性。默认情况下,网页浏览器在“游戏循环”中运行,其流程如下所示

while true:
    process input
    do computations
    repaint content
    display repainted content

在浏览器中,“执行计算”步骤可能任意昂贵,因为它可能涉及在网页内容中运行事件处理程序。因此,在接收输入和屏幕显示更新之间可能存在任意延迟。

响应性始终是好的,并且在基于触摸的交互中,它甚至比鼠标或键盘输入更重要。为了确保响应性,我们将浏览器的“游戏循环”模型拆分为一个多线程变体,其流程类似于以下所示

Thread 1 (compositor thread)
while true:
    receive input
    send a copy of input to thread 2
    adjust rendered content based on input
    display adjusted rendered content

Thread 2 (main thread)
while true:
    receive input from thread 1
    do computations
    rerender content
    update the copy of rendered content in thread 1

这种多线程模型称为离主线程合成 (OMTC),因为合成(内容在屏幕上显示的位置)发生在与主线程不同的线程上。请注意,这是一个非常简化的模型,但在此模型中,“根据输入调整渲染内容”是 APZ 代码的主要功能。

关于 APZ 与其他浏览器架构改进的关系,需要注意以下几点

  1. 由于 Electrolysis (e10s)、Site Isolation (Fission) 和 GPU 进程隔离,上述两个线程通常实际上在不同的进程中运行。APZ 在很大程度上与之无关,因为出于 APZ 目的,这两个线程之间所有通信都使用 IPC 层,该层抽象了线程与进程之间的通信。

  2. 使用 WebRender 图形后端,渲染管道的部分也从主线程卸载。在此架构中,从主线程发送的信息包括显示列表和引用该显示列表中内容的与滚动相关的元数据。元数据保存在队列中,直到显示列表在合成器(场景构建)中进行额外的渲染步骤。此时,我们已准备好告诉 APZ 新内容并使其开始对其应用调整,因为场景构建之外的进一步渲染步骤在每个合成上同步完成。

理论上,合成器可以连续将先前渲染的内容(由 APZ 在每个合成中调整)合成到屏幕上,而主线程则忙于执行其他操作和渲染新内容。

APZ 代码获取来自硬件的输入事件,并使用它们来确定用户试图做什么(例如,平移页面、放大)。然后,它以平移和/或缩放变换矩阵的形式表达此用户意图。这些变换矩阵在合成时应用于渲染内容,以便用户在屏幕上看到的内容尽可能地反映他们想要执行的操作。

技术概述

根据上面描述的简化模型,APZ 代码的基本目的是获取输入事件并生成变换矩阵。本节试图将其分解并识别使此任务不平凡的不同问题。

棋盘格效应

为其构建显示列表并发送到合成器的页面内容区域称为“显示端口”。APZ 代码负责确定显示端口的大小。一方面,我们希望显示端口尽可能大。至少它需要大于屏幕上可见的部分,因为否则,一旦用户平移,就会显示页面的一些未绘制区域。但是,我们不能总是将显示端口设置为整个页面,因为页面可以任意长,这将需要无限量的内存来存储。因此,一个好的显示端口大小是大于可见区域但不会占用大量内存的大小。由于显示端口通常小于整个页面,因此用户始终可以快速滚动到页面显示端口之外的区域。发生这种情况时,他们会看到未绘制的内容;这称为“棋盘格效应”,我们尽量避免这种情况。

有很多可能的方法来确定显示端口的大小,以平衡所涉及的权衡(即,显示端口太大不利于内存使用,而显示端口太小会导致过度棋盘格效应)。理想情况下,显示端口应覆盖我们知道用户将使其可见的区域。虽然我们无法确定这一点,但我们可以使用基于当前平移速度和方向的启发式方法来确保选择合理的显示端口区域。此计算在 APZ 代码中完成,并且在用户四处平移时,会频繁地将新的所需显示端口发送到主线程。

多个可滚动元素

例如,考虑一个包含 iframe 的可滚动页面,而 iframe 本身也是可滚动的。可以独立于顶级页面滚动 iframe,并且我们希望页面和 iframe 都可以响应地滚动。这意味着我们希望顶级页面和 iframe 都具有独立的异步平移。除了 iframe 之外,设置了 overflow:scroll CSS 属性的元素也是可滚动的。在显示列表中,可滚动元素以树结构排列,在 APZ 代码中,我们有一个匹配的 AsyncPanZoomController (APZC) 对象树,每个可滚动元素对应一个。为了管理此 APZC 实例树,我们有一个单独的 APZCTreeManager 对象。每个 APZC 相对独立并处理其关联的可滚动元素的滚动,但某些情况下它们需要交互;这些情况在下面的部分中描述。

命中检测

再次考虑我们有一个包含 iframe 的可滚动页面,而 iframe 本身也是可滚动的这种情况。如上所述,我们将有两个 APZC 实例 - 一个用于页面,另一个用于 iframe。当用户将手指放在屏幕上并移动它时,我们需要进行某种命中检测以确定他们的手指是在 iframe 上还是在顶级页面上。根据他们手指落在的位置,相应的 APZC 实例需要处理输入。

此命中检测由 APZCTreeManager 与 WebRender 协作完成,WebRender 拥有比 APZ 中直接存储的页面内容结构更详细的信息。有关更多详细信息,请参阅此部分

另请注意,对于某些类型的输入(例如,当用户放下两根手指进行捏合时),我们不希望输入在两个不同的 APZC 实例之间“拆分”。例如,在捏合的情况下,我们找到一个“共同祖先”APZC 实例 - 一个可缩放且包含所有触摸输入点的实例,并将输入定向到该 APZC 实例。

滚动切换

再次考虑我们有一个包含 iframe 的可滚动页面,而 iframe 本身也是可滚动的这种情况。假设用户滚动 iframe 直到其到达底部。如果用户继续在 iframe 上平移,则预期顶级页面将开始滚动。但是,如命中检测部分所述,iframe 的 APZC 实例与顶级页面的 APZC 实例是分开的。因此,我们需要这两个 APZC 实例以某种方式进行通信,以便 iframe 上的输入事件导致顶级页面滚动。此行为称为“滚动切换”(或在类似行为由用户抬起手指后页面的滚动动量导致的情况下称为“抛掷切换”)。

输入事件反变换

根据定义,APZC 架构导致每个可滚动元素的“滚动位置”有两个副本。一个是在主线程上的原始副本,可供网页内容以及布局和绘制代码访问。另一个是在合成器端,根据用户输入异步更新,并对应于用户在屏幕上看到的视觉效果。虽然这两个副本可能暂时发生偏差,但它们会定期协调。特别是,当 APZ 代码代表用户执行异步平移或缩放操作时,它们会发生偏差,并且当 APZ 代码请求主线程重绘时,它们会协调。

由于输入事件的表示方式,这会带来一些不利后果。输入事件坐标相对于设备屏幕表示 - 因此,如果用户触摸设备上的相同物理位置,则无论内容滚动位置如何,都会传送相同的输入事件。当主线程接收触摸事件时,它会将其与内容滚动位置组合以确定用户触摸了哪个 DOM 元素。但是,由于我们现在有两个不同的滚动位置,因此此过程可能无法完美运行。下面是一个具体的例子

考虑一个屏幕尺寸为 600 像素高的设备。在此设备上,用户正在查看高度为 1000 像素的文档,并且已向下滚动 200 像素。也就是说,文档从 200px 到 800px 的垂直部分可见。现在,如果用户触摸物理显示器顶部 100px 的一点,硬件将生成一个 y=100 的触摸事件。这将被发送到主线程,主线程将添加滚动位置 (200) 并获得一个文档相关的触摸事件,y=300。此新的 y 值将用于命中检测以确定用户触摸了什么。如果文档在 y=300 处有一个绝对定位的 div,则该 div 将接收触摸事件。

现在让我们在此示例中添加一些异步滚动。假设用户另外异步滚动文档另外 10 像素(即仅在合成器线程上),然后执行相同的触摸事件。硬件会生成相同的输入事件,并且与之前一样,文档会将触摸事件传递给 y=300 处的 div。但是,从视觉上看,文档额外滚动 10 像素,因此此结果是错误的。需要发生的是 APZ 代码需要拦截触摸事件并考虑 10 像素的异步滚动。因此,在传递到主线程之前,输入事件 y=100 在 APZ 代码中转换为 y=110。然后主线程添加它知道的滚动位置并确定用户触摸了文档相关的 y=310 位置。

对于水平滚动和缩放,需要执行类似的输入事件变换。

内容独立调整滚动

如上所述,在 APZ 架构中,滚动位置有两个副本 - 一个在主线程上,另一个在合成器线程上。通常对于此类架构,只有一个“真实来源”值,而另一个值只是一个副本。但是,在这种情况下,不容易做到这一点。原因是这两个值都可以合法地修改。在合成器端,用户触发的输入事件修改滚动位置,然后将其传播到主线程。但是,在主线程上,网页内容可能正在运行以编程方式设置滚动位置的 Javascript 代码(例如,通过 window.scrollTo)。从主线程驱动的滚动更改同样合法,需要传播到合成器线程,以便视觉显示相应更新。

由于跨线程消息传递是异步的,因此协调这两种类型的滚动更改是一个棘手的问题。我们的设计使用各种标志和生成计数器来解决此问题。我们的一般启发式方法是内容驱动的滚动位置更改(例如,来自 JS 的 scrollTo)永远不会丢失。例如,如果用户正在用手指进行异步滚动,并且内容在中间执行了 scrollTo,则一些异步滚动将在“跳转”之前发生,其余部分在“跳转”之后发生。

内容阻止输入事件的默认行为

我们需要处理的另一个问题是,网页内容被允许拦截触摸事件并阻止滚动的“默认行为”。此功能在 Web 标准中定义,不可协商。网页内容中的触摸事件监听器被允许在触摸点的 touchstart 或第一个 touchmove 事件上调用 preventDefault();这样做应该会“消耗”事件并阻止基于触摸的平移。正如我们在上一节中看到的,输入事件需要在 APZ 代码中进行去转换,然后才能传递给内容。但是,由于 preventDefault 问题,我们无法在 APZ 代码中完全处理触摸事件,直到内容有机会处理它。

为了平衡正确性(要求允许网页内容在需要时成功阻止事件的默认处理)和响应性(要求避免在对事件做出反应之前无限期地阻塞网页内容的 JavaScript)的需求,APZ 为网页内容提供了一个“截止时间”来处理事件并告知 APZ 是否在事件上调用了 preventDefault()。在桌面上,截止时间是从 APZ 接收事件的时间起 400 毫秒,在移动设备上是 600 毫秒。如果网页内容能够在此截止时间前处理事件,则会尊重是否阻止事件的决定。如果网页内容未能在截止时间前处理事件,则 APZ 假设不会调用 preventDefault(),并继续处理事件。

为了实现这一点,在收到触摸事件后,APZ 会立即返回一个可以分派到内容的未转换版本。它还会安排 400 毫秒或 600 毫秒的超时。有一个 API 允许主线程事件分派代码通知 APZ 是否应该阻止默认操作。如果 APZ 内容响应超时或主线程事件分派代码通知 APZ preventDefault 状态,则 APZ 继续处理事件(可能包括丢弃事件)。

为了限制此往返内容操作对响应性的影响,APZ 尝试识别可以排除 preventDefault() 作为可能结果的情况。为此,发送到合成器的命中测试信息包括有关页面哪些区域被具有触摸事件监听器的元素占据的信息。如果事件的目标区域位于这些区域之外,则可以排除 preventDefault(),并跳过往返操作。

此外,Web 标准的最新增强功能为页面作者提供了可以进一步限制 preventDefault() 对响应性影响的新工具。

  1. 事件监听器可以注册为“被动”,这意味着它们不允许调用 preventDefault()。作者可以在编写仅需要观察事件而不需要通过 preventDefault() 更改其行为的监听器时使用此标志。被动事件监听器的存在不会导致 APZ 执行内容往返操作。

  2. 如果页面作者希望完全禁用某些类型的触摸交互,他们可以使用来自指针事件规范的 touch-action CSS 属性以声明方式执行此操作,而不是注册调用 preventDefault() 的事件监听器。触摸操作标志也包含在发送到合成器的命中测试信息中,APZ 使用此信息来尊重 touch-action。(请注意,发送到合成器的触摸操作信息并不总是 100% 准确,有时 APZ 需要回退到向主线程请求触摸操作信息,这再次涉及往返操作。)

其他事件类型

以上各节主要讨论触摸事件,但随着时间的推移,APZ 已扩展到处理各种其他事件类型,例如触控板和鼠标滚轮滚动、滚动条拇指拖动以及某些情况下的键盘滚动。上述许多内容也适用于这些其他事件类型(例如,也可以阻止轮事件的默认操作)。

重要的是,即使对于 APZ 未处理的事件类型(例如鼠标单击事件),上述“去转换”也需要发生,因为异步滚动仍然会影响此类事件的正确目标。

技术细节

本节描述 APZ 代码的各个部分,并比上一节更详细地介绍 API 和代码。本节的主要目的是帮助计划更改代码的人员,同时又不至于过于详细,以至于需要在每次修补程序中更新。

输入事件的总体流程

本节描述输入事件如何在 APZ 代码中流动。

免责声明:本节中的一些详细信息已过时(例如,它假设主线程和合成器线程位于同一进程中,这在当今很少见,因此在实践中,例如步骤 6 和 8 涉及 IPC,而不仅仅是“堆栈展开”)。

  1. 输入事件从硬件/窗口小部件代码通过 APZCTreeManager::ReceiveInputEvent 传递到 APZ。调用此方法的线程称为“控制器线程”,它可能与 Gecko 主线程相同,也可能不同。

  2. 从概念上讲,APZCTreeManager 最先做的事情是将这些事件与“输入块”关联起来。输入块是一组共享某些属性的事件,通常旨在表示单个手势。例如,对于触摸事件,从 touchstart 开始到下一个 touchstart 之前的事件都属于同一块。给定块中的所有事件都将发送到同一个 APZC 实例,并且要么全部处理,要么全部丢弃。

  3. 使用输入块中的第一个事件,APZCTreeManager 进行命中测试以查看它命中了哪个 APZC。如果没有命中任何 APZC,则会丢弃事件,然后跳到步骤 6。否则,输入块将用命中的 APZC 标记为临时目标,并放入全局 APZ 输入队列中。除了目标 APZC 之外,命中测试的结果还包括输入事件是否落在“分派到内容”区域。这些是页面上正在发生某些事情的区域,这些事情需要将事件分派到内容并等待响应_然后_在 APZ 中处理事件;例如包含具有非被动事件监听器的元素的区域,如上所述。(待办事项:添加一个部分来讨论“分派到内容”机制的其他用途。)

    1. 如果输入事件落在“分派到内容”区域之外,则处理输入块中的任何可用事件。这些事件可能会触发诸如滚动或点击手势之类的行为。

    2. 如果输入事件落在“分派到内容”区域内,则这些事件将保留在队列中,并启动一个超时。如果超时在步骤 9 完成之前到期,则 APZ 假设输入块未被取消,并且临时目标是正确的,并将其作为步骤 10 的一部分进行处理。

  4. 调用堆栈回溯到 APZCTreeManager::ReceiveInputEvent,它会对输入事件进行就地修改,以便删除任何异步转换。

  5. 调用堆栈回溯到调用 ReceiveInputEvent 的窗口小部件代码。此代码现在拥有 Gecko 预期的坐标空间中的事件,因此可以将其分派到 Gecko 主线程。

  6. Gecko 对事件执行其自身的常规命中测试和事件分派。作为此过程的一部分,它会记录是否有任何触摸监听器通过调用 preventDefault() 取消了输入块。它还会激活被输入事件命中的非活动滚动框架。

  7. 调用堆栈回溯到窗口小部件代码,该代码向控制器线程上的 APZ 代码发送两个通知。第一个通知通过 APZCTreeManager::ContentReceivedInputBlock 发送,并通知 APZ 输入块是否被取消。第二个通知通过 APZCTreeManager::SetTargetAPZC 发送,并通知 APZ 事件分派期间 Gecko 命中测试的结果。请注意,Gecko 可能会报告输入事件根本没有命中任何可滚动框架。SetTargetAPZC 通知每个输入块仅发生一次,而 ContentReceivedInputBlock 通知可能每个块发生一次,或者每个块多次发生,具体取决于输入类型。

    1. 如果事件作为步骤 4(i) 的一部分进行处理,则会忽略步骤 8 中的通知,并跳过步骤 10。

    2. 如果事件作为步骤 4(ii) 的一部分排队,并且步骤 5-8 在超时之前完成,则步骤 8 中两个通知的到达将标志输入块已准备好进行处理。

    3. 如果事件作为步骤 4(ii) 的一部分排队,但步骤 5-8 耗时超过超时,则会忽略步骤 8 中的通知,并且步骤 10 已经发生。

  8. 如果事件作为步骤 4(ii) 的一部分排队,则它们现在要么被处理(如果输入块未被取消并且 Gecko 检测到输入事件下方的滚动框架,或者超时到期),要么被丢弃(所有其他情况)。请注意,处理事件的 APZC 在此步骤中可能与步骤 3 中的临时目标不同,具体取决于 SetTargetAPZC 通知。处理事件可能会触发诸如滚动或点击手势之类的行为。

如果启用了 CSS touch-action 属性,则上述步骤将按如下方式修改

  • 在步骤 4 中,APZC 还需要输入事件的允许触摸操作行为。这可能已在 APZCTreeManager 中的命中测试期间确定;如果没有,则会将事件排队。

  • 在步骤 6 中,窗口小部件代码确定输入元素下方的内容元素,并通知 APZ 代码允许的触摸操作行为。此通知通过对输入线程上的 APZCTreeManager::SetAllowedTouchBehavior 的调用发送。

  • 在步骤 9(ii) 中,只有在所有三个通知到达后,输入块才会被标记为已准备好进行处理。

线程考虑因素

APZ 代码中大部分输入处理发生在我们称为“控制器线程”上的线程。在实践中,控制器线程可能是 Gecko 主线程、合成器线程或其他一些线程。使用 Gecko 主线程有明显的缺点 - 也就是说,“异步”平移和缩放并不是真正的异步,因为只有在 Gecko 空闲时才能处理输入事件。在 e10s 环境中,使用 chrome 进程的 Gecko 主线程是可以接受的,因为在该进程中运行的代码比任意网页内容更易于控制和行为良好。使用合成器线程作为控制器线程可以在某些平台上工作,但在其他平台上可能效率低下。例如,在 Android(Fennec)上,我们从系统上的专用 UI 线程接收输入事件。如果我们希望输入线程与合成器线程相同,则必须将输入事件重新分派到合成器线程。这会导致潜在的更高延迟,特别是如果合成器执行任何阻塞操作(例如阻塞 SwapBuffers 操作)。因此,APZ 代码本身不假设控制器线程将与 Gecko 主线程或合成器线程相同。

活动与非活动滚动框架

页面上的滚动框架数量可能是不确定的。但是,我们不想立即为每个滚动框架创建单独的显示端口,因为这需要大量的内存。因此,滚动框架被指定为“活动”或“非活动”。活动滚动框架获得显示端口和合成器端上的 APZC。非活动滚动框架不会获得显示端口(仅为其视口构建显示列表,即当前可见的部分),也不会获得 APZC。

考虑一个页面,其中包含一个最初处于非活动状态的滚动框架。此滚动框架不会获得 APZC,因此针对它的事件将针对最近的活动可滚动祖先的 APZC(我们将其称为 P;请注意,给定进程中最顶层的滚动框架始终处于活动状态)。但是,非活动滚动框架的存在由一个“分派到内容”区域反映,该区域阻止框架上的事件错误地滚动 P。

当用户开始与该内容交互时,APZ 代码中的命中测试会命中 P 的“分派到内容”区域。因此,当输入块进入上述流程中的步骤 4(ii) 时,它将具有 P 的临时目标。当 gecko 处理输入事件时,它必须检测非活动滚动框架并将其激活,作为步骤 7 的一部分。最后,窗口小部件代码在步骤 8 中发送 SetTargetAPZC 通知以通知 APZ 输入块应该真正应用于此新的 APZC。这里的问题是,包含新活动滚动框架元数据的交易必须在 SetTargetAPZC 通知之前到达合成器和 APZ。如果这在 400 毫秒超时内未发生,则 APZ 代码将无法更新临时目标,并将继续对该输入块使用 P。在交易之后开始的输入块将被正确路由到新的滚动框架,因为现在将为此活动滚动框架存在一个 APZC 实例。

此模型意味着,当用户最初尝试滚动非活动滚动框架时,它最终可能会滚动祖先滚动框架。只有在往返到 gecko 线程完成后,才能在滚动框架本身实际发生异步滚动以进行异步滚动。此时,滚动框架将开始接收新的输入块并正常滚动。

注意:使用 Fission(其中非活动滚动框架将使无法在所有情况下定位正确的进程;有关更多详细信息,请参阅 本节)和 WebRender(它使显示端口更轻量级,因为实际渲染被卸载到合成器并且可以按需完成),非活动滚动框架正在逐步淘汰,我们正在转向一个模型,其中所有具有非空滚动范围的滚动框架都处于活动状态,并获得显示端口和 APZC。为了节省内存,最近未滚动过的滚动框架的显示端口将保持为等于视口大小的“最小”尺寸。

WebRender 集成

本节介绍 APZ 如何与 WebRender 图形后端交互。

请注意,APZ 早于 WebRender,最初是编写为与早期的 Layers 图形后端一起工作的。Layers 的设计对 APZ 产生了重大影响,这在代码的某些地方仍然可见。现在 Layers 后端已被删除,可能有机会简化 APZ 和 WebRender 之间的交互。

HitTestingTree

APZCTreeManager 将 HitTestingTreeNode 实例的树作为其内部状态的一部分保留。这称为 HitTestingTree。

HitTestingTree 的主要目的是模拟受异步滚动影响的内容之间的空间关系。树节点大致分为以下几类

  • 表示活动滚动框架中可滚动内容的节点。这些节点与滚动框架的 APZC 相关联。

  • 表示其他内容的节点,这些内容可能会以特殊方式响应异步滚动而移动,例如固定内容、粘性内容和滚动条。

  • (非叶子)节点不表示任何内容,只是应用于其后代节点的元数据(例如转换)。

如果例如滚动框架滚动两个与非滚动内容交错的内容,则一个 APZC 可能与多个节点相关联。

将这些节点排列成树状结构可以模拟关系,例如哪些内容由给定滚动框架滚动,APZC 之间的滚动交接关系是什么,以及哪些内容受哪些转换影响。

HitTestingTree 的另一个用途是允许 APZ 使内容进程了解它们所受的封闭转换。有关更多详细信息,请参阅 本节

(过去,使用 Layers 后端时,HitTestingTree 也用于合成器命中测试,因此得名。现在情况已不再如此,因此可能有机会简化此树。)

HitTestingTree 由另一个称为 WebRenderScrollData 的树数据结构创建。这里相关的类型是

  • WebRenderScrollData,它存储整个树。

  • WebRenderLayerScrollData,它表示内容的单个“层”,即一组在滚动时一起移动的显示项(或应用于此类层子树的元数据)。在 Layers 后端,此类内容将渲染到单个纹理中,然后可以在合成时异步移动。由于内容层可以由多个(嵌套)滚动帧滚动,因此 WebRenderLayerScrollData 可能包含多个滚动帧的滚动元数据。

  • WebRenderScrollDataWrapper,它包装 WebRenderLayerScrollData,但以“扩展”的方式,每个节点仅存储单个滚动帧的元数据。WebRenderScrollDataWrapper 节点与 HitTestingTreeNodes 具有 1:1 的对应关系。

在仅 WebRender 的世界中,WebRenderLayerScrollData 和 WebRenderScrollDataWrapper 之间的区别是否仍然有用尚不清楚。代码可以进行潜在的修改,以便我们直接构建和存储具有 WebRenderScrollDataWrapper 行为的单一类型的节点。

WebRenderScrollData 结构在主线程上构建,然后通过 IPC 发送到合成器,在那里它用于构建 HitTestingTree。

WebRenderScrollData 在 WebRenderCommandBuilder 中构建,在用于构建 WebRender 显示列表的 Gecko 显示列表的相同遍历期间。在撰写本文时,其架构如下:当我们遍历 Gecko 显示列表时,我们查询它以查看它是否包含 APZ 可能需要了解的任何信息(例如 CSS 变换),方法是调用 nsDisplayItem::UpdateScrollData(nullptr, nullptr)。如果此调用返回 true,则我们为该项创建一个 WebRenderLayerScrollData 实例,并在 WebRenderLayerScrollData::Initialize 中使用必要的信息填充它。如果我们检测到(通过 ASR 更改)我们现在正在处理的 Gecko 显示项与前一项位于不同的滚动帧中,我们也会创建 WebRenderLayerScrollData 实例。

此代码中的主要复杂性来源包括

  1. 确保 ScrollMetadata 实例以正确的 WebRenderLayerScrollData 实例结束(以便从叶子 WebRenderLayerScrollData 节点到根的每条路径都具有没有重复的滚动帧的一致顺序)。

  2. StackingContextHelper::mDeferredTransformItem 的声明中更详细地描述的延迟变换优化。

命中测试

由于 HitTestingTree 未用于 WebRender 后端的实际命中测试目的(请参阅上一节),因此本节介绍 WebRender 的命中测试工作原理。

Gecko 显示列表包含存储命中测试状态的显示项 (nsDisplayCompositorHitTestInfo)。这些项实现 CreateWebRenderCommands 方法,并生成“命中测试项”到 WebRender 显示列表中。这基本上只是 WebRender 显示列表中的一个矩形项,对于绘制目的而言是空操作,但包含命中测试应返回的信息(特别是命中信息标志和封闭滚动帧的 scrollId)。命中测试项以与 WebRender 显示列表中的所有其他项相同的方式进行裁剪和变换,通过剪辑链和封闭参考帧/堆叠上下文项。

当 WebRender 需要进行命中测试时,它会遍历其显示列表,考虑当前的剪辑和变换,并根据最新的异步滚动/缩放进行调整,并确定目标点下有哪些命中测试项,并返回这些项。然后,APZ 可以从该列表中获取最前面的项(或者如果它碰巧位于 pointer-events:none 的 OOP 子文档内,则跳过它),并将其用作命中目标。请注意,命中测试使用 SampleForWebRender API 提供的最后一个变换(请参阅下一节),该变换通常反映最后一个合成,并且不考虑此后发生的变换的进一步更改。在实践中,我们应该足够频繁地进行合成,以至于这并不重要。

在调试命中测试问题时,通常很有用的是应用 bug 1656260 上的补丁,这些补丁在 Gecko 显示项上引入了一个 guid 并将其一直传播到 APZ 获取命中测试结果的地方。这允许回答“哪个 nsDisplayCompositorHitTestInfo 导致了此命中测试结果?”这个问题,这通常是解决错误的很好第一步。从那里,可以确定前面是否存在其他应该生成 nsDisplayCompositorHitTestInfo 但未生成的显示项,或者显示项本身的信息是否不正确。该错误上的第二个补丁进一步允许将手写的调试信息公开给 APZ 代码,以便可以更有效地调试 WR 命中测试机制本身,以防 WR 显示项的变换或裁剪不正确。

WebRender 响应命中测试返回给 APZ 的信息足以让 APZ 识别 HitTestingTreeNode 作为事件的目标。然后,APZ 可以采取一些操作,例如滚动目标节点关联的 APZC 或其他适当的操作(例如,如果滚动条拇指节点成为鼠标按下事件的目标,则启动滚动条拖动)。

采样

合成步骤需要读取 APZ 中最新的异步变换,以确保滚动帧以正确的位置渲染。此 API 通过 APZSampler 类公开。当 WebRender 准备好进行合成时,它会调用 APZSampler::SampleForWebRender。在此,APZ 收集 WebRender 需要了解的所有异步变换,包括应用于滚动内容、固定和粘性内容以及滚动条拇指的变换。

除了采样 APZ 变换之外,合成器还会触发 APZ 动画以前进到下一个时间步长(通常是下一个垂直同步)。这发生在读取 APZ 变换之前。

Fission 集成

本节介绍 APZ 如何与 Fission(站点隔离)项目交互。

介绍

Fission 是一种由安全考虑因素驱动的架构更改,其中每个来源的 Web 内容都隔离在其自己的进程中。由于页面可以包含来自不同来源的内容混合(例如,顶级页面可以是来自来源 A 的内容,并且它可以包含来自来源 B 的内容的 iframe),这意味着渲染和交互页面现在可能涉及 APZ 和多个内容进程之间的协调。

输入事件的内容进程选择

输入事件最初在浏览器的父进程中接收。使用 Fission,浏览器需要确定事件的目标可能是多个内容进程中的哪一个。

由于进程边界对应于 iframe(子文档)边界,并且每个(html)文档都有一个根滚动帧,因此进程边界也是滚动帧边界。由于 APZ 已经需要一种命中测试机制才能确定事件的目标是哪个滚动帧,因此这种命中测试机制非常适合用于确定事件的目标是哪个内容进程。

因此,APZ 的命中测试也扩展到服务于此目的。这主要只需要进行小的修改,例如确保 APZ 了解 iframe 的根滚动帧,即使它们不可滚动。由于 APZ 已经需要处理所有输入事件以潜在地应用与异步滚动相关的 untransformations,作为此过程的一部分,它现在还使用识别其目标内容进程的信息标记输入事件。

命中测试精度

在 Fission 之前,APZ 的命中测试可以承受一定程度的不准确性,因为它可以回退到 dispatch-to-content 机制,以等待主线程提供更准确的答案(如果需要),仅承受性能成本(而不是正确性成本)。

使用 Fission,不准确的合成器命中测试现在意味着正确性成本,因为没有跨进程主线程回退机制。(已经考虑过这种机制,但认为它需要过多的复杂性和 IPC 通信量,不值得这样做。)

幸运的是,使用 WebRender,合成器可以获得比使用 Layers 时更多详细信息来用于命中测试。例如,即使存在不规则形状(例如圆角),合成器也可以执行准确的命中测试。

APZ 利用 WebRender 更准确的命中测试能力来准确选择事件的目标进程(以及目标滚动帧)。

一个结果是,dispatch-to-content 机制现在比以前使用得更少了(其主要剩余用途是处理 preventDefault())。

将变换发送到内容进程

内容进程有时需要能够在屏幕坐标和其本地坐标之间进行转换。为此,他们需要了解其包含的 iframe 及其祖先所受的任何变换,包括异步变换(尤其是在异步变换持续不止几帧的情况下)。

APZ 在其 HitTestingTree 中拥有有关这些变换的信息。使用 Fission,APZ 定期向内容进程发送有关这些变换的信息,以便使其保持相对最新。

测试

APZ 利用多个测试框架来验证是否观察到预期的行为。

Mochitest

当需要使用特定内容测试特定手势或事件时,APZ 特定的 mochitests 非常有用。APZ mochitests 位于 gfx/layers/apz/test/mochitest。要运行所有 APZ mochitests,请运行类似以下内容

./mach mochitest ./gfx/layers/apz/test/mochitest

APZ mochitests 通常组织为一组运行的子测试。例如,test_group_hittest-2.html 包含 >20 个子测试,如 helper_hittest_overscroll.html。在处理特定子测试时,通常使用 apz.subtest 首选项将运行的子测试过滤为您正在处理的测试。例如,以下操作只会运行 test_group_hittest-2.html 组的 helper_hittest_overscroll.html 子测试。

./mach mochitest --setpref apz.subtest=helper_hittest_overscroll.html \
    ./gfx/layers/apz/test/mochitest/test_group_hittest-2.html

有关 mochitest 的更多信息,请参阅 Mochitest 文档

GTest

APZ 特定的 GTests 可以在 gfx/layers/apz/test/gtest/ 中找到。要运行这些测试,请运行类似以下内容

./mach gtest "APZ*"

有关更多信息,请参阅 GTest 文档

Reftests

APZ reftests 可以在 layout/reftests/async-scrolling/gfx/layers/apz/test/reftest 中找到。要运行 APZ 的相关 reftests,请运行 APZ reftests 的很大一部分,请运行类似以下内容

./mach reftest ./layout/reftests/async-scrolling/

有关 reftests 的有用信息可以在 Reftest 文档 中找到。

没有定义用于选择 APZ reftests 应放置在哪个目录中的过程,但通常,reftests 应该存在于其他类似测试所在的位置。

线程/锁定概述

线程

与 APZ 相关的三个线程:**控制器线程**、**更新器线程**和**采样器线程**。此表列出了每个平台/配置上哪些线程扮演这些角色

APZ 线程名称

桌面

桌面+GPU

Android

控制器线程

UI 主线程

GPU 主线程

Java UI

更新器线程

SceneBuilder

SceneBuilder

SceneBuilder

采样器线程

RenderBackend

RenderBackend

RenderBackend

APZ 代码中还使用许多锁

锁类型

实例数量

APZ 树锁

每个 APZCTreeManager 一个

APZC 映射锁

每个 APZCTreeManager 一个

APZC 实例锁

每个 AsyncPanZoomController 一个

APZ 测试锁

每个 APZCTreeManager 一个

棋盘格事件锁

每个 AsyncPanZoomController 一个

线程/锁定顺序

为了避免死锁,线程和锁具有必须遵守的全局**顺序**。

遵守顺序意味着以下内容

  • 令“A < B”表示 A 在顺序上早于 B

  • 线程 T 只能获取锁 L,如果 T < L

  • 线程只能在持有锁 L1 的同时获取锁 L2,如果 L1 < L2

  • 一个线程只有在持有锁 L 的情况下,才能阻塞等待另一个线程 T 的响应,前提是 L < T。

锁的顺序如下::

  1. UI 主线程

  2. GPU 主线程(仅当启用 GPU 进程时)

  3. 合成器线程

  4. 场景构建器线程

  5. APZ 树锁

  6. 渲染后端线程

  7. APZC 映射锁

  8. APZC 实例锁

  9. APZ 测试锁

  10. 棋盘格事件锁

示例工作流

以下是一些 APZ 工作流的示例。请注意,它们都遵守全局线程/锁顺序。欢迎添加其他示例。

  • 输入处理(使用 GPU 进程):UI 主线程 -> GPU 主线程 -> APZ 树锁 -> 渲染后端线程

  • 同步消息PCompositorBridge.ipdl 中:UI 主线程 -> 合成器线程

  • GetAPZTestData:合成器线程 -> 场景构建器线程 -> 测试锁

  • 场景切换:场景构建器线程 -> APZ 树锁 -> 渲染后端线程

  • 更新拾取测试树:场景构建器线程 -> APZ 树锁 -> APZC 实例锁

  • 更新 APZC 映射:场景构建器线程 -> APZ 树锁 -> APZC 映射锁

  • 采样和动画延迟任务 [1]:渲染后端线程 -> APZC 映射锁 -> APZC 实例锁

  • 推进动画:渲染后端线程 -> APZC 实例锁