使用 ByteBuffer 中的像素数据连续更新 SurfaceView 时性能不佳
Poor performance when continuously updating a SurfaceView with pixel data from a ByteBuffer
我正在编写一个 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-specific。
好吧,有几个问题。使用 Kotlin 代码的主要问题是您每秒大约创建 60 次 Bitmap 并且不使用方法来处理它 bitmap.recycle()
,这会在您不再需要它时释放该位图的本机内存。虽然帧速率为 60 fps,但很难找到释放它的正确时间,但我认为它应该在执行帧绘制之后的某个地方。
但是您的整体代码中的主要问题是在本机代码中使用表面视图的正确方法是正确的,而您没有使用它。要正确执行此操作,您必须将 Surface 从 Android 代码直接传递到本机代码。之后你应该通过 ANativeWindow_fromSurface()
获得 ANativeWindow
。使用 ANativeWindow_setBuffersGeometry()
设置大小和颜色格式。锁定缓冲区,复制像素并将缓冲区解锁为 post。全部采用本机代码,无需任何进一步的 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 示例中的 google sample 显示了类似的方法(它通过编解码器渲染视频,但仍然如此)。如果有什么不清楚的地方,请联系我,我会尽力为您提供更多详细信息。
您可以使用的另一种方法是使用 OpenGl ES API Android 提供以高效的方式在 SurfaceView 上绘制图像 - 还有大量在线信息。不过与原生绘图相比,它的效率会降低。
我正在编写一个 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-specific。
好吧,有几个问题。使用 Kotlin 代码的主要问题是您每秒大约创建 60 次 Bitmap 并且不使用方法来处理它 bitmap.recycle()
,这会在您不再需要它时释放该位图的本机内存。虽然帧速率为 60 fps,但很难找到释放它的正确时间,但我认为它应该在执行帧绘制之后的某个地方。
但是您的整体代码中的主要问题是在本机代码中使用表面视图的正确方法是正确的,而您没有使用它。要正确执行此操作,您必须将 Surface 从 Android 代码直接传递到本机代码。之后你应该通过 ANativeWindow_fromSurface()
获得 ANativeWindow
。使用 ANativeWindow_setBuffersGeometry()
设置大小和颜色格式。锁定缓冲区,复制像素并将缓冲区解锁为 post。全部采用本机代码,无需任何进一步的 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 示例中的 google sample 显示了类似的方法(它通过编解码器渲染视频,但仍然如此)。如果有什么不清楚的地方,请联系我,我会尽力为您提供更多详细信息。
您可以使用的另一种方法是使用 OpenGl ES API Android 提供以高效的方式在 SurfaceView 上绘制图像 - 还有大量在线信息。不过与原生绘图相比,它的效率会降低。