You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android Studio 4模拟器测试录音播放功能时App崩溃求助

问题:点击停止录音按钮时App崩溃重启(Pixel 3a API 30模拟器)

我在Pixel 3a API 30模拟器上测试一款录音App时,点击停止录音按钮App持续崩溃重启。以下是使用的Java代码(来自YouTube教程):

//申请运行时权限
if (!checkPermissionFromDevice()) requestPermission();
//初始化视图
pl_btn = (Button)findViewById(R.id.play_btn);
rcrd_btn = (Button)findViewById(R.id.record_button);
stp_rcrd_btn = (Button)findViewById(R.id.stop_record_btn);
ps_btn = (Button)findViewById(R.id.pause_btn);
//从Android M开始,需申请运行时权限
rcrd_btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (checkPermissionFromDevice()) {
            pathSave = Environment.getExternalStorageDirectory()
                    .getAbsolutePath() + "/" + UUID.randomUUID().toString() + "audio_record.3gp";
            setupMediaRecorder();
            try {
                mediaRecorder.prepare();
                mediaRecorder.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
            pl_btn.setEnabled(false);
            ps_btn.setEnabled(false);
            rcrd_btn.setEnabled(false);
            stp_rcrd_btn.setEnabled(true);
            Toast.makeText(recording_and_play_test.this, "Recording...", Toast.LENGTH_SHORT).show();
        } else {
            requestPermission();
        }
    }
});
stp_rcrd_btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mediaRecorder.stop();
        stp_rcrd_btn.setEnabled(false);
        pl_btn.setEnabled(true);
        rcrd_btn.setEnabled(true);
        ps_btn.setEnabled(false);
    }
});
pl_btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ps_btn.setEnabled(true);
        stp_rcrd_btn.setEnabled(false);
        rcrd_btn.setEnabled(false);
        mediaPlayer = new MediaPlayer();
        try {
            mediaPlayer.setDataSource(pathSave);
            mediaPlayer.prepare();
        }catch (IOException e){
            e.printStackTrace();
        }
        mediaPlayer.start();
        Toast.makeText(recording_and_play_test.this, "Playing...", Toast.LENGTH_SHORT).show();
    }
});
ps_btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        stp_rcrd_btn.setEnabled(false);
        rcrd_btn.setEnabled(true);
        pl_btn.setEnabled(true);
        ps_btn.setEnabled(false);
        if (mediaPlayer != null){
            mediaPlayer.stop();
            mediaPlayer.release();
            setupMediaRecorder();
        }
    }
});
}
private void setupMediaRecorder() {
    mediaRecorder = new MediaRecorder();
    mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
    mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
    mediaRecorder.setAudioEncoder(MediaRecorder.OutputFormat.AMR_NB);
    mediaRecorder.setOutputFile(pathSave);
}
private void requestPermission() {
    ActivityCompat.requestPermissions(this, new String[]{
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.RECORD_AUDIO
    },REQUEST_PERMISSION_CODE);
}

崩溃日志如下:

E/MediaRecorder: stop called in an invalid state: 4
D/AndroidRuntime: Shutting down VM
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.adrsingingscope, PID: 7309
java.lang.IllegalStateException
    at android.media.MediaRecorder.stop(Native Method)
    at com.example.adrsingingscope.recording_and_play_test$2.onClick(recording_and_play_test.java:88)
    at android.view.View.performClick(View.java:7448)
    at android.view.View.performClickInternal(View.java:7425)
    at android.view.View.access$3600(View.java:810)
    at android.view.View$PerformClick.run(View.java:28305)
    at android.os.Handler.handleCallback(Handler.java:938)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:223)
    at android.app.ActivityThread.main(ActivityThread.java:7656)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

问题分析

