Android帧提取应用崩溃求助:超80帧触发数组越界异常
解决视频帧提取超过80帧崩溃的问题
首先,咱们来拆解你遇到的崩溃根源:IndexOutOfBoundsException: Index: 0, Size: 0 是因为当提取帧数量增多后,内存占用急剧飙升,导致 FFmpegMediaMetadataRetriever.getFrameAtTime() 无法成功获取到帧(返回了null),而你的extractRGB()方法在处理null Bitmap时返回了空列表,随后调用rgb.get(0)就触发了索引越界异常。
结合你的代码,下面给出针对性的修复方案:
1. 复用FFmpegMediaMetadataRetriever,减少资源消耗
你现在每次循环都创建新的FFmpegMediaMetadataRetriever实例,这会频繁申请和释放资源,不仅效率低,还会加速内存耗尽。应该把它移到循环外面,复用同一个实例。
2. 强制处理帧获取失败的情况
必须判断getFrameAtTime()返回的Bitmap是否为null,避免后续操作触发空指针或索引越界。
3. 优化内存占用(核心!)
提取800帧时,把所有Bitmap和RGB数据都存在内存里必然会内存溢出。建议边提取边写入文件,不要在内存中缓存所有数据;如果确实需要临时存储,也应该分批处理,或者用软引用等方式避免内存堆积。
4. 修正tabColor的数据结构逻辑
你现在每次循环都把rouge、vert、bleu添加到tabColor里,这会导致tabColor里的列表重复累积,数据结构完全混乱,同时也会额外占用大量内存。应该只在最后统一添加一次这三个列表即可。
5. 确保UI操作在主线程执行
线程里直接调用progressDialog.dismiss()是不安全的,需要用runOnUiThread切换到主线程操作。
修改后的代码示例
public void extractionFrame(final File file) throws IOException, JCodecException { if (file == null || !file.exists()) { return; // 先做基础参数校验 } final String path = file.getAbsolutePath(); final int nbrFrame = 800; // 改为你需要的800帧 final long frameInterval = 34000; // 帧间隔时间,单位微秒 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final ProgressDialog progressDialog = new ProgressDialog(getContext()); progressDialog.setMessage("提取帧中..."); progressDialog.setCancelable(false); progressDialog.setTitle("请稍候"); progressDialog.show(); Thread thread = new Thread() { @Override public void run() { FFmpegMediaMetadataRetriever mmr = new FFmpegMediaMetadataRetriever(); try { mmr.setDataSource(path); final ArrayList<Integer> rouge = new ArrayList<>(); final ArrayList<Integer> vert = new ArrayList<>(); final ArrayList<Integer> bleu = new ArrayList<>(); // 提前初始化tabColor,避免重复添加 final ArrayList<ArrayList<Integer>> tabColor = new ArrayList<>(); tabColor.add(rouge); tabColor.add(vert); tabColor.add(bleu); long currentTimeUs = 340000; // 起始时间 for (int i = 0; i < nbrFrame; i++) { Bitmap b = mmr.getFrameAtTime(currentTimeUs, FFmpegMediaMetadataRetriever.OPTION_CLOSEST); if (b == null) { Log.w("FrameExtraction", "无法获取第" + i + "帧,跳过"); currentTimeUs += frameInterval; continue; } ArrayList<Integer> rgb = extractRGB(b); // 先判断rgb列表是否有效 if (rgb.size() >= 3) { rouge.add(rgb.get(0)); vert.add(rgb.get(1)); bleu.add(rgb.get(2)); } else { Log.w("FrameExtraction", "第" + i + "帧RGB提取失败"); } // 及时回收Bitmap,释放内存 b.recycle(); currentTimeUs += frameInterval; Log.d("FrameExtraction", "已提取第" + i + "帧"); } // 保存数据,建议改成分批保存进一步降低内存压力 saveList(tabColor, getContext()); } catch (Exception e) { e.printStackTrace(); } finally { mmr.release(); // 最后统一释放资源 } // 回到主线程关闭进度对话框 getActivity().runOnUiThread(new Runnable() { @Override public void run() { progressDialog.dismiss(); } }); } }; thread.start(); } } // 确保extractRGB能正确处理null Bitmap,返回有效列表 private ArrayList<Integer> extractRGB(Bitmap bitmap) { ArrayList<Integer> rgb = new ArrayList<>(); if (bitmap == null) { return rgb; } // 这里替换成你实际的RGB提取逻辑,确保返回size>=3的列表 int pixel = bitmap.getPixel(0, 0); rgb.add(Color.red(pixel)); rgb.add(Color.green(pixel)); rgb.add(Color.blue(pixel)); return rgb; }
额外优化建议
- 如果
saveList是将数据写入文件,建议改成每提取50-100帧就写入一次,而不是等全部提取完再写入,进一步降低内存压力。 - 可以考虑使用
AsyncTask或者Kotlin的Coroutines来处理异步任务,比手动创建线程更安全、易维护。 - 测试时可以用Android Studio的Profiler查看内存使用情况,定位内存泄漏或占用过高的点。
内容的提问来源于stack exchange,提问作者Bocar Timite
相关产品推荐
相关产品推荐

