kotlin协程

作者: 慎独静思 | 来源:发表于2022-06-21 23:30 被阅读0次

协程是什么

协程是一个子程序调度组件,并且运行其挂起恢复。进程包含线程,线程包含协程。一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程共享该线程的资源。
协程是一种并发设计的模式,用来执行异步执行的代码。

协程的特点

轻量:可以在单个线程上运行多个协程,协程支持挂起,不会是正在执行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
内存泄露更少:使用结构化并发机制在一个作用域内执行多项操作。
内置取消机制:取消操作会自动在整个协程层次结构内传播。
jetpack集成:许多库提供全面协程支持的扩展。

添加依赖

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

viewModelScope属于view model ktx扩展的内容,需要添加view model ktx依赖,它是一个预定义的CoroutineScope,一个CoroutineScope管理一个或多个相关的协程。所有协程都必须在一个作用域内运行。
launch是一个函数,用于创建协程并将其函数主体的执行分派给相应的调度程序。
Dispatchers.IO指示此协程应在为I/O操作预留的线程上执行。
在该协程运行时,login函数会继续执行,并可能在网络请求完成前返回。
由于此协程通过viewModelScope启动,因此在viewModel的作用域内执行。如果view model销毁,则viewModelScope会自动取消,所有运行的协程也会被取消。

class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

withContext(Dispatchers.IO)将协程的操作移至一个IO线程,一种不错的做法是使用withContext来确保每个函数都是主线程安全的。
makeLoginRequest还会用关键字suspend进行标记。kotlin利用此关键字强制从协程内调用函数。

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

因为makeLoginRequest是一个suspend函数,而所有suspend函数都必须在协程中执行,所以此处需要协程。
launch未接受Dispatcher参数,那么从viewModelScope启动所有的协程都会在主线程中执行。
协程添加了suspend和resume来处理长时间执行的任务。
suspend用来暂停执行当前协程,并保存所有局部变量。
resume用于让已挂起的协程从挂起处继续执行。
如需调用suspend函数,只能从其他suspend函数进行调用或通过使用协程构建器来启动新的协程进行调用。

suspend fun fetchDocs() {                             // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result)                                      // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

get会在启动网络请求之前挂起协程,当网络请求完成时,get会恢复已挂起的协程,而不是使用回调通知主线程。
协程使用调度程序确定哪些线程用于执行协程。在kotlin中,所有协程都必须在调度程序中执行,协程可以自行挂起,而调度程序负责将其恢复。
kotlin提供了三个调度程序:
Dispatchers.Main:用于在主线程上运行协程,用于和界面交互和快速工作,比如调用suspend函数,更细UI等。
Dispatchers.IO:在主线程之外执行磁盘或网络I/O
*Dispatchers.Default:在主线程之外执行大量占用CPU资源的操作

启动协程的两种方式

launch:可启动新协程而不将结果返回给调用方,用于从常规函数启动新协程。
async:会启动一个新协程,并允许使用名为await的挂起函数返回结果,用于在另一个协程内或在挂起函数内且正在执行并行分解时才使用此方式。
coroutineScope用于启动一个或多个协程,然后,可以使用await或awaitAll等待协程返回结果之后函数再返回。

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
    }

协程概念

CoroutineScope会跟踪它使用launch或aysnc创建的所有协程。可以随时调用scope.cancel取消正在运行的工作。

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

已取消的作用域无法再创建协程。仅当控制其生命周期的类被销毁时才应调用cancel。使用viewModelScope时会自动在ViewModel#onCleared()时自动取消作用域。
Job是协程的句柄,新创建的每个job都返回一个job实例,通过它可以管理协程的生命周期。

class ExampleClass {
    ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (...) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}
CoroutinContext使用以下元素定义协程的行为:

Job:控制协程的生命周期;
CoroutineDispatcher: 将工作分配到适当的线程;
CoroutineName: 协程的名称;
CoroutineExceptionHandler:处理未捕获的异常;

Android协程最佳做法

注入调度程序
在创建新协程或调用withContext时,请勿对Dispatchers硬编码

// DO inject Dispatchers
class NewsRepository(
    private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}

// DO NOT hardcode Dispatchers
class NewsRepository {
    // DO NOT use Dispatchers.Default directly, inject it instead
    suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ }
}

挂起函数应该能够安全的从主线程调用
挂起函数应该是主线程安全的。

class NewsRepository(private val ioDispatcher: CoroutineDispatcher) {

    // As this operation is manually retrieving the news from the server
    // using a blocking HttpURLConnection, it needs to move the execution
    // to an IO dispatcher to make it main-safe
    suspend fun fetchLatestNews(): List<Article> {
        withContext(ioDispatcher) { /* ... implementation ... */ }
    }
}

// This use case fetches the latest news and the associated author.
class GetLatestNewsWithAuthorsUseCase(
    private val newsRepository: NewsRepository,
    private val authorsRepository: AuthorsRepository
) {
    // This method doesn't need to worry about moving the execution of the
    // coroutine to a different thread as newsRepository is main-safe.
    // The work done in the coroutine is lightweight as it only creates
    // a list and add elements to it
    suspend operator fun invoke(): List<ArticleWithAuthor> {
        val news = newsRepository.fetchLatestNews()

        val response: List<ArticleWithAuthor> = mutableEmptyList()
        for (article in news) {
            val author = authorsRepository.getAuthor(article.author)
            response.add(ArticleWithAuthor(article, author))
        }
        return Result.Success(response)
    }
}

ViewModel来创建协程
应该由ViewModel来创建协程,而不是公开挂起函数来执行业务逻辑。

// DO create coroutines in the ViewModel
class LatestNewsViewModel(
    private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {

    private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    fun loadNews() {
        viewModelScope.launch {
            val latestNewsWithAuthors = getLatestNewsWithAuthors()
            _uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)
        }
    }
}

// Prefer observable state rather than suspend functions from the ViewModel
class LatestNewsViewModel(
    private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
    // DO NOT do this. News would probably need to be refreshed as well.
    // Instead of exposing a single value with a suspend function, news should
    // be exposed using a stream of data as in the code snippet above.
    suspend fun loadNews() = getLatestNewsWithAuthors()
}

不要公开可变类型
最好向其他类公开不可变类型。

// DO expose immutable types
class LatestNewsViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(LatestNewsUiState.Loading)
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    /* ... */
}

class LatestNewsViewModel : ViewModel() {

    // DO NOT expose mutable types
    val uiState = MutableStateFlow(LatestNewsUiState.Loading)

    /* ... */
}

数据层或业务层应公开挂起函数或数据流。

参考:
https://developer.android.com/kotlin/coroutines?hl=zh-cn#kts

相关文章

网友评论

    本文标题:kotlin协程

    本文链接:https://www.haomeiwen.com/subject/wbksvrtx.html