Android TextureView /绘图/绘画性能

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

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

我尝试使用OnDraw的常规View(而不是TextureView ),性能绝对可怕 – 每秒不到1帧。 即使我只使用更改的rect调用了Invalidate(rect) ,也会发生这种情况。 我尝试关闭视图的硬件加速,但没有渲染,我假设因为4096×4096对于软件来说太大了。

然后我尝试使用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的内容TextureView闪烁并且被破坏,这是令人失望的。 我原以为它只是在下面使用OpenGL帧缓冲区/渲染纹理,或者至少有一个保留内容的选项。

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

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

关于TextureView

TextureView的工作方式如下:它有一个Surface,它是一个具有生产者 – 消费者关系的缓冲区队列。 如果您正在使用软件(Canvas)渲染,则可以锁定Surface,从而为您提供缓冲区; 你借鉴它; 然后你解锁Surface,它将缓冲区发送给消费者。 在这种情况下,消费者处于相同的过程中,称为SurfaceTexture或(内部,更恰当地)GLConsumer。 它将缓冲区转换为OpenGL ES纹理,然后渲染到View。

如果关闭硬件加速,GLES将被禁用,并且TextureView无法执行任何操作。 这就是为什么当你关闭硬件加速时什么都没有。 文档非常具体:“TextureView只能在硬件加速窗口中使用。在软件中渲染时,TextureView不会绘制任何内容。”

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

脏rect是一个in-out参数。 在调用lockCanvas()时传递所需的矩形,并允许系统在调用返回之前修改它。 (实际上,这样做的唯一原因是,如果没有前一帧或者Surfaceresize,在这种情况下它会扩展它以覆盖整个屏幕。我认为这样可以更直接地处理“我拒绝你的rect”信号。)你需要更新你回来的矩形内的每个像素。 您不允许更改您在示例中尝试执行的rect – 在lockCanvas()成功之后,脏矩形中的任何内容都是您需要绘制的内容。

我怀疑脏的直接error handling是你闪烁的根源。 遗憾的是,这是一个容易犯的错误,因为lockCanvas() dirtyRect arg的行为只记录在Surface类本身中。

表面和缓冲

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

如果你想要锁定 – 复制 – 解锁行为,你可以自己编写(或find一个执行它的库),它会像系统为你做的那样高效(假设你很好) blit循环)。 绘制到屏幕外的Canvas并将位图blit,或者绘制到OpenGL ES FBO并blit缓冲区。 您可以在Grafika的“ 记录GL应用程序 ”活动中find后者的示例,该活动具有一次在屏幕外呈现的模式,然后两次点亮(一次用于显示,一次用于录制video)。

更快的速度等

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

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

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

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

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

更新:腐败被识别为错误并在“L”中修复 。

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

老答案我最终在4×4网格中平铺了TextureView。 根据每个帧的脏矩形,我刷新相应的TextureView视图。 任何未更新的视图我称之为Invalidate on。

某些设备(例如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); } } }