Android:如何防止手机旋转时创建新线程并维持原线程运行
听起来你遇到的核心问题是屏幕旋转触发Activity销毁重建——旧线程没被清理,新Activity又启动了新线程,同时旧线程持有的UI控件引用已经失效,导致新UI无法更新,还出现多线程重复输出分贝值的情况。下面给你几个靠谱的解决方案,按推荐程度排序:
1. 用ViewModel托管检测逻辑(最推荐)
ViewModel的核心作用就是在配置变化(比如旋转)时保留数据和逻辑,不会随Activity销毁而消失。你可以把噪声检测的线程、音频采集对象(比如MediaRecorder)和状态数据都放到ViewModel里,通过LiveData通知UI更新。
代码示例:
第一步:创建NoiseDetectionViewModel
import android.media.MediaRecorder import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class NoiseDetectionViewModel : ViewModel() { // 用LiveData保存UI需要的状态 private val _decibelValue = MutableLiveData<Double>() val decibelValue: LiveData<Double> = _decibelValue private val _noiseStatusText = MutableLiveData<String>() val noiseStatusText: LiveData<String> = _noiseStatusText private val _noiseImageResId = MutableLiveData<Int>() val noiseImageResId: LiveData<Int> = _noiseImageResId private var mediaRecorder: MediaRecorder? = null private var isDetecting = false fun startDetection() { if (isDetecting) return // 避免重复启动线程 isDetecting = true viewModelScope.launch(Dispatchers.IO) { // 初始化MediaRecorder(根据你的实际采集逻辑调整) mediaRecorder = MediaRecorder().apply { setAudioSource(MediaRecorder.AudioSource.MIC) setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) setOutputFile("/dev/null") // 不需要保存文件,只采集数据 prepare() start() } while (isDetecting) { val amplitude = mediaRecorder?.maxAmplitude ?: 0 val db = if (amplitude > 0) 20 * Math.log10(amplitude.toDouble()) else Double.NEGATIVE_INFINITY // 切换到主线程更新LiveData withContext(Dispatchers.Main) { _decibelValue.value = db // 根据分贝值更新状态文本和图片 if (db >= 60) { _noiseStatusText.value = "房间嘈杂" _noiseImageResId.value = R.drawable.ic_noisy } else { _noiseStatusText.value = "房间安静" _noiseImageResId.value = R.drawable.ic_quiet } } delay(500) // 控制检测频率 } // 停止采集并释放资源 mediaRecorder?.apply { stop() release() } mediaRecorder = null } } fun stopDetection() { isDetecting = false } override fun onCleared() { super.onCleared() // ViewModel销毁时停止检测,避免内存泄漏 stopDetection() } }
第二步:在Activity中使用ViewModel
import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import androidx.lifecycle.ViewModelProvider import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { private lateinit var viewModel: NoiseDetectionViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this)[NoiseDetectionViewModel::class.java] // 观察LiveData,自动更新UI,旋转后会重新绑定 viewModel.decibelValue.observe(this) { db -> tv_decibel.text = if (db == Double.NEGATIVE_INFINITY) "-∞ dB" else String.format("%.1f dB", db) } viewModel.noiseStatusText.observe(this) { text -> tv_status.text = text } viewModel.noiseImageResId.observe(this) { resId -> iv_noise.setImageResource(resId) } // 启动检测(可以在onStart里调用,确保每次Activity可见时运行) viewModel.startDetection() } override fun onStop() { super.onStop() // 当Activity不可见时停止检测,节省资源 viewModel.stopDetection() } }
这种方案的优势:
- 旋转时ViewModel不会销毁,线程会持续运行(除非主动停止)
- 新Activity创建后会自动观察LiveData,UI实时更新
- 内置线程重复启动判断,不会出现多线程并行的问题
2. 阻止Activity旋转时重建
如果你的APP不需要针对横竖屏做差异化布局,可以在AndroidManifest.xml中给对应Activity添加configChanges属性,这样旋转时系统不会销毁重建Activity,而是调用onConfigurationChanged方法。
配置示例:
<activity android:name=".MainActivity" android:configChanges="orientation|screenSize|smallestScreenSize"> ... </activity>
若需要处理横竖屏布局切换,可在Activity中重写方法:
override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) // 手动切换横竖屏布局,比如: // setContentView(if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) R.layout.activity_main_land else R.layout.activity_main) }
这种方式的缺点是:横竖屏布局适配需要手动处理,灵活性不如ViewModel方案。
3. 手动清理旧线程(应急方案,不推荐)
若不想用上面两种方式,也可以在Activity的onDestroy方法中手动终止旧线程,但这种方式需要自己管理线程引用,容易出现内存泄漏或线程未及时停止的问题,同时仍需处理UI控件重新绑定的问题。
关键代码片段:
private var detectionThread: Thread? = null private var isDetecting = false override fun onDestroy() { super.onDestroy() isDetecting = false detectionThread?.join() // 等待线程终止 detectionThread = null }
额外提示:解决-∞输出问题
你看到的-∞是因为maxAmplitude返回0(比如MediaRecorder未准备就绪或采集刚启动),可以在代码中增加判断:当检测到-∞时,暂时显示"检测中..."提示,等拿到有效分贝值再更新正式状态。
内容的提问来源于stack exchange,提问作者user1836957

