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
相关产品推荐
相关产品推荐

