一、项目架构设计:从零搭建短视频播放器
做短视频播放器,我第一个想到的不是代码怎么写,而是架构怎么搭。说白了,架构就像房子的地基,地基不稳,后面加再多功能都是白搭。
我个人习惯,拿到需求先画一张整体架构图。这样能看清每个模块的职责,避免后期互相打架。
这张图我画了不下十遍才定稿。你想想看,每一层都有自己明确的职责,上层只管调用,下层只管干活,互不干扰。
1.1 为什么选 SurfaceView 而不是 TextureView?
这个问题我经常被问到。其实很简单——SurfaceView 有独立的窗口,渲染不走 View 的绘制流程。这意味着什么?意味着它不会阻塞主线程的 UI 刷新。
我在项目中遇到过,用 TextureView 做视频渲染,滑动列表时明显卡顿。因为 TextureView 本质上是和 UI 共用同一个 SurfaceFlinger 通道。而 SurfaceView 有自己的 Surface,渲染和 UI 是两条线,互不影响。
核心结论:短视频场景下,频繁切换视频、快速滑动列表,SurfaceView 是更优选择。TextureView 适合需要做动画变换的场景(比如缩放、旋转),但短视频播放器不需要这些花哨效果。
1.2 模块划分:各司其职
我把整个项目拆成了 6 个核心模块。每个模块的职责边界非常清晰:
| 模块名称 | 核心职责 | 关键类 |
|---|---|---|
| 播放器核心 | 视频解码、渲染、音频输出 | VideoPlayerEngine |
| Surface 管理 | Surface 创建、销毁、尺寸适配 | SurfaceManager |
| 缓存策略 | 预加载、边播边缓、磁盘管理 | CacheManager |
| 手势交互 | 滑动切换、亮度/音量调节 | GestureController |
| 列表管理 | RecyclerView 绑定、预加载触发 | VideoListAdapter |
| 状态管理 | 播放/暂停/缓冲/错误状态流转 | PlayerStateMachine |
每个模块我都要求团队只暴露最少接口。比如播放器核心,对外就三个方法:play()、pause()、release()。内部再复杂,外面也只需要知道这三个。
二、核心代码实现:SurfaceView 的正确打开方式
嗯,这里要注意。SurfaceView 的使用有几个坑,我踩过不止一次。
2.1 SurfaceView 的生命周期绑定
SurfaceView 的 Surface 创建和销毁是异步的。你不能在 onCreate 里直接拿 Surface 用,必须等 surfaceCreated 回调。
public class VideoSurfaceView extends SurfaceView
implements SurfaceHolder.Callback {
private VideoPlayerEngine playerEngine;
public VideoSurfaceView(Context context) {
super(context);
getHolder().addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// 这里 Surface 才真正可用
playerEngine.setSurface(holder.getSurface());
playerEngine.start();
}
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
// 屏幕旋转或尺寸变化时触发
playerEngine.updateDisplaySize(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// 必须释放 Surface 引用,否则会内存泄漏
playerEngine.releaseSurface();
}
}
我曾经踩过的坑:在 surfaceDestroyed 里直接调了 playerEngine.release(),结果切到后台再回来,播放器彻底废了。正确的做法是只释放 Surface 引用,播放器引擎保留,等 Surface 重新创建后再绑定。
2.2 视频渲染线程的设计
SurfaceView 的渲染不能在主线程做。我单独开了一个渲染线程,专门处理视频帧的提交。
public class VideoRenderThread extends Thread {
private Surface surface;
private volatile boolean running = false;
private final Object lock = new Object();
@Override
public void run() {
running = true;
while (running) {
// 等待新的视频帧
VideoFrame frame = waitForFrame();
if (frame == null) continue;
// 渲染到 Surface
renderFrame(frame);
// 控制帧率,避免 CPU 空转
sleepForFrameRate();
}
}
private void renderFrame(VideoFrame frame) {
Canvas canvas = surface.lockCanvas(null);
if (canvas != null) {
try {
canvas.drawBitmap(frame.bitmap, 0, 0, null);
} finally {
surface.unlockCanvasAndPost(canvas);
}
}
}
public void release() {
running = false;
synchronized (lock) {
lock.notifyAll();
}
}
}
这里有个细节:lockCanvas 和 unlockCanvasAndPost 必须成对出现,而且要在 finally 里保证解锁。否则一旦抛出异常,Surface 就永远锁住了。
三、性能调优:让播放器飞起来
短视频播放器最怕什么?卡顿、掉帧、启动慢。我总结了几条实战经验:
3.1 预加载策略
用户还在看第 1 条视频时,第 2 条已经在后台开始缓冲了。这个策略我称之为「看一缓二」。
- 内存缓存:保留最近 3 条视频的 Surface 和解码器,切换时零延迟
- 磁盘缓存:视频播放完后,后台线程把文件缓存到本地,下次直接读本地
- 网络预加载:当前视频播放到 70% 时,触发下一条视频的 HTTP 请求
我的经验:预加载不是越多越好。曾经我试过预加载 5 条,结果内存爆了,反而更卡。后来调整为 3 条,效果最好。这个数字你可以根据设备内存动态调整。
3.2 硬解 vs 软解
说白了,硬解就是交给 GPU 解码,省电又高效。软解用 CPU 硬扛,兼容性好但费电。
| 对比项 | 硬解 | 软解 |
|---|---|---|
| CPU 占用 | 低(约 10-20%) | 高(约 50-80%) |
| 功耗 | 低 | 高(发热明显) |
| 兼容性 | 部分机型不支持某些编码 | 几乎全兼容 |
| 启动速度 | 快(约 100ms) | 慢(约 300ms) |
我的策略是:优先硬解,失败自动降级到软解。代码里加个 try-catch,硬解抛异常就切软解,用户无感知。
3.3 内存优化:别让 Bitmap 撑爆了
短视频场景下,每一帧都是 Bitmap。如果不加控制,内存分分钟飙到几百 MB。
// 使用 RGB_565 而不是 ARGB_8888,内存减半
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
// 复用 Bitmap,避免频繁 GC
Bitmap reusableBitmap = Bitmap.createBitmap(
width, height, Bitmap.Config.RGB_565);
options.inBitmap = reusableBitmap;
options.inMutable = true;
RGB_565 每个像素只占 2 字节,ARGB_8888 占 4 字节。对于视频播放来说,人眼根本分辨不出颜色差异,但内存直接省了一半。
四、发布前的最后检查
发布前我会过一遍 checklist,确保没有遗漏:
- 低端机测试:找一台 2GB 内存的旧手机跑一遍,看会不会 OOM
- 弱网模拟:用 Charles 限速到 100KB/s,看缓冲策略是否正常
- 快速滑动:在列表中疯狂上下滑动,看 Surface 切换是否流畅
- 后台恢复:按 Home 键再回来,看播放状态是否正确恢复
- 内存泄漏检测:用 LeakCanary 跑一遍,确保没有 Surface 泄漏
最后说一句:短视频播放器看起来简单,但坑都在细节里。Surface 的生命周期、渲染线程的同步、内存的复用,每一个点都值得认真对待。我做了这么多年,每次发布前还是会紧张——嗯,这大概就是工程师的宿命吧。
公众号:蓝海资料掘金营,微信deep3321