Android- Surface, SurfaceView, TextureView, SurfaceTexture 原理图解

发布于:2025-09-11 ⋅ 阅读:(19) ⋅ 点赞:(0)

能画图不废话。本文以图解为主。

Surface内存与渲染机制

Surface底层对应的是一块内存!如下如所示:

我顺便把我们Android View的渲染机制给摆在这里吧。 因为原因是:一个窗口其实对应的也是一个Surface。这里面就是稍微夹带了View绘制触发流程。下一小节会讲。

我们再仔细思考一下,上图中的BufferQueue怎么回事。按道理讲一个Surface对应一个GraphicBuffer,这本质上是一个个共享内存块。那么假设当前应用只有一个Activity, 那么就只有一个Window,则Window就对应一个Surface是吧。此时BufferQueue里针对这个Surface也会存在多个GraphicBuffer的。原因是

  • BufferQueue 必须要预备三种内存块,代表,
    • 待分配
    • 正在填充
    • 消费中(待消费)差不多,随怎么理解吧。

这个其实是著名的多缓冲机制!为的就是快!不能让界面因为没有即时生产数据而导致卡顿的!

View渲染机制

Canvas与Surface的关系

Canvas你可以理解为发放绘制命令的。我们在获取Canvas的时候通常会这么写(见Android源码,onDraw()之前链路):

Canvas canvas = surface.lockCanvas();

这里面不仅仅是内存这么简单,而是获取一块直接内存映射的共享内存,说白了是将其操作区域锁定在 surface所对应的共享内存 GraphicBuffer(GraphicBuffer 为Surface真身,存于Native内存区域,是个共享内存)。 大致流程如下图所示

我们可以说,Surface是和OpenGL 高速处理的那些内存区域有关。 但是那些区域怎么处理,其所有涉及到的命令,全部都是用Canvas来编撰的。 RenderThread负责将这些命令翻译成OpenGL 指令

RenderThread

上图中的renderThread是一个App进程中的高级线程,不是UI线程。而是渲染线程!它专注于将UI线程中生成的绘制指令转换成GPU指令!通常呢,每一个Window都会对应一个RenderThread。这个线程的优先级非常高,仅次于音频线程!

为什么不写在进程里呢? 因为这样做会涉及大量的IPC, 太耗时了。不如每个窗口都加上。

SurfaceView里面涉及的洞机制是怎么回事?

SurfaceView通常用于视频预览,视频播放中。其核心是内部持有Surface。 还有个隐蔽的场景是浏览器的视频播放中,楼主最早做过一个叫H5课件的功能,是用浏览器播放的课件内容,视频播放时就遇见了打洞透穿,导致按钮展示不出来的问题。后来才知道浏览器的视频播放就是SurfaceView做的。SurfaceView用在复杂的界面里,难免会遇到其区域内的控件不可见的问题。对于这个问题现象下图有表示。

打不打洞?SurfaceView的快!是毋庸置疑的,性能极佳的实现,也得亏这个洞机制。出了bug先别着急。搞清SurfaceView为什么存在很关键。

如下图所示,当我们的一个App,开了一个Activity的时候,且Activity里面的布局加上了一个SurfaceView,那么在渲染合成的时候会是什么个结构呢?

答案是: Android为SurfaceView 单独开了一个BufferQueue!于是乎就出现了下图的状况:

为什么要给SurfaceView单开一个BufferQueue呢?

原因在于,SurfaceView就是要逃开普通View的渲染机制。SurfaceView里面的内容不需要Canvas来编辑绘制命令的。它直接承接的就是视频帧数据!这个视频帧数据的来源它不是Canvas呀,Canvas通天的能力也不能靠命令编撰吧。 并且如果靠命令编撰那可以是一张图接着一张图绘制,慢太多了! 我们可以直接用硬件解码器里面解码的图像信息即可。

也就是SurfaceView的内容展示,完全不需要靠着View那套编舞者协同调用View节点onDraw()来完成(这块涉及的节点过多,链路过长!耗时!)。所以就完全绕开了编舞者那套机制! 只需要直接接入解码器的输出即可!脱离这条机制最简单的做法,就是单开!

单开之后呢,我们依然可以遵循SurfaceFlinger的垂直同步信号,但是避开了普通布局的编舞者机制。而且SurfaceView中的内容是解码器给的,就会非常快!

SurfaceView适用场景

  • 实现实时相机预览
  • 高帧率游戏
  • 4K、8K视频播放器
  • 低延迟直播推流

SurfaceView挖洞引发的UI展示不全问题解决原理

我们在解决挖洞问题的时候通常做法是,将被“遮挡”(实际上是被隐形的)布局的层级调整一下。于是出现了类似这样的解决方案(就是调整布局层级,下文为各项调整策略之一):

view.bringToFront()

为什么这样一改,就好了呢?

挖洞细节

我们得重点看看这个挖洞的细节。下图右上角为我们上个章节展示的问题。当时只讲了抠洞了,但是没讲怎么挖的:

从上图看,有没有发现诡异之处??为什么就改了改节点渲染顺序,就能正常? 尤其是图中弟2️⃣种顺序。怎么就正常了? 正常在哪里呢?这我们要提及一下这个图绘制的时候发生了什么细节?其实看看图就明白了:

是的,透明色也是一种颜色, 也存在遮盖,这段内存区域就是被设置成了透明#00000000。因为是透明,所以在最终的合成的时候,OpenGL直接把这段内存表示的区域,和视界后面那层的内存内容这么一合成,诶? 就是背景色!

