Android 手写延迟优化(一):利用前缓冲快速上屏

2023-01-03 00:06:20 浏览数 (3)

背景

虽然 Android 的大屏生态和 iPadOS 相比不怎样,但随着移动互联网进入下半场,卷无可卷之下,各厂商纷纷在大屏生态方面各显神通,在 Android Pad、折叠屏等产品领域推陈出新。

这些设备往往打着生产力工具的卖点,和 iPad 一样配备了手写笔。但这些设备的书写体验却往往差强人意,往往是自家应用能做到及格线以上,第三方 APP 却很难实现像 iPadOS 上顺滑跟手的效果。

为了改善这种情况,Android 开发团队决定亲自上场,为开发者打个样,从两个方面辅助开发者解决这个让人头疼的延迟问题:

  1. 优化视觉反馈延迟
  2. 书写轨迹预测

本篇文章主要介绍视觉反馈延迟优化相关技术。

视觉反馈延迟优化原理

Low latency graphic,直译过来就是低延迟图形,这是 Android 团队提供的辅助开发者快速将用户输入的轨迹绘制到屏幕上的工具库,降低从手写输入到渲染上屏幕这个过程的耗时。

常规的手写笔迹绘制流程,从「获取到输入」到「绘制上屏」,在系统层面需要经过多个步骤的处理。我们知道,Android 采用多缓冲的方式进行渲染,同一时间内一般存在两个缓冲:

  1. 显示缓冲(Display Buffer):这块缓冲里面包含了用户在屏幕上看到图像数据
  2. 渲染缓冲(Rendering Buffer):程序的所有绘制操作并不会直接操作显示缓冲,程序的绘制最终修改的都是渲染缓冲,等整个画面都绘制完,再把渲染缓冲和显示缓冲进行交换,整体上屏。
双缓冲渲染机制双缓冲渲染机制

这种机制能够有效协调每一帧画面的绘制,保证有流畅的用户体验,也能有效避免因渲染直接上屏导致出现画面出现撕裂的问题。关于渲染的双缓冲机制可以参考官方的 Project Butter 。

但天底下没有免费的午餐,双缓冲机制的引入带来了延迟:从用户输入到最终绘制上屏至少有一帧的延迟,考虑到过程中的其他操作引入的耗时,实际延迟会更加严重。

前缓冲渲染:直接上屏

https://source.android.com/docs/core/graphics

为了优化双缓冲带来的延迟,低延迟视觉库引入了前缓冲技术,这个技术在双缓冲的基础上,增加了一个前缓冲图层(front buffer layer)。这个前缓冲图层盖在双缓冲图层的前面,它是透明的,且只会显示很短的一段时间。

前缓冲图层和双缓冲图层运作机制前缓冲图层和双缓冲图层运作机制

应用通过将用户的输入直接绘制到前缓冲上,实现快速上屏,在最短的时间内给到用户视觉上的反馈。由于前缓冲只会显示很短的一段时间,所以实际的操作结果还需要通过原来的方式,固化到双缓冲的图层上,替换前缓冲。

可能有人会问:既然前缓冲能快速上屏,直接用前缓冲图层绘制就行,为什么还需要用双缓冲图层?

答案很简单:前缓冲图层之所以能够快速上屏,是因为它抛弃了双缓冲变成了单缓冲,单缓冲的最大问题在于:进行大范围的画面更新时,会有画面撕裂的问题。

也就是说,低延迟视觉库快速实现上屏这种优化方法生效的前提是:只修改屏幕上很小的一块区域,比如很小的一块区域内的笔画变化。

当用户在进行手写输入时,小块的修改通过前缓冲反馈到屏幕上;而当用户抬起手写笔时,这些输入将汇总交给原有的双缓冲渲染机制,实现固化。

应用

低延迟视觉库提供的前缓冲绘制能力,适用于手写、画画、素描这类修改一小块区域的场景,但不适用于平移、缩放这些修改很大块区域的场景。

在接入低延时视觉库前,需要仔细评估下应用中哪些内容可以渲染到前缓冲图层(常见的就是笔画),哪些内容需要维持在双缓冲图层处理(如大面积的内容更新、平移、缩放)。

接入过程也简单,继承 GLFrontBufferedRenderer.Callback 接口,分别实现绘制前缓冲的操作和绘制双缓冲的操作,即可。

代码语言:text复制
val callbacks = object : GLFrontBufferedRenderer.Callback<DATA_TYPE> {
    override fun onDrawFrontBufferedLayer(
        eglManager: EGLManager,
        bufferWidth: Int,
        bufferHeight: Int,
        transform: FloatArray,
        param: DATA_TYPE
    ) {
        // 前缓冲绘制代码
    }

    override fun onDrawDoubleBufferedLayer(
        eglManager: EGLManager,
        bufferWidth: Int,
        bufferHeight: Int,
        transform: FloatArray,
        params: Collection<DATA_TYPE>
    ) {
        // 双缓冲绘制代码
    }
}

val frontBufferRenderer = GLFrontBufferedRenderer(mySurfaceView, callbacks)

// 添加触摸监听,获取动作输入,并将输入转为动作数据交给 frontBufferRenderer
mySurfaceView.setOnTouchListener { v, event ->
            // 需要自行处理 MotionEvent 转换成图形数据的逻辑
            val dataPoint = event.toDATA_TYPE()
                
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    // 直接绘制新的数据到前缓冲层
                    frontBufferRenderer.renderFrontBufferedLayer(dataPoint)
                }

                MotionEvent.ACTION_MOVE -> {
                    // 直接绘制新的数据到前缓冲层
                    frontBufferRenderer.renderFrontBufferedLayer(dataPoint)
                }

                MotionEvent.ACTION_UP -> {
                    // 用户操作完毕,笔画绘制完成,这个时候把过程中所有的数据提交给双缓冲层处理
                    frontBufferRenderer.commit()
                }
                else -> Unit
            }
            true
        }

完整的实例代码可以参考官方仓库中的测试工程,入口界面 FrontBufferedRendererTestActivity

0 人点赞