You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

递归扫描音频文件时应用崩溃,多线程优化方案均失效求助

解决音频文件扫描导致的主线程卡顿与崩溃问题

看起来你踩了Android开发中很常见的主线程阻塞和UI线程安全的坑,咱们一步步拆解问题,然后给出可行的修复方案。

问题根源分析

  1. 主线程阻塞:你的walkDir递归扫描文件是IO密集型操作,直接在主线程执行会导致UI卡顿,甚至触发ANR(应用无响应),这就是你一开始崩溃的核心原因。
  2. 子线程操作UI:你尝试开新线程时,大概率是直接在子线程里调用了scannedFiles.setText(path)——Android明确要求所有UI操作必须在主线程执行,这种违规操作会引发隐性异常,间接导致输入调度超时。
  3. Dialog生命周期不匹配:BadTokenException是因为后台任务运行时,你的Activity可能已经被销毁(比如用户按了返回键),此时再调用mediaScanRunningBox.dismiss(),Dialog绑定的Activity上下文已经失效,自然会抛出异常。

修复方案

我们用**AsyncTask(适配你的Java代码)**来分离后台任务与UI线程,同时做好Dialog的生命周期管理,优化扫描效率:

1. 定义AsyncTask处理扫描逻辑

创建内部类AsyncTask,专门负责后台文件扫描,通过onProgressUpdate安全更新UI,onPostExecute处理扫描完成后的收尾工作:

private class MediaScanTask extends AsyncTask<File, String, Void> {
    private AlertDialog runningDialog;
    private TextView scannedFilesView;
    private List<String> audioPaths = new ArrayList<>();

    public MediaScanTask(AlertDialog dialog, TextView textView) {
        this.runningDialog = dialog;
        this.scannedFilesView = textView;
    }

    @Override
    protected Void doInBackground(File... params) {
        // 后台线程执行递归扫描
        walkDir(params[0]);
        return null;
    }

    @Override
    protected void onProgressUpdate(String... values) {
        super.onProgressUpdate(values);
        // 主线程更新TextView
        scannedFilesView.setText(values[0]);
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        // 安全关闭Dialog
        if (runningDialog != null && runningDialog.isShowing()) {
            runningDialog.dismiss();
        }
        // 批量扫描所有收集到的音频文件
        if (!audioPaths.isEmpty()) {
            String[] pathsArray = audioPaths.toArray(new String[0]);
            MediaScannerConnection.scanFile(getApplicationContext(), pathsArray, new String[]{"audio/*"}, null);
        }
        Toast.makeText(SettingsActivity.this, "Scan Completed", Toast.LENGTH_SHORT).show();
    }

    private void walkDir(File file) {
        if (isCancelled()) { // 任务被取消时提前退出,避免内存泄漏
            return;
        }
        File[] files = file.listFiles();
        if (files != null) {
            for (File filess : files) {
                if (filess.isFile()) {
                    String path = filess.getAbsolutePath();
                    // 过滤音频文件,减少无效操作
                    if (path.endsWith(".mp3") || path.endsWith(".flac") || path.endsWith(".wav") || path.endsWith(".aac")) {
                        publishProgress(path); // 通知主线程更新UI
                        audioPaths.add(path); // 收集音频路径,后续批量扫描
                    }
                } else {
                    walkDir(filess);
                }
            }
        }
    }
}

2. 修改按钮点击事件,正确启动任务

在rescanForMedia的按钮回调中,初始化Dialog并启动AsyncTask,同时保存任务引用:

private MediaScanTask currentScanTask;
private AlertDialog mediaScanRunningBox;

public void rescanForMedia(View view) {
    AlertDialog.Builder rescanInit = new AlertDialog.Builder(this);
    LayoutInflater layoutInflator = LayoutInflater.from(this);
    View v = layoutInflator.inflate(R.layout.dialog_mediascanner, null);
    rescanInit.setView(v);
    Button mediaScanButton = v.findViewById(R.id.mediaScanButton);
    final AlertDialog mediaScaninitBox = rescanInit.create();
    mediaScaninitBox.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
    mediaScaninitBox.show();

    mediaScanButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(SettingsActivity.this, "Scan Started", Toast.LENGTH_SHORT).show();
            AlertDialog.Builder rescanRunningBoxBuilder = new AlertDialog.Builder(SettingsActivity.this);
            LayoutInflater inflater = LayoutInflater.from(SettingsActivity.this);
            View runningView = inflater.inflate(R.layout.dialog_scanning, null);
            rescanRunningBoxBuilder.setView(runningView);
            mediaScanRunningBox = rescanRunningBoxBuilder.create();
            mediaScanRunningBox.show();
            TextView scannedFiles = runningView.findViewById(R.id.scannedFiles);
            mediaScaninitBox.dismiss();

            // 启动扫描任务
            currentScanTask = new MediaScanTask(mediaScanRunningBox, scannedFiles);
            currentScanTask.execute(Environment.getExternalStorageDirectory());
        }
    });
}

3. 处理Activity生命周期,避免内存泄漏

在Activity的onDestroy方法中,取消正在运行的任务并关闭Dialog:

@Override
protected void onDestroy() {
    super.onDestroy();
    if (currentScanTask != null && !currentScanTask.isCancelled()) {
        currentScanTask.cancel(true);
    }
    if (mediaScanRunningBox != null && mediaScanRunningBox.isShowing()) {
        mediaScanRunningBox.dismiss();
    }
}

关键优化点

  • UI操作安全:所有更新TextView、Dialog的操作都通过onProgressUpdate执行,严格遵守Android的UI线程规则。
  • 任务取消机制:Activity销毁时主动取消后台任务,避免无效操作和内存泄漏。
  • 批量扫描优化:不再逐个文件调用scanFile,而是收集所有音频路径后批量提交,大幅提升扫描效率,减少系统资源消耗。

内容的提问来源于stack exchange,提问作者Rektirino

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 06:41:17