所以!!您的渲染顺序,也决定了最后会变成什么样子。对于布局中的Button1, Button2, SurfaceView。 只要我们想办法让Button1, Button2 的渲染次序,在SurfaceView之后!那就能看见,那段内存区域就不会被改成透明色了!

更改View层级顺序的核心目的,就是为了让控件的渲染顺序靠后!

TextureView

TetureView的类体系我们这里不讲了。其核心最终还是涉及到Surface。可以说,Surface在手,怎么变都能以某种方式将画面绘制到屏幕上。

在这里只讲原理。SurfaceView逃逸了View的编舞者机制,性能的确高,但是也对我们构建正常的布局带来了挑战。因为不在这个体系内,所以有些能力的确没有,比如:

  • View动画不支持!因为没有融进编舞者机制,即使自己生支持动画,也没办法融进正常的View体系。尤其是属性动画,实现难度极高。
  • 不支持半透明。这个和颜色混合有关,因为SurfaceView对应的Layer比较靠下,给透明度容易在计算上出问题。Android直接禁用了。
  • 无法直接截图。首先是因为它逃逸了编舞者体系,不能直接用View的截图能力。其次,由于SurfaceView单开了BufferQueue, 拿到这种特殊队列的数据,我们既没有线程的API,也没有相关权限。这都是受保护的缓冲区。

TextureView相比于SurfaceView

TextureView最大的特点就是将其纳入了View的体系。这是与SurfaceView最大的区别。所以TextureView它既能播放视频,也能像普通的View一样,参与View树布局构建。也不会出现什么打洞透穿的问题,动画呢也是支持的。可以支持小窗播放,动画特效。 但是因为实现这些也付出了相应的代价,它的性能也会有额外的消耗。容易出现发热,耗电的问题。

TextureView设计原理

TextureView对应的那块Surface最终会用OpenGLES来渲染。

在Androd的比较现代的设备上,其实是有策略存在的。现在我们拿出来最初的图看看,来研究图中的细节:

其实也就说明了,一般的界面,简单的,实际上不会走OpenGL ES这套GPU渲染的路子,而是采用手机自带的硬件混合渲染器来进行。因为大部分情况下不会涉及到超级复杂的界面也没有非常复杂的运算。

为什么要区分呢? 都走硬件混合渲染器不就得了? 原因在于HWC 与 OpenGL各自能力的特点

HWC的缺陷得靠OpenGL 来补。

TextureView的设计目标与OpenGL ES的选择

  • 拥有与View普通元素一样的能力,可以自由叠加,可以支持动画
  • 动态滤镜和特效要支持
  • 任意变换,包括旋转,透视。

这些设计目标,尤其是动态滤镜这块,不是HWC的能力范围!这块必须要仰仗诸如OpenGL ES这样的渲染引擎的能力。

设计

TextureView要求拥有View的能力,则其必定是要在View体系内的。也就意味着,它的布局必须要和整体的View所在的Layer是一个!是同一个!这跟SurfaceView有本质的区别。同时呢这个TextureView是要具备View所有的特点的,因为本身它就可以被当做一个普通的View来对待。你甚至可以理解为,就是一个ImageView了。其所有的绘制也要通过onDraw()方法。他们这些布局会在同一个Layer里。

但是呢? 这个布局里面的内容一般承载的是视频数据,这个数据呢,它还支持滤镜和特效。也就意味着,这一小块区域的内容,要经由OpenGL ES (我们假定目前Android采用的就是这个引擎)来进行渲染。

那怎么办??明明同属一个图层,但是经由渲染的引擎却是两个! 而渲染引擎渲染的单元就是一个Layer内的Surface集合啊。这就成了一个逻辑上行不通的问题。

TextureView做了一件事,就是如下图所示:

TextureView同样绑定了一个BufferQueue, 但是这个BufferQueue的生产者和消费者,不再像View里面那样,生产者生产完数据之后消费者拿给了SurfaceFlinger。 让SurfaceFlinger去进行Layer合成。

而是将其内容区域内容直接给到了OpenGL ES来处理,你可以理解为越过系统,私绑了OpenGL ES的某些插槽(TetureView中的SurfaceTexture专攻)。有机会讲讲这个OpenGL。具体是这样的流程:

  • Camera将数据填充后,通知TextureView相关类,告知数据准备完毕
  • TextureView直接调用invaliate()间接触发View绘制流程,会将自身区域标脏。从而使得View能计算出本区域是应该被绘制的。
  • 系统垂直同步信号来的时候,编舞者会启动调用链最终调用到TextureView的onDraw()
  • onDraw() 中利用canvas编撰指令,此时这个Canvas与平常View里给的不同,是硬件级别的HardwareCanvas,具备硬件加速能力, 让其绘制Camera数据对应的那块内存区域。(这块区域的变换, 滤镜也可以编撰)。canvas编撰的指令之后会由RenderThread转换成渲染指令,用以给HWC使用。 其实此时的指令的特点,就是让绘制一个图像,图像来源就是BufferQueue里面已经被填充的GraphicBuffer内存。 这块内存的数据呢? 实际的变换又绑定了OpenGL,OpenGL变换后的数据也在此区域。最后就当成一张图片来渲染了。
  • SurfaceFlingger对所有的Layer进行合成,一般走的是手机的硬件混合渲染器。而我们最新的一帧视频数据,其实相当于绘制了一幅画而已。

onDraw()中常见逻辑


网站公告

今日签到

点亮在社区的每一天
去签到