15、SurfaceView与TextureView:双缓冲机制差异、Surface生命周期管理、视频场景选型
这一章,我们来聊聊Android图形系统里两个老熟人——SurfaceView和TextureView。说实话,这两个东西我用了快十年,踩过的坑比写过的代码还多。很多开发者觉得它们不就是用来显示视频的吗?其实没那么简单。它们的双缓冲机制、生命周期管理,以及在不同场景下的选型,背后藏着不少门道。
我个人习惯把SurfaceView和TextureView比作「两个性格迥异的兄弟」。一个直来直去,一个心思细腻。选对了,你的视频播放流畅如丝;选错了,卡顿、黑屏、内存泄漏轮番上阵。嗯,咱们今天就把它彻底讲透。
15.1 双缓冲机制:一个「直给」,一个「绕路」
先说说双缓冲。说白了,就是解决画面撕裂和闪烁问题的老办法。SurfaceView和TextureView都用了双缓冲,但实现方式完全不同。
15.1.1 SurfaceView:独立窗口,直接怼
SurfaceView的做法很粗暴——它自己搞一个独立的Surface,这个Surface背后是一个单独的窗口(Window)。这个窗口不跟Activity共享View hierarchy,而是直接由WMS(WindowManagerService)管理。
这意味着什么?意味着SurfaceView的绘制不走主线程的UI渲染管线。它自己有一个独立的BufferQueue,双缓冲(甚至三缓冲)完全由SurfaceFlinger直接处理。
关键点:SurfaceView的Surface是独立的,它的双缓冲机制不依赖View的绘制流程。所以,你在SurfaceView上绘制内容时,不会阻塞主线程的UI刷新。
我在项目中遇到过一个问题:一个视频播放器,用SurfaceView渲染视频,同时界面上还有复杂的动画。如果换成TextureView,动画掉帧严重。但SurfaceView就没事,因为它压根不跟主线程抢资源。
15.1.2 TextureView:寄人篱下,走硬件合成
TextureView就不一样了。它本质上是一个View,它的Surface是依附于宿主Window的。TextureView通过SurfaceTexture来接收图像流,然后作为纹理(Texture)交给硬件加速渲染管线去合成。
所以,TextureView的双缓冲其实是在GPU层面完成的。它的BufferQueue是跟View hierarchy共享的。每次绘制,都要经过View的onDraw流程,走一遍硬件加速渲染。
注意:TextureView的绘制会占用主线程的渲染时间。如果你的UI已经够复杂了,再叠一个TextureView,掉帧几乎是必然的。
我曾经在一个直播项目中,为了做圆角视频效果,硬着头皮用了TextureView。结果在低端机上,帧率直接掉到20fps以下。后来换成SurfaceView + 自定义遮罩,问题才解决。
15.1.3 双缓冲机制对比
| 特性 | SurfaceView | TextureView |
|---|---|---|
| 缓冲区位置 | 独立窗口,由SurfaceFlinger管理 | 依附于宿主窗口,由GPU管理 |
| 绘制线程 | 独立线程,不阻塞主线程 | 主线程UI渲染管线 |
| 双缓冲实现 | BufferQueue + SurfaceFlinger | GPU纹理合成 |
| 性能开销 | 低,适合高频刷新 | 较高,受UI复杂度影响 |
| 典型场景 | 视频播放、相机预览、游戏 | 需要动画、变换、圆角的视频场景 |
15.2 Surface生命周期管理:别让Surface「死」得不明不白
Surface的生命周期,是很多开发者容易忽略的地方。尤其是SurfaceView,它的Surface创建和销毁跟Activity的生命周期并不同步。你想想看,如果你在Surface已经销毁的情况下还往上面画东西,轻则黑屏,重则崩溃。
15.2.1 SurfaceView的Surface生命周期
SurfaceView的Surface生命周期由SurfaceHolder.Callback管理。核心回调就三个:
surfaceCreated(SurfaceHolder holder):Surface创建好了,可以开始绘制。surfaceChanged(SurfaceHolder holder, int format, int width, int height):Surface尺寸或格式变了,需要重新配置。surfaceDestroyed(SurfaceHolder holder):Surface即将销毁,必须停止所有绘制操作。
这里有个坑:surfaceDestroyed不一定会被调用。比如,Activity被强杀时,这个回调可能就不会走。所以,你不能完全依赖它来做资源释放。
我的习惯:在onPause里就主动停止绘制线程,并释放Surface相关的资源。不要等到surfaceDestroyed。这样即使回调没来,也不会出问题。
我曾经在一个项目中,因为没处理好Surface生命周期,导致用户切到后台再切回来时,视频画面卡住了。排查了半天,发现是绘制线程在Surface销毁后还在往BufferQueue里提交数据。嗯,从那以后,我每次都会在onPause里加一个「停止绘制」的标记。
15.2.2 TextureView的Surface生命周期
TextureView的Surface生命周期是通过SurfaceTextureListener来管理的:
onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height):SurfaceTexture可用。onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height):尺寸变化。onSurfaceTextureDestroyed(SurfaceTexture surface):返回true表示释放SurfaceTexture,返回false表示不释放。onSurfaceTextureUpdated(SurfaceTexture surface):每一帧更新时回调。
注意onSurfaceTextureDestroyed的返回值。如果你返回false,SurfaceTexture不会被释放,TextureView还可以继续使用。但如果你返回true,SurfaceTexture会被销毁,下次需要重新创建。
我曾经踩过的坑:在onSurfaceTextureDestroyed里返回了false,但忘了在Activity销毁时手动释放SurfaceTexture。结果导致SurfaceTexture泄漏,GPU内存只增不减。最后在低端机上直接OOM。
15.2.3 生命周期管理最佳实践
我总结了几条铁律,照着做基本不会出问题:
- 绘制线程的生命周期必须短于Surface的生命周期。在surfaceCreated里启动线程,在surfaceDestroyed里停止线程。如果担心回调不执行,在onPause里也做一次停止。
- 不要在Surface销毁后还持有它的引用。尤其是SurfaceView的Surface,销毁后就是无效对象。继续使用会导致IllegalStateException。
- TextureView的SurfaceTexture要成对释放。在onSurfaceTextureDestroyed里返回true,或者在Activity的onDestroy里手动调用release()。
- 使用标志位控制绘制状态。比如一个AtomicBoolean类型的mIsSurfaceValid,在surfaceCreated里设为true,surfaceDestroyed里设为false。绘制线程每次循环都检查这个标志位。
15.3 视频场景选型:什么时候用谁?
这个问题,我几乎每次技术分享都会被问到。其实选型没那么复杂,记住几个原则就行。
15.3.1 选SurfaceView的场景
- 纯视频播放:不需要圆角、旋转、缩放等变换。SurfaceView性能最好,功耗最低。
- 相机预览:相机预览需要高帧率、低延迟。SurfaceView的独立窗口机制天然适合。
- 游戏渲染:尤其是OpenGL ES游戏,SurfaceView可以直接绑定EGLSurface,效率极高。
- 低端机适配:在内存和GPU性能有限的设备上,SurfaceView比TextureView稳定得多。
15.3.2 选TextureView的场景
- 需要动画效果:比如视频要平移、缩放、旋转、做圆角。TextureView作为View,可以直接应用View的动画和变换。
- 需要跟UI层叠加:比如在视频上覆盖文字、贴纸、弹幕。TextureView的Z-order跟普通View一样,不需要额外处理。
- 需要截图或录屏:TextureView可以通过getBitmap()直接获取当前帧,SurfaceView做不到。
- 需要支持硬件加速的复杂合成:比如视频跟UI做混合模式(BlendMode)效果。
15.3.3 我的选型决策树
我一般会按这个顺序问自己:
- 需不需要对视频画面做变换(圆角、旋转、缩放)?是 → TextureView,否 → 继续问
- 需不需要频繁截图或录屏?是 → TextureView,否 → 继续问
- 目标设备是不是低端机?是 → SurfaceView,否 → 继续问
- UI层是否已经很复杂,容易掉帧?是 → SurfaceView,否 → TextureView
你看,其实没那么纠结。大多数视频播放场景,SurfaceView是更稳妥的选择。只有当你确实需要TextureView的那些特殊能力时,才去用它。
一句话总结:能用SurfaceView就用SurfaceView,除非你明确需要TextureView的特性。
15.4 知识体系结构图
下面这张图,我把本章的核心知识点串起来了。你可以看到双缓冲机制、生命周期管理、选型决策三者之间的关系。
15.5 避坑指南:我踩过的那些坑
最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路。
坑一:SurfaceView黑屏问题
我曾经在一个视频通话项目里,发现SurfaceView偶尔会黑屏。排查了很久,发现是Surface创建和Activity onResume的时序问题。Surface创建需要时间,如果你在onResume里立刻开始绘制,Surface可能还没准备好。解决方案:在surfaceCreated回调里再启动绘制,不要依赖Activity的生命周期。
坑二:TextureView内存泄漏
TextureView的SurfaceTexture如果不及时释放,会导致GPU内存泄漏。我见过一个项目,在Activity里用了TextureView,但onDestroy里忘了调用surfaceTexture.release()。结果每次进出页面,GPU内存涨几十MB,几次之后就OOM了。嗯,这个坑我印象特别深。
坑三:SurfaceView与动画冲突
SurfaceView默认是独立的Window,它的Z-order总是高于普通View。如果你想在SurfaceView上面显示一个动画View,需要调用setZOrderOnTop(false),并配合SurfaceHolder的setFormat来调整。否则,动画会被SurfaceView盖住。
好了,关于SurfaceView和TextureView的双缓冲机制、生命周期管理、以及选型策略,就讲到这里。这些东西看似基础,但真正用好了,能让你的视频应用在性能和功能之间找到最佳平衡点。
公众号:蓝海资料掘金营,微信deep3321