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异常的完整思路:
这张图的核心思想是:每个步骤都可能出错,但错误处理要集中。不要在每个try块里都写一堆重复的释放代码,而是统一在finally里做清理。异常处理分支(右侧红色虚线框)负责记录日志和用户交互,业务逻辑分支(左侧蓝色流程)只关心正常流程。
总结一下我的经验
- 细粒度捕获:一个try块只做一件事,异常类型要精确
- 日志必须打:线上问题全靠日志定位,别偷懒
- 资源必须释放:finally里release,或者用try-with-resources
- 不要吞异常:空catch块是万恶之源
- 考虑回退方案:硬解不行就软解,本地文件不行就提示用户
嗯,异常处理这块其实没什么高深的技术,就是细心+习惯。你只要养成“先写try-finally再写业务逻辑”的习惯,就能避免90%的线上崩溃问题。