【问题标题】:What are the cause(s) of input touch/display lag in android?android中输入触摸/显示滞后的原因是什么?
【发布时间】:2014-01-07 21:09:15
【问题描述】:

我有一个非常简单的应用程序,它使用 opengl 渲染一个正方形,输入触摸由 GLSurfaceView 读取,最后位置使用 volatile 变量与渲染线程交换。

我观察到(在https://www.mail-archive.com/android-developers@googlegroups.com/msg235325.html 中也有很好的描述)是触摸位置和显示之间存在延迟(滞后)。

当激活开发者选项以显示触摸位置时,我看到快速移动时:

  • 与我的手指位置相比,系统显示的跟踪触摸位置的圆圈延迟了 1 厘米。我认为这是系统/硬件相关的,无法由应用程序纠正。

  • 但在我的应用程序中呈现的运动在调试圈之后也延迟了 1 厘米。 所以实际上延迟是可能的两倍。

当手指移动变慢时,绘图的最终位置停止,当前触摸位置同步。完全如本视频所示(上述讨论中的链接)http://www.youtube.com/watch?v=fWZGshsXDhM

我仍然对导致第二次延迟的原因一无所知,我已经确定了从 onTouchEvent 到渲染线程的位置交换时间,它只有几毫秒。 渲染频率为 18 毫秒。

我曾尝试使用速度通过预测 18 毫秒后的位置来缩短延迟的出现,但它并没有改变任何延迟感知。

有人解释这种延迟吗? 在 onTouchEvent 中访问之前是由系统层引起的吗?其中 万一我看不出有什么办法纠正它。

google 群里没有具体的答案。另外困扰我的是,在线程中提到延迟通过使用画布而不是OpenGL而消失。这提出了更多它回答的问题。

【问题讨论】:

  • 这是输入延迟,这是与异步 CPU/GPU 操作相关的经典问题。大多数 GL 实现将接受并从命令返回,无论它们是否完整,这可能导致在任何给定时间排队的多帧命令。在用户输入产生的命令进入最终显示的图像之前,所有其他排队的命令必须首先完成。另外,您设备的显示屏是否真的以刷新率或 55.5 Hz 运行,或者您是否随意选择了 18 毫秒作为更新间隔?
  • 您可以在战略点发出glFinish (...) 以减少“提前渲染”的数量,但您确实违反了实时计算机图形的现代设计原则......保持 CPU 和 GPU 异步工作。这在here进行了简要讨论。
  • @Andon Tbe 的行为看起来确实如您所描述的那样:好像帧已排队。按照您的建议,我尝试调用 glFinish 和 glFlush 但它并没有改变行为。 18ms的时间是平均值,我只是测量一下。

标签: android multithreading opengl-es touch lag


【解决方案1】:

我最近一直在研究同样的事情。这里对 Android 图形堆栈内部有一个很好的解释:

https://source.android.com/devices/graphics/architecture.html

Android 上的 SurfaceView 本质上代表缓冲区队列的生产者端。这些缓冲区的使用者是 SurfaceFlinger,它负责更新在下一个 vsync 上显示的表面。关键是这是一个队列。执行 eglSwapBuffers 实际上不会阻塞除非队列已满。这意味着如果您的渲染很简单,GLSurfaceView 会非常快速地渲染前几帧,填充缓冲区队列,然后进入“每 16 毫秒渲染一次”的稳定状态。

您可以使用 systrace 查看队列中已填充缓冲区的数量。您会注意到 GLSurfaceView 通常以队列中的 1 帧开始一个 vsync 周期,当您的渲染函数完成时,这会上升到 2,这意味着您需要等待 2 个 vsync 才能显示(实际上是 3,我认为,因为SurfaceFlinger 为合成添加了另一个,但每个人都一样)。

Canvas 是按需绘制而不是连续绘制,并且由于该绘制通常以远低于显示更新率的速度进行,因此该窗口的 BufferQueue 通常为空,因此 SurfaceFlinger 在下一次 vsync 时立即使用新内容。

您可以在 vsync 上注册回调,这样您的渲染永远不会超过显示刷新率。这样就避免了 GLSurfaceView 连续渲染的“尽可能快地填满队列”的效果。但是我不确定是否保证队列不会填满,因为 SurfaceFlinger 可能会跳过一个节拍,从而导致您的队列无论如何都会增长。我在这里有一个问题:

Minimize Android GLSurfaceView lag

我已经完成了一个测试,我在 vsync 上进行渲染,但每隔几百帧就会跳过几帧,以确保队列正确清空。在这种情况下,GL 渲染的对象确实与“指针轨迹”覆盖十字准线同步移动。 “调试圈”似乎仍然略微领先 - 我认为这是在 SurfaceFlinger 绘制表面的合成过程中作为简单的图层重新定位实现的,这将使其在其他渲染之前增加一帧。

【讨论】:

    【解决方案2】:

    据我所知,这个问题与输入延迟或 OpenGL 绘图无关。我的猜测是您的线程之间存在同步问题。

    输入事件通过 Android UI 线程发出,而绘图发生在应用程序的 OpenGL 线程中。假设您的测试应用程序像您链接的(优化的)示例一样完成,您将在 UI 线程中设置 xy 变量,并在绘图线程中使用它们而不同步。在多核 CPU 上(如在 S3 等现代手机中),这些线程很可能在不同的内核上运行(因为 OGL 线程一直处于活动状态)。所以每个核心都有一个缓存版本的 xy 变量,不能保证立即更新到另一个核心。

    这个问题可以通过设置变量volatile来解决。来自 Java 7 规范:

    Java 编程语言允许线程访问共享变量(第 17.1 节)。作为一项规则,为了确保共享变量始终如一且可靠地更新,线程应该通过获取一个锁来确保它对这些变量具有独占性,该锁通常会强制这些共享变量互斥。

    Java 编程语言提供了第二种机制,即 volatile 字段,在某些情况下它比锁定更方便。

    一个字段可能被声明为 volatile,在这种情况下,Java 内存模型确保所有线程都能看到变量的一致值(第 17.4 节)。

    您链接的组中的最后一条评论也建议了此解决方案(它可能是最后一条评论,因为它有效)。它还解决了使用两个始终一起使用和更新的易失性变量的问题(您有时可能会看到第一个变量的效果在一个帧中更新但第二个变量没有)。最好的解决方案是为此使用一个 volatile 变量,或者应用一种也能确保原子性的锁定机制。

    我自己编写了几个 OpenGL 应用程序,从来没有遇到过这种“输入延迟”问题,一直使用 volatile 变量。当然,您必须期望显示比输入晚一帧,因为您每帧只更新一次输入数据。所以你的对象在调试圈后面一帧听起来很正常。

    【讨论】:

    • 正如你所描述的,我已经在使用 volatile 变量,所以这应该不是同步问题。
    猜你喜欢
    • 2019-06-28
    • 2011-07-11
    • 2020-01-26
    • 2020-09-24
    • 1970-01-01
    • 2012-10-05
    • 2014-10-17
    • 1970-01-01
    • 2011-06-20
    相关资源
    最近更新 更多