我正在尝试使用Android上的TextureView
制作绘图/绘画应用。我想支持高达4096x4096像素的绘图表面,这对于我的最小目标设备(我用于测试)来说似乎是合理的,这是一款具有漂亮的四核CPU和2GB内存的Google Nexus 7 2013。
我的一个要求是我的视图必须位于允许放大和缩小的视图中,这是我编写的所有自定义代码(想想来自iOS的UIScrollView)。
我尝试使用OnDraw的常规视图(不是TextureView
),性能非常糟糕 - 每秒不到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中执行所有操作之外还有其他选项可以在绘制调用之间保留的表面上进行高性能绘制和绘制吗?
答案 0 :(得分:18)
首先,如果您想了解幕后发生的事情,您需要阅读Android Graphics Architecture document。很长一段时间,但如果你真的想要理解"为什么",它就是开始的地方。
关于TextureView
TextureView的工作方式如下:它有一个Surface,它是一个具有生产者 - 消费者关系的缓冲区队列。如果您正在使用软件(Canvas)渲染,则可以锁定Surface,从而为您提供缓冲区;你借鉴它;然后你解锁Surface,它将缓冲区发送给消费者。在这种情况下,消费者处于相同的过程中,称为SurfaceTexture或(内部,更恰当地)GLConsumer。它将缓冲区转换为OpenGL ES纹理,然后渲染到View。
如果关闭硬件加速,GLES将被禁用,并且TextureView无法执行任何操作。这就是为什么当你关闭硬件加速时什么都没有。 The documentation非常具体:" TextureView只能在硬件加速窗口中使用。在软件中渲染时,TextureView将不会绘制任何内容。"
如果指定脏矩形,则软件渲染器将在渲染完成后将先前内容存储到帧中。我不相信它会设置一个剪辑矩形,所以如果你调用drawColor(),你将填满整个屏幕,然后覆盖这些像素。如果您当前没有设置剪辑矩形,那么您可能会看到一些性能优势。 (虽然我没有检查代码。)
脏矩形是一个输入输出参数。在调用lockCanvas()
时传递所需的矩形,并允许系统在调用返回之前修改它。 (实际上,这样做的唯一原因是,如果没有前一帧或者Surface调整大小,在这种情况下它会扩展它以覆盖整个屏幕。我认为这样可以更直接地处理"我拒绝你的直接"信号。)你需要更新你回来的矩形内的每个像素。您不允许更改您在样本中尝试执行的矩形 - lockCanvas()
成功后的脏矩形中的任何内容都是您需要的借鉴。
我怀疑脏兮兮的误操作是你闪烁的根源。遗憾的是,这是一个容易犯的错误,因为lockCanvas()
dirtyRect
arg的行为只记录在the Surface class本身。
表面和缓冲
所有Surfaces都是双缓冲或三缓冲。没有办法解决这个问题 - 你不能同时读写,也不能撕裂。如果您需要可以在需要时修改和推送的单个缓冲区,则需要锁定,复制和解锁该缓冲区,这会在合成管道中创建停顿。为了获得最佳吞吐量和延迟,翻转缓冲区会更好。
如果您想要锁定 - 复制 - 解锁行为,您可以自己编写(或找到一个可以执行此操作的库),并且它将像系统为您做的那样高效(假设您&# 39;对于blit循环很好。绘制到屏幕外的Canvas并将位图blit,或者绘制到OpenGL ES FBO并对缓冲区进行blit。你可以在Grafika" record GL app"中找到后者的例子。活动,其模式在屏幕外呈现一次,然后两次点亮(一次用于显示,一次用于录制视频)。
更快的速度等
在Android上绘制像素有两种基本方法:使用Canvas或使用OpenGL。对Surface或Bitmap的Canvas渲染始终在软件中完成,而OpenGL渲染则使用GPU完成。唯一的例外是,当渲染到custom View时,您可以选择使用硬件加速,但这在渲染到SurfaceView或TextureView的Surface时不适用。
绘图或绘画应用程序可以记住绘图命令,或者只是将像素放在缓冲区并将其用作内存。前者允许更深入"撤消"后者更简单,并且随着渲染内容的增长而具有越来越好的性能。听起来你想要做后者,所以从屏幕外进行blitting是有道理的。
对于GLES纹理,大多数移动设备的硬件限制为4096x4096或更小,因此您无法将单个纹理用于任何更大的纹理。您可以查询大小限制值(GL_MAX_TEXTURE_SIZE),但最好使用大小合适的内部缓冲区,并只渲染适合屏幕的部分。我不知道Skia(Canvas)的限制是什么,但我相信你可以制作更大的Bitmaps。
根据您的需要,SurfaceView可能比TextureView更受欢迎,因为它避免了中间GLES纹理步骤。您在Surface上绘制的任何内容都将直接转到系统合成器(SurfaceFlinger)。这种方法的缺点在于,由于Surface的消费者不在进行中,因此View系统没有机会处理输出,因此Surface是一个独立的层。 (对于绘图程序,这可能是有益的 - 正在绘制的图像位于一个图层上,您的UI位于顶部的单独图层上。)
FWIW,我还没看过代码,但Dan Sandler的Markers应用可能值得一看(源代码here)。
更新:贪污是identified as a bug和fixed in 'L'。
答案 1 :(得分:1)
UPDATE 我抛弃了TextureView,现在使用OpenGL视图,我调用glTexSubImage2D来更新渲染纹理的更改部分。
OLD ANSWER 我最终在4x4网格中平铺了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);
}
}
}