Android TextureView/绘图/绘画性能

Android TextureView / Drawing / Painting Performance

我正尝试在 Android 上使用 TextureView 制作一个 drawing/painting 应用程序。我想支持高达 4096x4096 像素的绘图表面,这对于我的最小目标设备(我用于测试)来说似乎是合理的,它是一个 Google Nexus 7 2013,它有一个很好的四核 CPU和 2GB 内存。

我的一个要求是我的视图必须位于允许放大、缩小和平移的视图内,这是我编写的所有自定义代码(想想来自 iOS 的 UIScrollView)。

我试过将常规视图(不是 TextureView)与 OnDraw 一起使用,性能非常糟糕 - 每秒不到 1 帧。即使我只用改变的矩形调用 Invalidate(rect) 也会发生这种情况。我尝试关闭视图的硬件加速,但没有渲染,我想是因为 4096x4096 对于软件来说太大了。

然后我尝试使用 TextureView 并且性能稍微好一些 - 大约每秒 5-10 帧(仍然很糟糕但更好)。用户绘制到位图中,然后使用后台线程将其绘制到纹理中。我正在使用 Xamarin,但希望代码对 Java 人有意义。

private void RunUpdateThread()
{
    try
    {
        TimeSpan sleep = TimeSpan.FromSeconds(1.0f / 60.0f);
        while (true)
        {
            lock (dirtyRect)
            {
                if (dirtyRect.Width() > 0 && dirtyRect.Height() > 0)
                {
                    Canvas c = LockCanvas(dirtyRect);
                    if (c != null)
                    {
                        c.DrawBitmap(bitmap, dirtyRect, dirtyRect, bufferPaint);
                        dirtyRect.Set(0, 0, 0, 0);
                        UnlockCanvasAndPost(c);
                    }
                }
            }
            Thread.Sleep(sleep);
        }
    }
    catch
    {
    }
}

如果我将 lockCanvas 更改为传递 null 而不是 rect,性能在 60 fps 时会很好,但是 TextureView 的内容会闪烁并损坏,这令人失望。我原以为它会简单地在下面使用 OpenGL 帧缓冲区/渲染纹理,或者至少有一个选项来保留内容。

除了在 Android 中的原始 OpenGL 中完成所有操作之外,是否还有其他选项可以在绘制调用之间保留的表面上进行高性能绘图和绘画?

更新 我放弃了 TextureView,现在使用 OpenGL 视图,我在其中调用 glTexSubImage2D 来更新渲染纹理的更改部分。

旧答案 我最终在 4x4 网格中平铺 TextureView。根据每一帧的脏矩形,我刷新适当的 TextureView 视图。在我调用 Invalidate 的那一帧未更新的任何视图。

某些设备,例如 Moto G phone 存在一个问题,即一帧的双缓冲已损坏。您可以通过在父视图调用 onLayout 时调用 lockCanvas 两次来解决此问题。

private void InvalidateRect(int l, int t, int r, int b)
{
    dirtyRect.Set(l, t, r, b);

    foreach (DrawSubView v in drawViews)
    {
        if (Rect.Intersects(dirtyRect, v.Clip))
        {
            v.RedrawAsync();
        }
        else
        {
            v.Invalidate();
        }
    }

    Invalidate();
}

protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
    for (int i = 0; i < ChildCount; i++)
    {
        View v = GetChildAt(i);
        v.Layout(v.Left, v.Top, v.Right, v.Bottom);
        DrawSubView sv = v as DrawSubView;
        if (sv != null)
        {
            sv.RedrawAsync();

            // why are we re-drawing you ask? because of double buffering bugs in Android :)
            PostDelayed(() => sv.RedrawAsync(), 50);
        }
    }
}

首先,如果您想了解幕后发生的事情,您需要阅读 Android Graphics Architecture document。它很长,但如果您真诚地想了解 "why",那么它就是开始的地方。

关于 TextureView

TextureView是这样工作的:它有一个Surface,它是一个具有生产者-消费者关系的缓冲区队列。如果您使用软件 (Canvas) 渲染,则锁定 Surface,这会为您提供缓冲区;你在上面画画;然后解锁 Surface,它将缓冲区发送给消费者。在这种情况下,消费者处于同一进程中,称为 SurfaceTexture 或(在内部,更恰当地)GLConsumer。它将缓冲区转换为 OpenGL ES 纹理,然后渲染到视图。

