17、错误处理与异常:常见异常类型、try-catch最佳实践

做音视频开发,说白了就是跟各种格式、编码、硬件打交道。你想想看,一个MP4文件可能从网络下载一半就断了,或者用户手机里存了个损坏的3GP文件。这时候如果代码没做好异常处理,直接崩掉,那体验可就太糟糕了。

我个人习惯是:写MediaExtractor代码之前,先把异常处理框架搭好。这不是小题大做,而是血的教训换来的经验。

常见异常类型,你一定会遇到

Android的MediaExtractor在运行时,主要抛出以下几类异常。我按出现频率排个序:

异常类型 触发场景 我的处理建议
IllegalStateException 状态不对时调用方法,比如没setDataSource就调用selectTrack 检查调用顺序,用状态标志位保护
IllegalArgumentException 传入无效参数,比如空的文件路径、负数的track索引 参数校验前置,不信任任何外部输入
IOException 文件不存在、权限不足、网络资源不可达 区分处理,给用户明确的提示
SecurityException 没有读取权限就尝试打开文件 运行时权限检查,不要假设用户给了权限
MediaCodec.CodecException 解码器初始化失败或解码过程中出错 回退到软解,或者提示设备不支持

重点提醒:MediaExtractor本身不会抛出MediaCodec.CodecException,但你在实际项目中,提取完数据后往往紧接着就要解码。所以我把这个异常也列进来了——它俩是“连体婴儿”,你躲不开的。

try-catch最佳实践,我踩过的坑

嗯,这里要注意。很多新手写try-catch,喜欢把一大坨代码全包进去。我曾经也这么干过,结果线上出了个bug,异常被吞掉了,查了两天才定位到问题。

我的做法是:细粒度捕获,精准处理。来看一个实际例子:

MediaExtractor extractor = new MediaExtractor();

try {
    // 第一步:设置数据源
    extractor.setDataSource(filePath);
} catch (IOException e) {
    // 文件不存在或无法访问
    Log.e(TAG, "文件打开失败,请检查路径和权限", e);
    showToast("无法读取视频文件");
    return;
} catch (SecurityException e) {
    // 权限不足
    Log.e(TAG, "缺少文件读取权限", e);
    requestStoragePermission();
    return;
}

try {
    // 第二步:选择轨道
    int trackIndex = selectBestTrack(extractor);
    if (trackIndex < 0) {
        Log.w(TAG, "没有找到可用的音视频轨道");
        return;
    }
    extractor.selectTrack(trackIndex);
} catch (IllegalStateException e) {
    // 状态异常,比如extractor已经被释放
    Log.e(TAG, "extractor状态异常", e);
    releaseExtractor(extractor);
    return;
} catch (IllegalArgumentException e) {
    // track索引越界
    Log.e(TAG, "轨道索引无效", e);
    return;
}

我的小技巧:每个try块只做一件事。这样异常发生时,你立刻就知道是哪一步出了问题。日志里带上上下文信息,线上排查时能省一半时间。

资源释放,比异常处理更重要

你想想看,如果try块里new了MediaExtractor,结果中间抛异常了,finally没写释放代码,那这个对象就泄漏了。Android系统对MediaCodec和MediaExtractor这类资源有限制,泄漏多了就会报“无法创建新的实例”。

我建议用try-finally或者try-with-resources(API 19+)来保证释放:

MediaExtractor extractor = null;
try {
    extractor = new MediaExtractor();
    extractor.setDataSource(inputFile);
    // ... 处理逻辑
} catch (IOException | IllegalStateException e) {
    Log.e(TAG, "处理过程中出错", e);
} finally {
    if (extractor != null) {
        extractor.release();
        extractor = null; // 帮助GC回收
    }
}

注意:release()方法本身也可能抛出异常。虽然概率极低,但严谨的做法是在finally里再包一层try-catch。不过我个人觉得没必要过度设计,release失败通常意味着系统资源已经严重异常了,这时候崩溃反而是更安全的选择。

避坑指南:我曾经犯过的错

我曾经在项目里遇到过一个诡异的问题:某些华为手机上,调用selectTrack之后,紧接着读取sample数据时偶尔会抛出IllegalStateException。查了半天才发现,原来是多线程环境下没有做同步——UI线程在释放extractor,而工作线程还在读数据。

解决方案很简单:用一个专用的HandlerThread来串行化所有MediaExtractor操作,或者加一个读写锁。我个人更推荐前者,因为音视频处理本身就是流水线式的,没必要搞并发。

还有一个坑:不要捕获Exception后什么都不做。我曾经看到有人这样写:

try {
    extractor.advance();
} catch (Exception e) {
    // 空catch块
}

这简直是灾难。异常被吞掉后,你永远不知道哪里出了问题。至少打个日志吧!

异常处理的核心逻辑图

下面这张图总结了我处理MediaExtractor异常的完整思路:

MediaExtractor 异常处理流程 开始提取 参数校验(路径、权限、格式) IllegalArgumentException / SecurityException setDataSource() IOException(文件/网络问题) selectTrack() / 读取数据 IllegalStateException(状态错误) 释放资源 (finally) 异常处理 • 记录日志 • 用户提示 • 权限申请 • 回退方案 • 资源释放

这张图的核心思想是:每个步骤都可能出错,但错误处理要集中。不要在每个try块里都写一堆重复的释放代码,而是统一在finally里做清理。异常处理分支(右侧红色虚线框)负责记录日志和用户交互,业务逻辑分支(左侧蓝色流程)只关心正常流程。

总结一下我的经验

  • 细粒度捕获:一个try块只做一件事,异常类型要精确
  • 日志必须打:线上问题全靠日志定位,别偷懒
  • 资源必须释放:finally里release,或者用try-with-resources
  • 不要吞异常:空catch块是万恶之源
  • 考虑回退方案:硬解不行就软解,本地文件不行就提示用户

嗯,异常处理这块其实没什么高深的技术,就是细心+习惯。你只要养成“先写try-finally再写业务逻辑”的习惯,就能避免90%的线上崩溃问题。