協(xié)程是什么
協(xié)程是一個子程序調(diào)度組件寞酿,并且運行其掛起恢復(fù)匠襟。進程包含線程钝侠,線程包含協(xié)程。一個線程中可以有任意多個協(xié)程宅此,但某一時刻只能有一個協(xié)程在運行机错,多個協(xié)程共享該線程的資源。
協(xié)程是一種并發(fā)設(shè)計的模式父腕,用來執(zhí)行異步執(zhí)行的代碼弱匪。
協(xié)程的特點
輕量:可以在單個線程上運行多個協(xié)程,協(xié)程支持掛起璧亮,不會是正在執(zhí)行協(xié)程的線程阻塞萧诫。掛起比阻塞節(jié)省內(nèi)存,且支持多個并行操作枝嘶。
內(nèi)存泄露更少:使用結(jié)構(gòu)化并發(fā)機制在一個作用域內(nèi)執(zhí)行多項操作帘饶。
內(nèi)置取消機制:取消操作會自動在整個協(xié)程層次結(jié)構(gòu)內(nèi)傳播。
jetpack集成:許多庫提供全面協(xié)程支持的擴展群扶。
添加依賴
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擴展的內(nèi)容及刻,需要添加view model ktx依賴镀裤,它是一個預(yù)定義的CoroutineScope,一個CoroutineScope管理一個或多個相關(guān)的協(xié)程缴饭。所有協(xié)程都必須在一個作用域內(nèi)運行暑劝。
launch是一個函數(shù),用于創(chuàng)建協(xié)程并將其函數(shù)主體的執(zhí)行分派給相應(yīng)的調(diào)度程序颗搂。
Dispatchers.IO指示此協(xié)程應(yīng)在為I/O操作預(yù)留的線程上執(zhí)行担猛。
在該協(xié)程運行時,login函數(shù)會繼續(xù)執(zhí)行丢氢,并可能在網(wǎng)絡(luò)請求完成前返回傅联。
由于此協(xié)程通過viewModelScope啟動,因此在viewModel的作用域內(nèi)執(zhí)行疚察。如果view model銷毀蒸走,則viewModelScope會自動取消,所有運行的協(xié)程也會被取消稍浆。
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)將協(xié)程的操作移至一個IO線程载碌,一種不錯的做法是使用withContext來確保每個函數(shù)都是主線程安全的。
makeLoginRequest還會用關(guān)鍵字suspend進行標(biāo)記衅枫。kotlin利用此關(guān)鍵字強制從協(xié)程內(nèi)調(diào)用函數(shù)嫁艇。
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函數(shù),而所有suspend函數(shù)都必須在協(xié)程中執(zhí)行弦撩,所以此處需要協(xié)程步咪。
launch未接受Dispatcher參數(shù),那么從viewModelScope啟動所有的協(xié)程都會在主線程中執(zhí)行益楼。
協(xié)程添加了suspend和resume來處理長時間執(zhí)行的任務(wù)猾漫。
suspend用來暫停執(zhí)行當(dāng)前協(xié)程,并保存所有局部變量感凤。
resume用于讓已掛起的協(xié)程從掛起處繼續(xù)執(zhí)行悯周。
如需調(diào)用suspend函數(shù),只能從其他suspend函數(shù)進行調(diào)用或通過使用協(xié)程構(gòu)建器來啟動新的協(xié)程進行調(diào)用陪竿。
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會在啟動網(wǎng)絡(luò)請求之前掛起協(xié)程禽翼,當(dāng)網(wǎng)絡(luò)請求完成時,get會恢復(fù)已掛起的協(xié)程族跛,而不是使用回調(diào)通知主線程闰挡。
協(xié)程使用調(diào)度程序確定哪些線程用于執(zhí)行協(xié)程。在kotlin中礁哄,所有協(xié)程都必須在調(diào)度程序中執(zhí)行长酗,協(xié)程可以自行掛起,而調(diào)度程序負責(zé)將其恢復(fù)桐绒。
kotlin提供了三個調(diào)度程序:
Dispatchers.Main:用于在主線程上運行協(xié)程夺脾,用于和界面交互和快速工作之拨,比如調(diào)用suspend函數(shù),更細UI等咧叭。
Dispatchers.IO:在主線程之外執(zhí)行磁盤或網(wǎng)絡(luò)I/O
*Dispatchers.Default:在主線程之外執(zhí)行大量占用CPU資源的操作
啟動協(xié)程的兩種方式
launch:可啟動新協(xié)程而不將結(jié)果返回給調(diào)用方敦锌,用于從常規(guī)函數(shù)啟動新協(xié)程。
async:會啟動一個新協(xié)程佳簸,并允許使用名為await的掛起函數(shù)返回結(jié)果,用于在另一個協(xié)程內(nèi)或在掛起函數(shù)內(nèi)且正在執(zhí)行并行分解時才使用此方式颖变。
coroutineScope用于啟動一個或多個協(xié)程生均,然后,可以使用await或awaitAll等待協(xié)程返回結(jié)果之后函數(shù)再返回腥刹。
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
}
協(xié)程概念
CoroutineScope會跟蹤它使用launch或aysnc創(chuàng)建的所有協(xié)程马胧。可以隨時調(diào)用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()
}
}
已取消的作用域無法再創(chuàng)建協(xié)程佩脊。僅當(dāng)控制其生命周期的類被銷毀時才應(yīng)調(diào)用cancel。使用viewModelScope時會自動在ViewModel#onCleared()時自動取消作用域垫卤。
Job是協(xié)程的句柄威彰,新創(chuàng)建的每個job都返回一個job實例,通過它可以管理協(xié)程的生命周期穴肘。
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使用以下元素定義協(xié)程的行為:
Job:控制協(xié)程的生命周期歇盼;
CoroutineDispatcher: 將工作分配到適當(dāng)?shù)木€程;
CoroutineName: 協(xié)程的名稱评抚;
CoroutineExceptionHandler:處理未捕獲的異常豹缀;
Android協(xié)程最佳做法
注入調(diào)度程序
在創(chuàng)建新協(xié)程或調(diào)用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) { /* ... */ }
}
掛起函數(shù)應(yīng)該能夠安全的從主線程調(diào)用
掛起函數(shù)應(yīng)該是主線程安全的慨代。
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來創(chuàng)建協(xié)程
應(yīng)該由ViewModel來創(chuàng)建協(xié)程邢笙,而不是公開掛起函數(shù)來執(zhí)行業(yè)務(wù)邏輯。
// 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)
/* ... */
}
數(shù)據(jù)層或業(yè)務(wù)層應(yīng)公開掛起函數(shù)或數(shù)據(jù)流氮惯。
參考:
https://developer.android.com/kotlin/coroutines?hl=zh-cn#kts