Android 协程的使用:结合一个环境噪音检查功能的例子来玩玩

目录

  1. flow的数据异步返回,我们需要使用协程来监听,那么需要注意什么问题?
  2. 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是什么?

  1. lifecycleScope是每个 LifecycleOwner(如 ActivityFragment)都自带的一个 CoroutineScope。它的生命周期与所属的 UI 组件紧密相连。
  2. 当 UI 组件进入 onDestroy()状态时,lifecycleScope会自动取消在其中启动的所有协程。
  3. collect函数默认在调用它的协程的上下文中执行,而 lifecycleScope.launch默认使用 Dispatchers.Main.immediate作为其调度器。​
  4. Dispatchers.Main:​ 这个调度器专门设计用于将协程的执行分发到 Android 的主线程(UI 线程)。Android 规定,更新 UI 的操作​必须​ 在主线程上进行,否则会抛出 CalledFromWrongThreadException
  5. collect的 lambda 表达式内部(binding.noise.text = noiseData.toString())更新 UI 是安全的,因为它确实是在主线程上执行的。

可能这里就会有疑问,这个监听不是阻塞的?为什么不影响主线程呢? Flowcollect函数是一个挂起函数。

挂起函数的关键在于,当它需要等待(比如等待 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
    }

    // ...其他方法保持不变...
}

好了,这篇文章就到这里,我们下期见~

相关推荐
阿华的代码王国4 小时前
【Android】内外部存储的读写
android·内外存储的读写
李少兄5 小时前
解决IntelliJ IDEA 提交代码时无复选框问题
java·ide·intellij-idea
cyforkk5 小时前
Spring Boot @RestController 注解详解
java·spring boot·后端
叫我阿柒啊6 小时前
从Java全栈到前端框架:一次真实面试的深度复盘
java·spring boot·typescript·vue·database·testing·microservices
点云SLAM6 小时前
C++ 常见面试题汇总
java·开发语言·c++·算法·面试·内存管理
sniper_fandc6 小时前
IDEA修改系统缓存路径,防止C盘爆满
java·ide·intellij-idea
aristo_boyunv6 小时前
拦截器和过滤器(理论+实操)
java·数据仓库·hadoop·servlet
半夏陌离6 小时前
SQL 入门指南:排序与分页查询(ORDER BY 多字段排序、LIMIT 分页实战)
java·前端·数据库
CUIYD_19896 小时前
Eclipse 常用搜索功能汇总
java·ide·eclipse