一、项目架构设计:从零搭建短视频播放器

做短视频播放器,我第一个想到的不是代码怎么写,而是架构怎么搭。说白了,架构就像房子的地基,地基不稳,后面加再多功能都是白搭。

我个人习惯,拿到需求先画一张整体架构图。这样能看清每个模块的职责,避免后期互相打架。

短视频播放器架构总览 UI 层(Activity / Fragment) SurfaceView + 手势控制 + 播放控制栏 业务逻辑层(Presenter / ViewModel) 播放状态管理 · 视频列表加载 · 缓存策略 手势事件分发 · 生命周期绑定 核心引擎层(MediaPlayer / ExoPlayer) 视频解码 · 音频同步 · 渲染控制 缓冲策略 · 硬解/软解切换 数据层(网络请求 · 本地缓存 · 数据库)

这张图我画了不下十遍才定稿。你想想看,每一层都有自己明确的职责,上层只管调用,下层只管干活,互不干扰。

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();
        }
    }
}

这里有个细节:lockCanvasunlockCanvasAndPost 必须成对出现,而且要在 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,确保没有遗漏:

  1. 低端机测试:找一台 2GB 内存的旧手机跑一遍,看会不会 OOM
  2. 弱网模拟:用 Charles 限速到 100KB/s,看缓冲策略是否正常
  3. 快速滑动:在列表中疯狂上下滑动,看 Surface 切换是否流畅
  4. 后台恢复:按 Home 键再回来,看播放状态是否正确恢复
  5. 内存泄漏检测:用 LeakCanary 跑一遍,确保没有 Surface 泄漏

最后说一句:短视频播放器看起来简单,但坑都在细节里。Surface 的生命周期、渲染线程的同步、内存的复用,每一个点都值得认真对待。我做了这么多年,每次发布前还是会紧张——嗯,这大概就是工程师的宿命吧。


公众号:蓝海资料掘金营,微信deep3321