在 viewModelScope 中从 Flow 收集数据是否会在 Android Studio 中阻塞 UI?
Will collection data from Flow in viewModelScope maybe block UI in Android Studio?
代码A来自官方article关于Flow
viewModelScope.launch{}
运行 默认在 UI 线程中,我认为 suspend fun fetchLatestNews()
默认也会 运行 在 UI 线程中,所以我认为代码 A 可能导致 UI 在 fetchLatestNews()
长时间操作时被阻塞,对吗?
我认为代码 B 可以解决问题,对吗?
代码A
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
init {
viewModelScope.launch {
// Trigger the flow and consume its elements using collect
newsRepository.favoriteLatestNews.collect { favoriteNews ->
// Update View with the latest favorite news
}
}
}
}
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val refreshIntervalMs: Long = 5000
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
while(true) {
val latestNews = newsApi.fetchLatestNews()
emit(latestNews) // Emits the result of the request to the flow
delay(refreshIntervalMs) // Suspends the coroutine for some time
}
}
}
// Interface that provides a way to make network requests with suspend functions
interface NewsApi {
suspend fun fetchLatestNews(): List<ArticleHeadline>
}
代码B
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
init {
viewModelScope.launch(Dispatchers.IO) {
//The same
}
}
}
//The same
新增内容:
致 Tenfour04:谢谢!
我认为挂起函数可能会阻塞UI,所以我认为定义dispatchers
是最重要的,以免阻塞UI!对吗?
如果我使用 Dispatchers.IO
.
,当我单击“开始”按钮显示信息时,代码 1 工作正常
如果我使用 Dispatchers.Main
.
,当我单击“开始”按钮时,代码 2 被冻结并且没有更新任何信息
如果我使用Dispatchers.Main
,代码3可以像代码1一样工作,原因只是因为我设置了delay(100)
。
顺便说一句,Flow 已暂停。所以如果有一个长时间的操作,即使它用协程包裹而不是soundDbFlow().collect { myInfo.value = it.toString() }
,我想我可以得到与代码1、代码2和代码3相同的测试结果。
此外,代码 4 在我为 Flow 添加 flowOn(Dispatchers.IO)
后就可以了,即使它是在 viewModelScope.launch(Dispatchers.Main){}
中启动的!
代码 1 正常
class HandleMeter: ViewModel() {
var myInfo = mutableStateOf("World")
private var myJob: Job?=null
private var k=0
private fun soundDbFlow() = flow {
while (true) {
emit(k++)
delay(0)
}
}
fun calCurrentAsynNew() {
myJob?.cancel()
myJob = viewModelScope.launch(Dispatchers.IO){
soundDbFlow().collect { myInfo.value = it.toString() }
}
}
fun cancelJob(){
myJob?.cancel()
}
}
@Composable
fun Greeting(handleMeter: HandleMeter) {
var info = handleMeter.myInfo
Column(
modifier = Modifier.fillMaxSize()
) {
Text(text = "Hello ${info.value}")
Button(
onClick = { handleMeter.calCurrentAsynNew() }
) {
Text("Start")
}
Button(
onClick = { handleMeter.cancelJob() }
) {
Text("Stop")
}
}
}
代码 2 冻结
class HandleMeter: ViewModel() {
private fun soundDbFlow() = flow {
while (true) {
emit(k++)
delay(0)
}
}
fun calCurrentAsynNew() {
myJob?.cancel()
myJob = viewModelScope.launch(Dispatchers.Main){
soundDbFlow().collect { myInfo.value = it.toString() }
}
}
...
//The same
}
...
//The same
代码 3 OK
class HandleMeter: ViewModel() {
private fun soundDbFlow() = flow {
while (true) {
emit(k++)
delay(100) //It's 100
}
}
fun calCurrentAsynNew() {
myJob?.cancel()
myJob = viewModelScope.launch(Dispatchers.Main){
soundDbFlow().collect { myInfo.value = it.toString() }
}
}
...
//The same
}
...
//The same
代码 4 正常
class HandleMeter: ViewModel() {
private fun soundDbFlow() = flow {
while (true) {
emit(k++)
delay(0)
}
}.flowOn(Dispatchers.IO)// I added
fun calCurrentAsynNew() {
myJob?.cancel()
myJob = viewModelScope.launch(Dispatchers.Main){
soundDbFlow().collect { myInfo.value = it.toString() }
}
}
...
//The same
}
...
//The same
代码A不会阻塞UI线程,因为launch
方法不会阻塞当前线程
如文档所述:
Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a [Job].
If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used.
因此,在您的情况下,CodeA 在后台使用 Dispatches.Default
,而 CodeB 使用 Dispatchers.IO
关于协程的更多信息here
@qki 写对了。但他的回答并不准确。 ViewModel 有 viewModelScope。 viewModelScope 有上下文 SuperVisorJob() + Dispatchers.Main.immediatly。 LatestNewsViewModel 的 init 将在主组线程中执行,但这不会阻塞 ui 线程。
挂起函数不会阻塞,除非它打破了挂起函数绝不能阻塞的约定。因此,从主调度程序调用协程并不重要。调用 fetchLatestNews()
、 不会阻塞主线程,除非 您不正确地编写了函数的实现,以至于它实际上阻塞了。
您通常不需要像在您的 代码 B 中那样执行此操作:
viewModelScope.launch(Dispatchers.IO) {
因为您通常不会在协程的顶层调用阻塞函数。如果是,您可以将这些部分包装在 withContext(Dispatchers.IO) { }
中。通常将协程留在主调度程序上更方便,因为 Android 中有很多 non-suspend 函数需要您从主线程调用它们。如果你翻转它,你可能需要 withContext(Dispatchers.Main) { }
在更多的地方比你需要相反的地方,并且你也会在协程实际启动之前产生一帧延迟。此外,如果您的协程与 ViewModel 中的属性交互,如果您只从 Main 调度程序接触它们,则可以避免并发访问属性的潜在问题,因为它是 single-threaded.
当您启动一个不与任何此类 Main-required 函数交互并直接调用阻塞函数的协程时,可能会有例外情况,但我认为这种情况应该很少见,尤其是如果您练习良好的话封装(see here)。如果将协程顶层的一大块代码分解成它自己的函数,则可以将该单独的函数变成一个挂起函数,必要时使用 withContext(Dispatchers.IO)
。那么你的顶层协程看起来会很干净。
代码A来自官方article关于Flow
viewModelScope.launch{}
运行 默认在 UI 线程中,我认为 suspend fun fetchLatestNews()
默认也会 运行 在 UI 线程中,所以我认为代码 A 可能导致 UI 在 fetchLatestNews()
长时间操作时被阻塞,对吗?
我认为代码 B 可以解决问题,对吗?
代码A
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
init {
viewModelScope.launch {
// Trigger the flow and consume its elements using collect
newsRepository.favoriteLatestNews.collect { favoriteNews ->
// Update View with the latest favorite news
}
}
}
}
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val refreshIntervalMs: Long = 5000
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
while(true) {
val latestNews = newsApi.fetchLatestNews()
emit(latestNews) // Emits the result of the request to the flow
delay(refreshIntervalMs) // Suspends the coroutine for some time
}
}
}
// Interface that provides a way to make network requests with suspend functions
interface NewsApi {
suspend fun fetchLatestNews(): List<ArticleHeadline>
}
代码B
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
init {
viewModelScope.launch(Dispatchers.IO) {
//The same
}
}
}
//The same
新增内容:
致 Tenfour04:谢谢!
我认为挂起函数可能会阻塞UI,所以我认为定义dispatchers
是最重要的,以免阻塞UI!对吗?
如果我使用 Dispatchers.IO
.
如果我使用 Dispatchers.Main
.
如果我使用Dispatchers.Main
,代码3可以像代码1一样工作,原因只是因为我设置了delay(100)
。
顺便说一句,Flow 已暂停。所以如果有一个长时间的操作,即使它用协程包裹而不是soundDbFlow().collect { myInfo.value = it.toString() }
,我想我可以得到与代码1、代码2和代码3相同的测试结果。
此外,代码 4 在我为 Flow 添加 flowOn(Dispatchers.IO)
后就可以了,即使它是在 viewModelScope.launch(Dispatchers.Main){}
中启动的!
代码 1 正常
class HandleMeter: ViewModel() {
var myInfo = mutableStateOf("World")
private var myJob: Job?=null
private var k=0
private fun soundDbFlow() = flow {
while (true) {
emit(k++)
delay(0)
}
}
fun calCurrentAsynNew() {
myJob?.cancel()
myJob = viewModelScope.launch(Dispatchers.IO){
soundDbFlow().collect { myInfo.value = it.toString() }
}
}
fun cancelJob(){
myJob?.cancel()
}
}
@Composable
fun Greeting(handleMeter: HandleMeter) {
var info = handleMeter.myInfo
Column(
modifier = Modifier.fillMaxSize()
) {
Text(text = "Hello ${info.value}")
Button(
onClick = { handleMeter.calCurrentAsynNew() }
) {
Text("Start")
}
Button(
onClick = { handleMeter.cancelJob() }
) {
Text("Stop")
}
}
}
代码 2 冻结
class HandleMeter: ViewModel() {
private fun soundDbFlow() = flow {
while (true) {
emit(k++)
delay(0)
}
}
fun calCurrentAsynNew() {
myJob?.cancel()
myJob = viewModelScope.launch(Dispatchers.Main){
soundDbFlow().collect { myInfo.value = it.toString() }
}
}
...
//The same
}
...
//The same
代码 3 OK
class HandleMeter: ViewModel() {
private fun soundDbFlow() = flow {
while (true) {
emit(k++)
delay(100) //It's 100
}
}
fun calCurrentAsynNew() {
myJob?.cancel()
myJob = viewModelScope.launch(Dispatchers.Main){
soundDbFlow().collect { myInfo.value = it.toString() }
}
}
...
//The same
}
...
//The same
代码 4 正常
class HandleMeter: ViewModel() {
private fun soundDbFlow() = flow {
while (true) {
emit(k++)
delay(0)
}
}.flowOn(Dispatchers.IO)// I added
fun calCurrentAsynNew() {
myJob?.cancel()
myJob = viewModelScope.launch(Dispatchers.Main){
soundDbFlow().collect { myInfo.value = it.toString() }
}
}
...
//The same
}
...
//The same
代码A不会阻塞UI线程,因为launch
方法不会阻塞当前线程
如文档所述:
Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a [Job].
If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used.
因此,在您的情况下,CodeA 在后台使用 Dispatches.Default
,而 CodeB 使用 Dispatchers.IO
关于协程的更多信息here
@qki 写对了。但他的回答并不准确。 ViewModel 有 viewModelScope。 viewModelScope 有上下文 SuperVisorJob() + Dispatchers.Main.immediatly。 LatestNewsViewModel 的 init 将在主组线程中执行,但这不会阻塞 ui 线程。
挂起函数不会阻塞,除非它打破了挂起函数绝不能阻塞的约定。因此,从主调度程序调用协程并不重要。调用 fetchLatestNews()
、 不会阻塞主线程,除非 您不正确地编写了函数的实现,以至于它实际上阻塞了。
您通常不需要像在您的 代码 B 中那样执行此操作:
viewModelScope.launch(Dispatchers.IO) {
因为您通常不会在协程的顶层调用阻塞函数。如果是,您可以将这些部分包装在 withContext(Dispatchers.IO) { }
中。通常将协程留在主调度程序上更方便,因为 Android 中有很多 non-suspend 函数需要您从主线程调用它们。如果你翻转它,你可能需要 withContext(Dispatchers.Main) { }
在更多的地方比你需要相反的地方,并且你也会在协程实际启动之前产生一帧延迟。此外,如果您的协程与 ViewModel 中的属性交互,如果您只从 Main 调度程序接触它们,则可以避免并发访问属性的潜在问题,因为它是 single-threaded.
当您启动一个不与任何此类 Main-required 函数交互并直接调用阻塞函数的协程时,可能会有例外情况,但我认为这种情况应该很少见,尤其是如果您练习良好的话封装(see here)。如果将协程顶层的一大块代码分解成它自己的函数,则可以将该单独的函数变成一个挂起函数,必要时使用 withContext(Dispatchers.IO)
。那么你的顶层协程看起来会很干净。