【问题标题】:Poor performance when continuously updating a SurfaceView with pixel data from a ByteBuffer使用 ByteBuffer 中的像素数据连续更新 SurfaceView 时性能不佳
【发布时间】:2021-01-29 15:10:31
【问题描述】:

我正在编写一个 Android 应用程序,其中本机线程以大约 60 fps 的速度不断生成图像帧(作为原始像素数据),然后应该显示在 SurfaceView 中。目前线程直接将像素数据写入到原生线程和JVM共享的ByteBuffer,然后通过JNI调用回调通知JVM端一帧准备好;然后将缓冲区读入Bitmap 并在SurfaceView 上绘制。我的代码大致是这样的(完整的源代码太大而无法共享):

// this is a call into native code that retrieves
// the width and height of the frame in pixels
// returns something on the order of 320×200
private external fun getFrameDimensions(): (Int, Int)

// a direct ByteBuffer shared between the JVM and the native thread
private val frameBuffer: ByteBuffer

private fun getFrame(): Bitmap {
    val (width, height) = getFrameDimensions()

    return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
        setHasAlpha(false)
        copyPixelsFromBuffer(frameBuffer.apply {
            order(ByteOrder.nativeOrder())
            rewind()
        })
    }
}

// the SurfaceView widget which should display the image
private lateinit var surfaceView: SurfaceView

private val SCALE = 8

// callback invoked by the native thread
public fun onFrameReady() {
    val canvas: Canvas = surfaceView.holder.lockCanvas() ?: return
    val bitmap = getFrame()
    
    try {
        canvas.drawBitmap(bitmap.scale(
            bitmap.width * SCALE,
            bitmap.height * SCALE),
            0.0f, 0.0f, null
        )
    } finally {
        holder.unlockCanvasAndPost(canvas)
    }
}

好消息是上述方法有效:屏幕以大致预期的帧速率更新。然而,性能真的很差,以至于应用程序锁定:它停止响应例如按下 按钮,最终会弹出 ANR 消息。显然我做错了什么,但我不太清楚到底是什么。

有没有办法让上面的运行更快?最好我想避免编写不必要的本机代码。我特别想避免将任何特定于 Android 的东西放在本机方面。

【问题讨论】:

  • 我知道你不能分享完整的源代码,但我建议在 Github 上创建一个简单的演示项目,我们可以轻松地重现这个问题。

标签: android performance kotlin surfaceview image-rendering


【解决方案1】:

嗯,有几个问题。 Kotlin 代码的主要优点是您每秒大约创建 60 次位图,并且不使用该方法来处理它bitmap.recycle(),这会在您不再需要该位图后释放该位图的本机内存。虽然帧速率为 60 fps,但很难找到一个正确的时间来释放它,但我认为它应该在执行帧绘制之后的某个地方。

但您的整体代码中的主要问题是在本机代码中使用表面视图的正确方法是您没有使用它。要正确执行此操作,您必须将 Surface 从 Android 代码直接传递到本机代码。之后,您应该通过ANativeWindow_fromSurface() 获得ANativeWindow。使用ANativeWindow_setBuffersGeometry() 设置大小和颜色格式。锁定缓冲区,复制像素并解锁缓冲区以发布它。全部在本机代码中,无需任何进一步的 Android 交互。它看起来像

ANativeWindow* window = ANativeWindow_fromSurface(env, javaSurface);
ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);

ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(window, &buffer, NULL) == 0) {
  memcpy(buffer.bits, pixels,  width * height * 2);
  ANativeWindow_unlockAndPost(window);
}

ANativeWindow_release(window);

请注意,这样您就不会创建大量位图,而这是导致性能问题的主要原因 - 您将直接将像素写入 Surface,而无需任何中间容器 网上有很多关于此的信息,甚至 NDK 示例中的谷歌sample 也显示了类似的方法(它通过编解码器呈现视频,但仍然如此)。如果有不清楚的地方,请联系我,我会尽力为您提供更多详细信息。

您可以使用的另一种方法是使用 Android 提供的 OpenGL ES API 以高效的方式在 SurfaceView 上绘制图像 - 还有大量在线信息。不过,与原生绘图相比,它的效率会更低。

【讨论】:

  • 实际上,我在某个时候添加 OpenGL 渲染,但我想首先关注软件渲染器。但我认为这将是另一个问题。
  • 好的,我想我会试试这个。我希望避免在事物的本机方面编写任何特定于 Android 的代码,以便我编写的 JVM 绑定可能会照原样在其他 JVM 平台上使用,但说实话,我没有遵守该原则一切都很好。
  • 对于 Android,很难做出可靠的 crosJVM 解决方案,因为 Android 框架本身使用大量优化来处理细节 - 一般方法可能无法正常工作或根本不起作用。
  • 好的,我切换到ANativeWindow,帧率明显变流畅了。奇怪的是,虽然 ANR 消失了,但这并没有解决无响应的 UI 问题。不过,我很高兴我至少可以消除这个原因。
  • 关于无响应的 UI - 如果它在没有 ANR 的情况下仍然存在,则似乎以不正确的方式进行了一些触摸处理。顺便说一句,外部onClickListeners 和onTouchListeners 有时可能不适用于默认SurfaceView - 您必须扩展SurfaceView 本身并在那里覆盖onTouchEvent 或将onTouchListener 设置为不SurfaceView 但对于像surfaceView.getHolder().setOnTouchListener() 这样的持有者......你必须使用它才能让它在你的情况下工作。希望它会有所帮助。
猜你喜欢
  • 2018-03-21
  • 2017-04-06
  • 1970-01-01
  • 2015-04-06
  • 2022-09-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多