26、性能优化:渲染性能分析(Systrace/Profiler)、减少掉帧、内存优化、GPU过度绘制优化
说到SurfaceView的视频渲染,性能优化这块儿,我估计是很多同学心里最没底的地方。代码写完了,能跑了,但一上真机就卡成PPT。嗯,这事儿我太熟了。早期我做视频播放器的时候,就吃过这个亏——明明逻辑都对,但就是掉帧,用户反馈说“看个视频像在翻连环画”。
所以这一章,咱们就把性能优化这事儿掰开揉碎了讲。说白了,就是三件事:找到瓶颈、干掉掉帧、管好内存。我把自己这些年踩过的坑、用过的工具、总结的方法,全放在这里了。
26.1 渲染性能分析:你得先知道问题在哪儿
优化之前,先得诊断。我个人习惯用两个工具:Systrace 和 GPU Profiler。它们俩配合起来,基本能覆盖90%的渲染问题。
26.1.1 用Systrace抓帧
Systrace是Android自带的性能分析工具。它能告诉你每一帧里,CPU、GPU、UI线程都在干嘛。你想想看,如果一帧超过了16ms,那肯定掉帧了。Systrace能帮你定位到具体是哪个环节超时。
我一般这么用:
// 命令行启动Systrace
python systrace.py -t 10 -o trace.html sched gfx view wm am app
// 或者直接在Android Studio里用Profiler抓取
抓完之后,打开生成的trace.html。你会看到一条条彩色的时间轴。重点关注这几个东西:
- Choreographer#doFrame:如果这个阶段耗时超过16ms,说明UI线程卡了。
- SurfaceFlinger:如果这里出现断层,说明GPU提交帧太慢。
- BufferQueue:如果这里显示“dequeueBuffer”等待,说明SurfaceView的缓冲区被占满了。
重点:我在项目中遇到过,SurfaceView的渲染线程和UI线程互相抢资源。Systrace里一看,两个线程的“锁等待”时间占了整整5ms。后来我把渲染线程的优先级调低了一点,问题就解决了。
26.1.2 GPU Profiler看渲染管线
GPU Profiler能直接看到每一帧的渲染管线耗时。包括:顶点处理、光栅化、片段着色。如果某个阶段特别长,那就要针对性优化。
举个例子,有一次我发现片段着色器耗时异常高。查了半天,发现是SurfaceView的像素格式用了RGBA_8888,但实际视频源是NV12。每次渲染都要做格式转换,GPU累得够呛。改成直接支持NV12的格式后,耗时直接降了一半。
| 阶段 | 正常耗时 | 异常耗时 | 可能原因 |
|---|---|---|---|
| 顶点处理 | < 2ms | > 5ms | 顶点数过多、VBO更新频繁 |
| 光栅化 | < 3ms | > 8ms | 分辨率过高、Overdraw严重 |
| 片段着色 | < 4ms | > 10ms | 着色器复杂、纹理采样过多 |
26.2 减少掉帧:从16ms到稳稳的幸福
掉帧的本质,就是一帧的渲染时间超过了16ms。为什么会这样?我总结了几种常见情况:
- CPU/GPU负载不均衡:CPU拼命准备数据,GPU闲着;或者GPU忙死,CPU在等。
- 缓冲区交换卡顿:SurfaceView的双缓冲或三缓冲机制出了问题。
- 主线程阻塞:渲染线程被UI操作、垃圾回收等打断。
怎么解决?我一般从这几个方向入手:
26.2.1 使用三缓冲
SurfaceView默认是双缓冲。但如果你发现掉帧是因为“dequeueBuffer”等待,那就试试三缓冲。说白了,就是多一个缓冲区做缓冲池,减少等待。
// 在SurfaceView创建时设置
SurfaceView surfaceView = new SurfaceView(context);
surfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
// 在渲染线程中,使用三缓冲
EGLConfig config = ...;
EGLContext context = ...;
EGLDisplay display = ...;
// 设置EGL_SWAP_BEHAVIOR为EGL_BUFFER_PRESERVED
int[] attribs = {
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL10.EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_SWAP_BEHAVIOR_PRESERVED_BIT,
EGL10.EGL_BUFFER_SIZE, 32,
EGL10.EGL_DEPTH_SIZE, 0,
EGL10.EGL_STENCIL_SIZE, 0,
EGL10.EGL_NONE
};
小技巧:三缓冲虽然能减少掉帧,但会增加内存占用。如果你的设备内存紧张(比如2GB以下的老机型),建议还是用双缓冲,配合帧率控制。
26.2.2 控制帧率
不是所有场景都需要60fps。比如播放静态PPT或者低帧率视频,强行跑60fps反而浪费性能。我习惯根据视频源的实际帧率来动态调整渲染帧率。
// 动态帧率控制
long targetFrameTime = 1000 / targetFps; // targetFps从视频源获取
long lastFrameTime = 0;
while (isRunning) {
long currentTime = System.nanoTime() / 1000000;
long elapsed = currentTime - lastFrameTime;
if (elapsed < targetFrameTime) {
// 还没到时间,休眠一会儿
Thread.sleep(targetFrameTime - elapsed);
continue;
}
// 渲染一帧
renderFrame();
eglSwapBuffers(display, surface);
lastFrameTime = currentTime;
}
26.3 内存优化:别让SurfaceView成为内存黑洞
SurfaceView本身不占太多内存,但它背后关联的图形缓冲区和纹理可是吃内存大户。我记得有一次,一个720p的视频,SurfaceView占用了将近200MB内存。查了半天,发现是纹理缓存没释放。
26.3.1 纹理管理
每次渲染一帧,都会生成一个纹理。如果不及时释放,纹理就会越积越多。我一般用纹理池来管理:
// 纹理池示例
class TexturePool {
private Queue<Integer> availableTextures = new LinkedList<>();
private static final int MAX_TEXTURES = 3; // 最多保留3个纹理
public int acquireTexture() {
if (availableTextures.isEmpty()) {
// 创建新纹理
int[] textures = new int[1];
glGenTextures(1, textures, 0);
return textures[0];
}
return availableTextures.poll();
}
public void releaseTexture(int textureId) {
if (availableTextures.size() < MAX_TEXTURES) {
availableTextures.offer(textureId);
} else {
// 超过上限,直接删除
glDeleteTextures(1, new int[]{textureId}, 0);
}
}
}
26.3.2 缓冲区复用
SurfaceView的渲染循环里,经常需要分配临时缓冲区。比如YUV数据转RGB,每次new一个byte数组,GC就会频繁触发。我建议用对象池或者直接复用全局缓冲区。
注意:千万不要在渲染循环里做内存分配。我曾经因为这个原因,导致GC每秒钟触发两三次,掉帧掉到怀疑人生。后来把所有临时对象都改成复用,GC频率降到了每10秒一次。
26.4 GPU过度绘制优化:别让GPU做无用功
过度绘制(Overdraw)就是同一像素被画了多次。在SurfaceView里,常见的情况是:背景层、视频层、UI层叠加在一起,GPU把每一层都画了一遍,但用户只能看到最上面那层。
怎么查过度绘制?打开开发者选项里的“显示GPU过度绘制”,颜色越红,说明越严重。
26.4.1 减少图层
我一般遵循一个原则:能合并的图层就合并。比如视频画面和字幕,如果字幕是直接叠加在视频上的,那就别分成两个图层渲染,直接在片段着色器里合成。
// 片段着色器:视频和字幕一次合成
#version 300 es
precision mediump float;
uniform sampler2D videoTexture;
uniform sampler2D subtitleTexture;
in vec2 vTexCoord;
out vec4 fragColor;
void main() {
vec4 videoColor = texture(videoTexture, vTexCoord);
vec4 subtitleColor = texture(subtitleTexture, vTexCoord);
// 如果字幕像素不透明,就覆盖视频
if (subtitleColor.a > 0.1) {
fragColor = subtitleColor;
} else {
fragColor = videoColor;
}
}
26.4.2 裁剪不可见区域
如果SurfaceView有一部分被其他View遮挡了,那就别渲染那部分。用clipRect或者scissor test来裁剪。
// 开启裁剪测试
glEnable(GL_SCISSOR_TEST);
glScissor(x, y, width, height); // 只渲染可见区域
// 渲染...
// 关闭裁剪测试
glDisable(GL_SCISSOR_TEST);
核心思路:GPU每画一个像素,都是有成本的。你想想看,如果屏幕上有30%的区域是看不见的,那GPU就白干了30%的活。裁剪掉这些区域,性能提升立竿见影。
26.5 知识体系总览
说了这么多,我画了一张图,把这一章的核心逻辑串起来。你可以把它当作一个检查清单,每次做性能优化时,对着这张图一步步排查。
这张图把四个优化方向串在了一起。你从性能分析入手,找到问题后,再针对性地选择减少掉帧、内存优化或者过度绘制优化。别想着一次性全搞定,每次只解决一个问题,效果反而更好。
我的习惯:每次优化前,先用Systrace抓一次基线数据。优化完再抓一次,对比前后差异。这样你就能清楚地知道,每一行代码改动到底带来了多少性能提升。
好了,这一章的内容就到这儿。性能优化这事儿,没有银弹。但只要你掌握了这些工具和方法,遇到问题就不会慌。嗯,去试试吧,看看你的SurfaceView能不能跑到60fps稳稳的。