6

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

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

我尝试使用TextureView带有 OnDraw 的常规 View (不是),性能绝对糟糕 - 每秒不到 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 中执行所有操作以实现高性能绘图和在绘图调用之间保留的表面上绘图之外,还有其他选择吗?

4

2 回答 2

21

首先,如果您想了解幕后发生的事情,您需要阅读Android 图形架构文档。它很长,但如果你真的想了解“为什么”,那就是开始的地方。

关于纹理视图

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

如果关闭硬件加速,GLES 将被禁用,TextureView 将无法执行任何操作。这就是为什么当你关闭硬件加速时你什么也得不到。 文档很具体:“TextureView 只能在硬件加速的窗口中使用。在软件中渲染时,TextureView 什么都不会画。”

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

脏矩形是一个输入输出参数。你在调用时传入你想要的矩形lockCanvas(),并且系统可以在调用返回之前对其进行修改。(在实践中,这样做的唯一原因是如果没有前一帧或调整了 Surface 大小,在这种情况下,它将扩展它以覆盖整个屏幕。我认为用更直接的方式处理会更好“我拒绝你的矩形”信号。)你需要更新你返回的矩形内的每个像素。不允许您更改 rect,这似乎是您在示例中尝试执行的操作 -lockCanvas()成功后脏 rect 中的任何内容都是您需要使用的内容。

我怀疑肮脏的 rect 处理不当是你闪烁的根源。遗憾的是,这是一个容易犯的错误,因为lockCanvas() dirtyRectarg 的行为仅记录在Surface 类本身中。

表面和缓冲

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

如果你想要 lock-copy-unlock 行为,你可以自己写(或找到一个库),它会像系统为你做的一样高效(假设你擅长blit 循环)。绘制到离屏 Canvas 并 blit 位图,或绘制到 OpenGL ES FBO 并 blit 缓冲区。您可以在 Grafika 的“记录 GL 应用程序”活动中找到后者的示例,该活动具有一种模式,即在屏幕外渲染一次,然后闪烁两次(一次用于显示,一次用于录制视频)。

更快的速度等等

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

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

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

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

FWIW,我没有看过代码,但 Dan Sandler 的Markers应用程序可能值得一看(源代码在这里)。

更新:损坏被确定为错误在 'L' 中修复

于 2015-06-26T15:40:44.770 回答
1

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

旧答案 我最终将 TextureView 平铺在 4x4 网格中。根据每个帧的脏矩形,我刷新相应的 TextureView 视图。任何未在该框架上更新的视图,我都会调用 Invalidate。

某些设备(例如 Moto G 手机)存在双缓冲一帧损坏的问题。当父视图调用 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);
        }
    }
}
于 2015-06-25T20:40:35.683 回答