目录
- flow的数据异步返回,我们需要使用协程来监听,那么需要注意什么问题?
- activity、fragment中应该使用什么协程,非四大组件应该如何使用协程呢?用那个?
一、写一个噪音分析的功能作为例子
比如,我们每0.1秒读取一次噪音情况,然后将结果放到flow里面,让其他Activity可以监听
kotlin
object NoiseMeter {
private var mediaRecorder: MediaRecorder? = null
private var isRecording = false
private var scope: CoroutineScope? = null
private var updateInterval: Long = 100L // 更新间隔(毫秒)
// 噪音数据流
private val _noiseLevelFlow = MutableStateFlow(NoiseData(0.0, NoiseStatus.QUIET))
val noiseLevelFlow: StateFlow<NoiseData> = _noiseLevelFlow.asStateFlow()
// 初始化
fun initialize(context: Context, scope: CoroutineScope, updateInterval: Long = 100L) {
if (this.scope != null) return // 避免重复初始化
this.scope = scope
this.updateInterval = updateInterval
}
// 开始测量
fun start(context:Context) {
val cacheDir: File = context.cacheDir
if (isRecording || scope == null) return
scope!!.launch {
try {
mediaRecorder = MediaRecorder().apply {
// 1. 设置音频源
setAudioSource(MediaRecorder.AudioSource.MIC)
// 2. 设置输出格式
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
// 3. 设置音频编码
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
// 4. 设置输出文件(使用临时文件更安全)
val tempFile = File.createTempFile("temp_audio", ".3gp", cacheDir)
setOutputFile(tempFile.absolutePath)
prepare()
start()
}
isRecording = true
// 开始更新噪音数据
while (isRecording) {
val amplitude = mediaRecorder?.maxAmplitude?.toDouble() ?: 0.0
if (amplitude > 0) {
val db = 20 * log10(amplitude)
val status = when {
db < 40 -> NoiseStatus.QUIET
db < 70 -> NoiseStatus.NORMAL
else -> NoiseStatus.LOUD
}
_noiseLevelFlow.value = NoiseData(db, status)
}
delay(updateInterval)
}
} catch (e: Exception) {
e.printStackTrace()
Log.d("NoiseMeter", "start failed: ${e.message}")
releaseMediaRecorder()
}
}
}
private fun releaseMediaRecorder() {
try {
mediaRecorder?.stop()
mediaRecorder?.release()
} catch (e: Exception) {
Log.e("NoiseMeter", "Error releasing MediaRecorder: ${e.message}")
} finally {
mediaRecorder = null
isRecording = false
}
}
// 停止测量
fun stop() {
if (!isRecording) return
isRecording = false
mediaRecorder?.apply {
try {
stop()
release()
} catch (e: Exception) {
e.printStackTrace()
}
}
mediaRecorder = null
}
// 噪音数据类
data class NoiseData(
val decibels: Double,
val status: NoiseStatus
)
// 噪音状态枚举
enum class NoiseStatus {
QUIET, NORMAL, LOUD
}
}
在Activity中使用监听数据
kt
lifecycleScope.launch {
NoiseMeter.noiseLevelFlow.collect { noiseData ->
binding.noise.text = noiseData.toString()
}
}
如果之前没有接触过协程,这里可以理解为是开了一个线程这样去理解。
1.1 lifecycleScope是什么?
-
lifecycleScope
是每个LifecycleOwner
(如Activity
或Fragment
)都自带的一个CoroutineScope
。它的生命周期与所属的 UI 组件紧密相连。 - 当 UI 组件进入
onDestroy()
状态时,lifecycleScope
会自动取消在其中启动的所有协程。 -
collect
函数默认在调用它的协程的上下文中执行,而lifecycleScope.launch
默认使用Dispatchers.Main.immediate
作为其调度器。 Dispatchers.Main
: 这个调度器专门设计用于将协程的执行分发到 Android 的主线程(UI 线程)。Android 规定,更新 UI 的操作必须 在主线程上进行,否则会抛出CalledFromWrongThreadException
。- 在
collect
的 lambda 表达式内部(binding.noise.text = noiseData.toString()
)更新 UI 是安全的,因为它确实是在主线程上执行的。
可能这里就会有疑问,这个监听不是阻塞的?为什么不影响主线程呢? Flow
的 collect
函数是一个挂起函数。
挂起函数的关键在于,当它需要等待(比如等待 Flow 的下一个数据项、等待网络响应、等待文件读取完成)时,它不会阻塞其运行的线程。它会暂停 (挂起)当前协程的执行,并将线程的控制权交还给线程的调度器(例如 Android 的主线程 Looper)。
也就是说,协程挂起时,它所占用的线程(在这个例子中是宝贵的主线程!)就被释放了。这个线程可以立即去处理其他任务,比如绘制 UI、响应用户点击事件、处理其他消息队列中的消息。
当挂起函数等待的条件满足时(例如,Flow 发出了一个新的数据项),协程会在它原来运行的调度器上 (这里是 Dispatchers.Main
)被恢复执行,继续运行挂起点之后的代码。
其实这个代码还有问题。。。。。
1.2 repeatOnLifecycle
lifecycleScope
默认在 onDestroy()
时才取消所有协程,Activity 进入后台时(onPause()
/onStop()
),协程不会自动取消 ,也就是在非前台的时候,逻辑还是会执行,会导致不必要的传感器/网络资源消耗,CPU/电池资源浪费,甚至潜在崩溃风险。
解决方法是,使用 repeatOnLifecycle
,也是android官方推荐的最佳实践
kt
lifecycleScope.launch {
// 当生命周期至少达到 STARTED 时开始收集
// 当生命周期低于 STARTED 时自动取消收集
repeatOnLifecycle(Lifecycle.State.STARTED) {
NoiseMeter.noiseLevelFlow.collect { noiseData ->
binding.noise.text = noiseData.toString()
}
}
}
当 Activity 进入 onStart()
(可见但可能不在前台)时开始收集,当 Activity 进入 onStop()
时自动取消收集,当 Activity 再次回到前台时自动重新开始收集.
那么他是如何做到的呢?不是在collect的时候会被挂起?repeatOnLifecycle在collect的外层,理应干预不了呀。
看他的实现就知道,repeatOnLifecycle
不是一个普通的函数包装器,而是一个精心设计的生命周期感知协程构建器
kt
suspend fun LifecycleOwner.repeatOnLifecycle(
state: Lifecycle.State,
block: suspend CoroutineScope.() -> Unit
) {
// 1. 等待生命周期达到目标状态
awaitState(state)
// 2. 启动一个可取消的子协程
val job = currentCoroutineContext()[Job] ?: return
val controlledJob = Job(job)
try {
// 3. 在子协程中执行block
withContext(controlledJob) {
block()
}
} finally {
// 4. 当生命周期低于目标状态时
controlledJob.cancel()
}
}
也就是,不是在当前协程直接执行block,而是在新的子协程中执行。当生命周期状态下降时(如进入onStop),取消controlledJob,这会取消子协程中的collect操作。
如果是这下这种写法,可能会更加可读。
ini
lifecycleScope.launch {
NoiseMeter.noiseLevelFlow
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { noiseData ->
binding.noise.text = noiseData.toString()
}
}
使用建议:
-
对于简单的单个 Flow 收集,优先使用
flowWithLifecycle
-
对于多个 Flow 收集或需要复杂逻辑的场景,使用
repeatOnLifecycle
上述这些,基本都是和生命周期绑定相关的协程,或者说和四大组件相关的,那么在非四大组件的情况下,应该用什么呢?
二、非四大组件的情况下,使用哪些协程呢?
比如:自定义 CoroutineScope 、GlobalScope(谨慎使用)
作用域类型 | 适用场景 | 生命周期绑定 | 自动取消 | 推荐度 |
---|---|---|---|---|
lifecycleScope | Activity/Fragment | Activity/Fragment 生命周期 | onDestroy() 时 | ★★★★★ |
viewModelScope | ViewModel | ViewModel 生命周期 | onCleared() 时 | ★★★★★ |
ProcessLifecycleOwner.get().lifecycleScope | 应用全局服务 | 应用进程生命周期 | 进程终止时 | ★★★★☆ |
自定义 CoroutineScope | 自定义 View、全局服务 | 手动控制 | 手动取消 | ★★★★☆ |
GlobalScope | 极少数全局任务 | 无 | 永不取消 | ★☆☆☆☆ |
WorkManager | 后台任务、下载更新 | 系统管理 | 任务完成时 | ★★★★★ |
-
永远不要在 Activity/Fragment 中使用 GlobalScope
-
及时取消不再需要的协程
2.1 举个例子,我如下这个噪音读取,他是全局的,不依赖某个Activity的,那么应该使用那个方案?
kt
object NoiseMeter {
private var mediaRecorder: MediaRecorder? = null
private var isRecording = false
private var scope: CoroutineScope? = null
private var updateInterval: Long = 100L // 更新间隔(毫秒)
// 噪音数据流
private val _noiseLevelFlow = MutableStateFlow(NoiseData(0.0, NoiseStatus.QUIET))
val noiseLevelFlow: StateFlow<NoiseData> = _noiseLevelFlow.asStateFlow()
// 初始化
fun initialize(context: Context, scope: CoroutineScope, updateInterval: Long = 100L) {
if (this.scope != null) return // 避免重复初始化
this.scope = scope
this.updateInterval = updateInterval
}
// 开始测量
fun start(context:Context) {
val cacheDir: File = context.cacheDir
if (isRecording || scope == null) return
scope!!.launch {
try {
mediaRecorder = MediaRecorder().apply {
// 1. 设置音频源
setAudioSource(MediaRecorder.AudioSource.MIC)
// 2. 设置输出格式
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
// 3. 设置音频编码
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
// 4. 设置输出文件(使用临时文件更安全)
val tempFile = File.createTempFile("temp_audio", ".3gp", cacheDir)
setOutputFile(tempFile.absolutePath)
prepare()
start()
}
isRecording = true
// 开始更新噪音数据
while (isRecording) {
val amplitude = mediaRecorder?.maxAmplitude?.toDouble() ?: 0.0
if (amplitude > 0) {
val db = 20 * log10(amplitude)
val status = when {
db < 40 -> NoiseStatus.QUIET
db < 70 -> NoiseStatus.NORMAL
else -> NoiseStatus.LOUD
}
_noiseLevelFlow.emit(NoiseData(db, status))
}
delay(updateInterval)
}
} catch (e: Exception) {
e.printStackTrace()
Log.d("NoiseMeter", "start failed: ${e.message}")
releaseMediaRecorder()
}
}
}
private fun releaseMediaRecorder() {
try {
mediaRecorder?.stop()
mediaRecorder?.release()
} catch (e: Exception) {
Log.e("NoiseMeter", "Error releasing MediaRecorder: ${e.message}")
} finally {
mediaRecorder = null
isRecording = false
}
}
// 停止测量
fun stop() {
if (!isRecording) return
isRecording = false
mediaRecorder?.apply {
try {
stop()
release()
} catch (e: Exception) {
e.printStackTrace()
}
}
mediaRecorder = null
}
// 噪音数据类
data class NoiseData(
val decibels: Double,
val status: NoiseStatus
)
// 噪音状态枚举
enum class NoiseStatus {
QUIET, NORMAL, LOUD
}
}
使用 ProcessLifecycleOwner.get().lifecycleScope
(方便)
kt
// 初始化(不再需要传入scope)
fun initialize(context: Context, updateInterval: Long = 100L) {
if (this.scope != null) return
// 使用应用进程的生命周期作用域
this.scope = ProcessLifecycleOwner.get().lifecycleScope
this.updateInterval = updateInterval
}
因为这个获取噪音情况,肯定是在app运行期间一直运行的,不管我在哪个界面,应用启动时开始,应用终止时结束,无需手动管理协程取消,系统自动处理生命周期,- 应用进入后台时协程仍运行(符合需求)。
当然,也可以使用自定义全局CoroutineScope,只不过是需要自己取消协程(虽然应用结束,也是取消,哈哈哈,但你可以某些需求的情况下,可以暂停他。)
kotlin
object AppCoroutineScope {
private val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.IO + job)
// 在应用退出时调用
fun release() {
job.cancel()
}
}
class NoiseMeter private constructor() {
// ...其他成员变量...
fun initialize(context: Context, updateInterval: Long = 100L) {
if (this.scope != null) return
this.scope = AppCoroutineScope.scope
this.updateInterval = updateInterval
}
// ...其他方法保持不变...
}
好了,这篇文章就到这里,我们下期见~