如果关闭硬件加速,GLES 将被禁用,TextureView 将无能为力。这就是为什么当您关闭硬件加速时您什么也得不到的原因。 The documentation 非常具体:"TextureView can only be used in a hardware accelerated window. When rendered in software, TextureView will draw nothing."

如果您指定一个脏矩形,软件渲染器将在 渲染完成后将之前的内容 memcpy 到帧中 。我不相信它设置了一个剪辑矩形,所以如果你调用 drawColor(),你将填满整个屏幕,然后覆盖那些像素。如果您当前没有设置裁剪矩形,您可能会看到这样做的一些性能优势。 (虽然我没有检查代码。)

脏矩形是输入输出参数。你在调用lockCanvas()时传入你想要的rect,系统允许在调用returns之前修改它。 (在实践中,它会这样做的唯一原因是如果没有前一帧或者 Surface 被调整大小,在这种情况下它会扩展它以覆盖整个屏幕。我认为这会用更直接的方式更好地处理"I reject your rect" 信号。)您需要更新返回的矩形内的每个像素。您 不允许 更改矩形,这似乎是您在示例中尝试做的 - lockCanvas() 成功后脏矩形中的任何内容都是您想要的需要借鉴。

我怀疑脏矩形处理不当是您闪烁的根源。可悲的是,这是一个容易犯的错误,因为 lockCanvas() dirtyRect arg 的行为仅在 the Surface class 本身中记录。

表面和缓冲

所有表面都是双缓冲或三缓冲的。没有办法解决这个问题——你不能同时读写而不流泪。如果您想要一个可以在需要时修改和推送的缓冲区,则需要锁定、复制和解锁该缓冲区,这会在合成管道中造成停顿。为了获得最佳吞吐量和延迟,翻转缓冲区更好。

如果你想要锁定-复制-解锁行为,你可以自己写(或者找一个库来做),它会像系统为你做的那样高效(假设你很擅长 blit 循环)。绘制到屏幕外 Canvas 并 blit 位图,或绘制到 OpenGL ES FBO 并 blit 缓冲区。您可以在 Grafika 的“record GL app”Activity 中找到后者的示例,它有一种模式,即在屏幕外渲染一次,然后 blits 两次(一次用于显示,一次用于录制视频)。

更快的速度等等

在 Android 上绘制像素有两种基本方法:使用 Canvas 或使用 OpenGL。 Canvas Surface 或 Bitmap 的渲染总是在软件中完成,而 OpenGL 渲染是通过 GPU 完成的。唯一的例外是,当渲染到 custom View 时,您可以选择使用硬件加速,但这不适用于渲染到 SurfaceView 或 TextureView 的表面时。

绘图或绘画应用程序可以记住绘图命令,或者只是将像素扔到缓冲区并将其用作内存。前者允许更深 "undo",后者更简单,并且随着要渲染的内容量的增加,性能也越来越好。听起来你想做后者,所以从屏幕外发出 blitting 是有道理的。

大多数移动设备对 GLES 纹理的硬件限制为 4096x4096 或更小,因此您无法将单个纹理用于​​更大的纹理。您可以查询大小限制值 (GL_MAX_TEXTURE_SIZE),但使用一个尽可能大的内部缓冲区可能会更好,并且只渲染适合屏幕的部分。我不知道 Skia (Canvas) 的限制是什么,但我相信你可以创建更大的位图。

根据您的需要,SurfaceView 可能比 TextureView 更可取,因为它避免了中间的 GLES 纹理步骤。您在 Surface 上绘制的任何内容都会直接进入系统合成器 (SurfaceFlinger)。这种方法的缺点是,由于 Surface 的消费者不在进程中,View 系统没有机会处理输出,因此 Surface 是一个独立的层。 (对于绘图程序,这可能是有益的——正在绘制的图像在一层上,您的 UI 在顶部的单独层上。)

FWIW,我没有看过代码,但是 Dan Sandler 的 Markers app might be worth a peek (source code here).

更新: 损坏是 identified as a bug and fixed in 'L'