日志里的E/MediaRecorder: stop called in an invalid state: 4是核心问题,状态4对应MediaRecorder.ERROR_INVALID_OPERATION,说明调用mediaRecorder.stop()时,Recorder处于不允许执行该操作的状态,主要原因有两个:

  1. 音频编码器配置错误:你的setupMediaRecorder()里误用了MediaRecorder.OutputFormat.AMR_NB作为音频编码器,应该使用MediaRecorder.AudioEncoder下的常量,这会导致录制初始化失败,进而触发stop时的异常。
  2. 缺少状态检查:没有判断mediaRecorder是否有效、是否处于录制状态就直接调用stop(),容易触发非法状态异常。

解决方案

1. 修复音频编码器配置错误

修改setupMediaRecorder()方法,替换正确的音频编码器常量:

private void setupMediaRecorder() {
    mediaRecorder = new MediaRecorder();
    mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
    mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
    // 替换为AudioEncoder下的常量
    mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
    mediaRecorder.setOutputFile(pathSave);
}

2. 安全调用stop(),添加状态检查与异常捕获

在停止录制的点击事件中,先验证mediaRecorder的有效性,同时捕获异常避免崩溃:

stp_rcrd_btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (mediaRecorder != null) {
            try {
                mediaRecorder.stop();
                // 停止后及时释放资源,避免重复操作出错
                mediaRecorder.release();
                mediaRecorder = null;
            } catch (IllegalStateException e) {
                e.printStackTrace();
                Toast.makeText(recording_and_play_test.this, "录制未正常启动", Toast.LENGTH_SHORT).show();
            }
        }
        // 更新按钮状态
        stp_rcrd_btn.setEnabled(false);
        pl_btn.setEnabled(true);
        rcrd_btn.setEnabled(true);
        ps_btn.setEnabled(false);
    }
});

3. 优化录制启动的异常处理

在录制启动时,捕获prepare()start()的异常,及时反馈用户并重置按钮状态:

rcrd_btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (checkPermissionFromDevice()) {
            pathSave = Environment.getExternalStorageDirectory()
                    .getAbsolutePath() + "/" + UUID.randomUUID().toString() + "audio_record.3gp";
            setupMediaRecorder();
            try {
                mediaRecorder.prepare();
                mediaRecorder.start();
                // 只有启动成功才更新按钮状态
                pl_btn.setEnabled(false);
                ps_btn.setEnabled(false);
                rcrd_btn.setEnabled(false);
                stp_rcrd_btn.setEnabled(true);
                Toast.makeText(recording_and_play_test.this, "Recording...", Toast.LENGTH_SHORT).show();
            } catch (IOException e) {
                e.printStackTrace();
                Toast.makeText(recording_and_play_test.this, "录制启动失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
                // 启动失败,重置按钮状态
                rcrd_btn.setEnabled(true);
                stp_rcrd_btn.setEnabled(false);
                // 释放无效的recorder
                if (mediaRecorder != null) {
                    mediaRecorder.release();
                    mediaRecorder = null;
                }
            }
        } else {
            requestPermission();
        }
    }
});

4. 补充权限申请回调

确保权限申请通过后再允许录制操作,添加onRequestPermissionsResult回调:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_PERMISSION_CODE) {
        boolean allGranted = true;
        for (int result : grantResults) {
            if (result != PackageManager.PERMISSION_GRANTED) {
                allGranted = false;
                break;
            }
        }
        if (!allGranted) {
            Toast.makeText(this, "需要录音和存储权限才能录制", Toast.LENGTH_SHORT).show();
        }
    }
}

额外提示

  • 在Android 10及以上版本,建议改用getExternalFilesDir()存储录制文件,无需额外的WRITE_EXTERNAL_STORAGE权限:
    pathSave = getExternalFilesDir(Environment.DIRECTORY_MUSIC)
            .getAbsolutePath() + "/" + UUID.randomUUID().toString() + "audio_record.3gp";
    
  • 在Activity的onDestroy()方法中释放剩余的MediaRecorderMediaPlayer资源,避免内存泄漏。

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

火山引擎 最新活动