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

Kotlin音频录制APP点击按钮崩溃问题求助

音频录制APP点击按钮崩溃问题修复

问题描述

用Kotlin开发音频录制APP,完成后点击开始/停止/播放按钮时APP崩溃,Toast按钮功能正常。

开发环境

  • Android Studio版本:Android Studio Arctic Fox | 2020.3.1 Patch 3
  • SDK平台:Android 11.0(R)API Level 30

原代码文件

MainActivity.kt

package com.example.recordaudiodemo

import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.media.MediaPlayer
import android.media.MediaRecorder
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import android.widget.Button
import android.widget.Toast
import androidx.core.app.ActivityCompat
import kotlinx.android.synthetic.main.activity_main.*
import java.io.IOException

class MainActivity : AppCompatActivity() {

    private lateinit var mr : MediaRecorder

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val button4 : Button = findViewById(R.id.button4)
        val button5 : Button = findViewById(R.id.button5)
        val button6 : Button = findViewById(R.id.button6)
        val buttonT : Button = findViewById(R.id.buttonT)

        var path = Environment.getExternalStorageDirectory().toString()+"/myrec.3gp"
        mr = MediaRecorder()

        button4.isEnabled = false //start
        button5.isEnabled = false //stop

        if(ActivityCompat.checkSelfPermission(this,
                android.Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED)
            ActivityCompat.requestPermissions(this,
                arrayOf(Manifest.permission.RECORD_AUDIO,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE), 111)
            button4.isEnabled = true

        // start recording
        button4.setOnClickListener {
            mr.setAudioSource(MediaRecorder.AudioSource.MIC)
            mr.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
            mr.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
            mr.setOutputFile(path)
            try {
                mr.prepare()
            } catch (e: IOException) {
                // handle error
                e.printStackTrace()
            } catch (e: IllegalStateException) {
                // handle error
                e.printStackTrace()
            }
//            mr.prepare()
            mr.start()
            button5.isEnabled = true
            button4.isEnabled = false
//            Toast.makeText(this, "Start button clicked", Toast.LENGTH_SHORT).show()
        }

        // stop recording
        button5.setOnClickListener {
            mr.stop()
            button4.isEnabled = true
//            button5.isEnabled = false
        }

        // play recording
        button6.setOnClickListener {
            var mp = MediaPlayer()
            mp.setDataSource(path)
            mp.prepare()
            mp.start()
        }

        // Toast button
        buttonT.setOnClickListener {
            Toast.makeText(this, "Toast button clicked", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == 111 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
            button4.isEnabled = true
    }
}

build.gradle(app)

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions'
}

android {
    compileSdk 33

    defaultConfig {
        applicationId "com.example.recordaudiodemo"
        minSdk 21
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.9.0'
    implementation 'androidx.appcompat:appcompat:1.4.0'
    implementation 'com.google.android.material:material:1.6.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation 'com.android.support.test:rules:1.0.2'
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.recordaudiodemo">

    <uses-permission android:name="android.permission.RECORD_AUDIO"></uses-permission>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.RecordAudioDemo">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="36dp"
        android:text="Record Audio App"
        android:textSize="40dp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:text="STOP"
        app:layout_constraintBaseline_toBaselineOf="@+id/button4"
        app:layout_constraintStart_toEndOf="@+id/button4" />

    <Button
        android:id="@+id/button4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="48dp"
        android:layout_marginTop="140dp"
        android:text="START"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button6"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:text="PLAY"
        app:layout_constraintBaseline_toBaselineOf="@+id/button5"
        app:layout_constraintStart_toEndOf="@+id/button5" />

    <Button
        android:id="@+id/buttonT"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="143dp"
        android:layout_marginBottom="153dp"
        android:text="Toast Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>

问题分析与修复方案

1. Manifest语法错误

<activity>标签中android:exported="true"后缺少闭合的>,导致XML解析失败,直接引发APP运行崩溃。
修复后Manifest对应部分:

<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

2. 权限检查逻辑错误

原代码if语句未加花括号,导致无论权限是否授予,button4.isEnabled = true都会立即执行。用户可能在权限未通过时点击按钮,触发权限异常。
修复后权限检查代码:

if (ActivityCompat.checkSelfPermission(this,
        Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ||
    ActivityCompat.checkSelfPermission(this,
        Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this,
        arrayOf(Manifest.permission.RECORD_AUDIO,
            Manifest.permission.WRITE_EXTERNAL_STORAGE), 111)
} else {
    button4.isEnabled = true
}

3. MediaRecorder状态管理问题

  • 每次录制前未重置MediaRecorder,重复设置参数会触发IllegalStateException。
  • 停止录制后未释放资源,再次录制会出错。
    修复后录制/停止逻辑:
// start recording
button4.setOnClickListener {
    try {
        mr.reset()
        mr.setAudioSource(MediaRecorder.AudioSource.MIC)
        mr.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
        mr.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
        mr.setOutputFile(path)
        mr.prepare()
        mr.start()
        button5.isEnabled = true
        button4.isEnabled = false
        Toast.makeText(this, "Recording started", Toast.LENGTH_SHORT).show()
    } catch (e: IOException) {
        e.printStackTrace()
        Toast.makeText(this, "Prepare failed: ${e.message}", Toast.LENGTH_SHORT).show()
    } catch (e: IllegalStateException) {
        e.printStackTrace()
        Toast.makeText(this, "Recorder state error: ${e.message}", Toast.LENGTH_SHORT).show()
    }
}

// stop recording
button5.setOnClickListener {
    try {
        mr.stop()
        mr.release()
        mr = MediaRecorder() // 重新初始化以便下次录制
        button4.isEnabled = true
        button5.isEnabled = false
        Toast.makeText(this, "Recording stopped", Toast.LENGTH_SHORT).show()
    } catch (e: IllegalStateException) {
        e.printStackTrace()
        Toast.makeText(this, "Stop failed: ${e.message}", Toast.LENGTH_SHORT).show()
    }
}

4. MediaPlayer异常处理与资源释放

原代码MediaPlayer未处理异常,也未释放资源,容易引发崩溃。
修复后播放逻辑:

// play recording
button6.setOnClickListener {
    try {
        val mp = MediaPlayer()
        mp.setDataSource(path)
        mp.prepare()
        mp.start()
        mp.setOnCompletionListener {
            it.release() // 播放完成后释放资源
        }
        Toast.makeText(this, "Playing recording", Toast.LENGTH_SHORT).show()
    } catch (e: IOException) {
        e.printStackTrace()
        Toast.makeText(this, "Play failed: ${e.message}", Toast.LENGTH_SHORT).show()
    } catch (e: IllegalStateException) {
        e.printStackTrace()
        Toast.makeText(this, "Player state error: ${e.message}", Toast.LENGTH_SHORT).show()
    }
}

5. Android 11存储适配

Android 11及以上版本中,WRITE_EXTERNAL_STORAGE权限不再授予公共目录写入权限,直接使用Environment.getExternalStorageDirectory()会抛出异常。建议改用应用私有目录存储录音文件:

// 替换原path定义
val path = filesDir.absolutePath + "/myrec.3gp"

此目录属于应用私有空间,无需额外存储权限,更安全且符合Android存储规范。

6. 权限请求结果处理优化

原代码只检查了第一个权限的授予结果,需同时验证录音和存储权限:

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == 111) {
        val recordGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED
        val storageGranted = grantResults[1] == PackageManager.PERMISSION_GRANTED
        if (recordGranted && storageGranted) {
            button4.isEnabled = true
        } else {
            Toast.makeText(this, "需要录音和存储权限才能使用", Toast.LENGTH_SHORT).show()
        }
    }
}

修复后完整MainActivity.kt

package com.example.recordaudiodemo

import android.Manifest
import android.content.pm.PackageManager
import android.media.MediaPlayer
import android.media.MediaRecorder
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.core.app.ActivityCompat
import java.io.IOException

class MainActivity : AppCompatActivity() {

    private lateinit var mr: MediaRecorder

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val button4: Button = findViewById(R.id.button4)
        val button5: Button = findViewById(R.id.button5)
        val button6: Button = findViewById(R.id.button6)
        val buttonT: Button = findViewById(R.id.buttonT)

        // 使用应用私有目录存储录音
        val path = filesDir.absolutePath + "/myrec.3gp"
        mr = MediaRecorder()

        button4.isEnabled = false // start
        button5.isEnabled = false // stop

        // 检查录音和存储权限
        if (ActivityCompat.checkSelfPermission(this,
                Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ||
            ActivityCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                arrayOf(Manifest.permission.RECORD_AUDIO,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE), 111)
        } else {
            button4.isEnabled = true
        }

        // start recording
        button4.setOnClickListener {
            try {
                mr.reset()
                mr.setAudioSource(MediaRecorder.AudioSource.MIC)
                mr.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
                mr.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
                mr.setOutputFile(path)
                mr.prepare()
                mr.start()
                button5.isEnabled = true
                button4.isEnabled = false
                Toast.makeText(this, "Recording started", Toast.LENGTH_SHORT).show()
            } catch (e: IOException) {
                e.printStackTrace()
                Toast.makeText(this, "Prepare failed: ${e.message}", Toast.LENGTH_SHORT).show()
            } catch (e: IllegalStateException) {
                e.printStackTrace()
                Toast.makeText(this, "Recorder state error: ${e.message}", Toast.LENGTH_SHORT).show()
            }
        }

        // stop recording
        button5.setOnClickListener {
            try {
                mr.stop()
                mr.release()
                mr = MediaRecorder() // 重新初始化
                button4.isEnabled = true
                button5.isEnabled = false
                Toast.makeText(this, "Recording stopped", Toast.LENGTH_SHORT).show()
            } catch (e: IllegalStateException) {
                e.printStackTrace()
                Toast.makeText(this, "Stop failed: ${e.message}", Toast.LENGTH_SHORT).show()
            }
        }

        // play recording
        button6.setOnClickListener {
            try {
                val mp = MediaPlayer()
                mp.setDataSource(path)
                mp.prepare()
                mp.start()
                mp.setOnCompletionListener {
                    it.release()
                }
                Toast.makeText(this, "Playing recording", Toast.LENGTH_SHORT).show()
            } catch (e: IOException) {
                e.printStackTrace()
                Toast.makeText(this, "Play failed: ${e.message}", Toast.LENGTH_SHORT).show()
            } catch (e: IllegalStateException) {
                e.printStackTrace()
                Toast.makeText(this, "Player state error: ${e.message}", Toast.LENGTH_SHORT).show()
            }
        }

        // Toast button
        buttonT.setOnClickListener {
            Toast
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 23:45:22