Background
In order to solve the callback hell generated by asynchronous threads
//传统回调方式
api.login(phone,psd).enquene(new Callback<User>(){
public void onSuccess(User user){
api.submitAddress(address).enquene(new Callback<Result>(){
public void onSuccess(Result result){
...
}
});
}
});
//使用协程后
val user=api.login(phone,psd)
api.submitAddress(address)
...
What is a coroutine
Essentially, coroutines are lightweight threads.
Key nouns for coroutines
val job = GlobalScope.launch {
delay(1000)
println("World World!")
}
-
CoroutineScope (Scope of Action)
Control the execution thread and life cycle of the coroutine code block, including GlobeScope, lifecycleScope, viewModelScope and other custom CoroutineScope
GlobeScope: global scope, will not automatically end execution
lifecycleScope: lifecycle scope, used for activity and other lifecycle components, it will automatically end when DESTROYED, and additional introduction is required
viewModelScope: viewModel scope, used in ViewModel, will automatically end when ViewModel is recycled, additional introduction is required
-
Job
The unit of measurement of the coroutine, which is equivalent to a job task. The launch method returns a new Job by default
-
suspend
Acting on the method, it means that the method is a time-consuming task, such as the delay method above
public suspend fun delay(timeMillis: Long) {
...
}
The introduction of coroutines
Main frame ($coroutines_version is replaced with the latest version, such as 1.3.9, the same below)
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
lifecycleScope (optional, version 2.2.0)
implementation 'androidx.activity:activity-ktx:$lifecycle_scope_version'
viewModelScope (optional, version 2.3.0-beta01)
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$coroutines_viewmodel_version"
Simple to use
Let me give you a simple example
lifecycleScope.launch {
delay(2000)
tvTest.text="Test"
}
The function implemented in the above example is to wait for 2 seconds, and then modify the text value of the TextView control whose id is tvTest to Test
Custom delay return method
In Kotlin, for methods that require delay to return results, you need to use suspend to indicate
lifecycleScope.launch {
val text=getText()
tvTest.text = text
}
suspend fun getText():String{
delay(2000)
return "getText"
}
If you need to use Continuation for thread switching in other threads, you can use suspendCancellableCoroutine or suspendCoroutine to wrap (the former can be cancelled, which is equivalent to the extension of the latter), call it.resume() successfully, call it.resumeWithException(Exception()) if it fails, Throw an exception
suspend fun getTextInOtherThread() = suspendCancellableCoroutine<String> {
thread {
Thread.sleep(2000)
it.resume("getText")
}
}
Exception catch
Failures in the coroutine can be caught by exceptions to deal with special situations uniformly
lifecycleScope.launch {
try {
val text=getText()
tvTest.text = text
} catch (e:Exception){
e.printStackTrace()
}
}
Cancel function
Two jobs are executed below, the first one is original, the second one is to cancel the first job after 1 second, which will cause the text of tvText to not change
val job = lifecycleScope.launch {
try {
val text=getText()
tvTest.text = text
} catch (e:Exception){
e.printStackTrace()
}
}
lifecycleScope.launch {
delay(1000)
job.cancel()
}
Set timeout
This is equivalent to the system encapsulating the automatic cancellation function, corresponding to the function withTimeout
lifecycleScope.launch {
try {
withTimeout(1000) {
val text = getText()
tvTest.text = text
}
} catch (e:Exception){
e.printStackTrace()
}
}
Job with return value
Similar to launch, there is also an async method, which returns a Deferred object, which belongs to the extended class of Job. Deferred can get the returned result. The specific usage is as follows
lifecycleScope.launch {
val one= async {
delay(1000)
return@async 1
}
val two= async {
delay(2000)
return@async 2
}
Log.i("scope test",(one.await()+two.await()).toString())
}
Advanced advanced
Custom CoroutineScope
First look at the CoroutineScope source code
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
CoroutineScope mainly contains a coroutineContext object, we only need to implement the get method of coroutineContext to customize
class TestScope() : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = TODO("Not yet implemented")
}
To create a coroutineContext, you must first know what CoroutineContext is, and then we look at the source code of CoroutineContext
/**
* Persistent context for the coroutine. It is an indexed set of [Element] instances.
* An indexed set is a mix between a set and a map.
* Every element in this set has a unique [Key].
*/
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext =
...
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
public interface Element : CoroutineContext {
...
}
}
Through comments, we know that it is essentially a collection containing Element, but unlike the set and map collections, it implements acquisition (get), folding (fold, a combination of addition and replacement), and subtraction (minusKey, shift). Except), object combination (plus, such as val coroutineContext=coroutineContext1+coroutineContext2)
Its main content is Element, and the realization of Element has
- Job task
- ContinuationInterceptor interceptor
- AbstractCoroutineContextElement
- CoroutineExceptionHandler
- ThreadContextElement
- DownstreamExceptionElement
- …
You can see that Element is implemented in many places, and its main purpose is to limit the scope and handle exceptions. Here we first understand two important elements, one is Job and the other is CoroutineDispatcher
Job
- Job: If the child job is cancelled, the parent job and other child jobs will be cancelled; if the parent job is cancelled, all child jobs will be cancelled
- SupervisorJob: the parent job is cancelled, all child jobs are cancelled
CoroutineDispatcher
- Dispatchers.Main: main thread execution
- Dispatchers.IO: IO thread execution
We simulate a custom TestScope similar to lifecycleScope
class TestScope() : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = SupervisorJob() +Dispatchers.Main
}
Here we define a total process line SupervisorJob() and a specific execution environment Dispatchers.Main (Android main thread). If we want to replace the lifecycleScope of the activity, we need to create an instance in the activity
val testScope=TestScope()
Then cancel all jobs when the activity is destroyed
override fun onDestroy() {
testScope.cancel()
super.onDestroy()
}
Other usage methods are the same as lifecycleScope, such as
testScope.launch{
val text = getText()
tvTest.text = text
}
Deep understanding of Job
CoroutineScope contains a main job, and the jobs created by launch or other methods that are called later belong to the child jobs of CoroutineScope. Each job has its own state, including isActive, isCompleted, isCancelled, and some basic operations start(), cancel(), join(), the specific conversion process is as follows
Let's start with creating a job. When launch is called, there are three parameters CoroutineContext, CoroutineStart, and code block parameters by default.
- context: The object of CoroutineContext, the default is CoroutineStart.DEFAULT, which will be folded with the context of CoroutineScope
- start: The object of CoroutineStart, the default is CoroutineStart.DEFAULT, which represents immediate execution, and CoroutineStart.LAZY, which represents non-immediate execution, must call start() of the job to start execution
val job2= lifecycleScope.launch(start = CoroutineStart.LAZY) {
delay(2000)
Log.i("scope test","lazy")
}
job2.start()
When created in this mode, the default is the new state. At this time, isActive, isCompleted, and isCancelled are all false. When start is called, it is converted to the active state. Only isActive is true. If its task is completed, it will enter the Completing state. At this time, it is waiting for the completion of the child job. In this state, only isActive is true. If all child jobs are also completed, it will enter the Completed state, and only isCompleted is true. If there is a cancellation or exception in the active or Completing state, it will enter the Cancelling state. If it is necessary to cancel the parent job and other child jobs, it will wait for their cancellation to complete. At this time, only isCancelled is true. After the cancellation is completed, it will finally enter the Cancelled state, isCancelled And isCompleted are both true
State | isActive | isCompleted | isCancelled |
---|---|---|---|
New | FALSE | FALSE | FALSE |
Active | TRUE | FALSE | FALSE |
Completing | TRUE | FALSE | FALSE |
Cancelling | FALSE | FALSE | TRUE |
Cancelled | FALSE | TRUE | TRUE |
Completed | FALSE | TRUE | FALSE |
Different job interactions need to use join() and cancelAndJoin()
- join(): Add the current job to other coroutine tasks
- cancelAndJoin(): cancel the operation, just add it and then cancel it
val job1= GlobleScope.launch(start = CoroutineStart.LAZY) {
delay(2000)
Log.i("scope test","job1")
}
lifecycleScope.launch {
job1.join()
delay(2000)
Log.i("scope test","job2")
}
Deep understanding of suspend
Suspend as a new method modifier of kotlin, the final implementation is still java, let's look at their differences
suspend fun test1(){}
fun test2(){}
Corresponding to java code
public final Object test1(@NotNull Continuation $completion) {
return Unit.INSTANCE;
}
public final void test2() {
}
Corresponding bytecode
public final test1(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
...
L0
LINENUMBER 6 L0
GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;
ARETURN
L1
LOCALVARIABLE this Lcom/lieni/android_c/ui/test/TestActivity; L0 L1 0
LOCALVARIABLE $completion Lkotlin/coroutines/Continuation; L0 L1 1
MAXSTACK = 1
MAXLOCALS = 2
public final test2()V
L0
LINENUMBER 9 L0
RETURN
L1
LOCALVARIABLE this Lcom/lieni/android_c/ui/test/TestActivity; L0 L1 0
MAXSTACK = 0
MAXLOCALS = 1
As you can see, the method with suspend is actually the same as the normal method, except that there is an additional Continuation object when it is passed in, and the Unit.INSTANCE object is returned.
Continuation is an interface that contains the context object and resumeWith method
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
The specific implementation of Continuation is in BaseContinuationImpl
internal abstract class BaseContinuationImpl(...) : Continuation<Any?>, CoroutineStackFrame, Serializable {
public final override fun resumeWith(result: Result<Any?>) {
...
while (true) {
...
with(current) {
val outcome = invokeSuspend(param)
...
releaseIntercepted()
if (completion is BaseContinuationImpl) {
...
} else {
...
return
}
}
}
}
...
}
When we call resumeWith, it will continue to execute a loop, call invokeSuspend(param) and releaseIntercepted(), until the top-level completion is completed and return, and release the interceptor of the coroutine
The final release is implemented in ContinuationImpl
internal abstract class ContinuationImpl(...) : BaseContinuationImpl(completion) {
...
protected override fun releaseIntercepted() {
val intercepted = intercepted
if (intercepted != null && intercepted !== this) {
context[ContinuationInterceptor]!!.releaseInterceptedContinuation(intercepted)
}
this.intercepted = CompletedContinuation
}
}
Through here, the release is finally realized through the Element of ContinuationInterceptor in CoroutineContext
The same is true for suspending, continue to look at suspendCoroutine
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T =
suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
val safe = SafeContinuation(c.intercepted())
...
}
The intercepted() method of Continuation is called by default
internal abstract class ContinuationImpl(...) : BaseContinuationImpl(completion) {
...
public fun intercepted(): Continuation<Any?> =intercepted
?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
.also { intercepted = it }
}
It can be seen that the suspension is finally realized by the Element which is the ContinuationInterceptor in the CoroutineContext.
Process summary (thread switching)
- Create a new Continuation
- Call the interceptContinuation method of the context's ContinuationInterceptor in CoroutineScope to suspend the parent task
- Execute subtasks (if a thread is specified, it will be executed in a new thread and the Continuation object will be passed in)
- After the execution is completed, the user calls resume or resumeWith of Continuation to return the result
- Call the releaseInterceptedContinuation method of the context's ContinuationInterceptor in CoroutineScope to restore the parent task
Blocking and non-blocking
CoroutineScope does not block the current thread by default. If you need to block, you can use runBlocking. If you execute the following code in the main thread, a 2s white screen will appear.
runBlocking {
delay(2000)
Log.i("scope test","runBlocking is completed")
}
Blocking principle: Executing runBlocking will create BlockingCoroutine by default, and BlockingCoroutine will continue to execute a loop until the current Job is in the isCompleted state.
public fun <T> runBlocking(...): T {
...
val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine.joinBlocking()
}
private class BlockingCoroutine<T>(...) : AbstractCoroutine<T>(parentContext, true) {
...
fun joinBlocking(): T {
...
while (true) {
...
if (isCompleted) break
...
}
...
}
}
Let me share with you a copy of Google's advanced Kotlin enhanced practical manual (with Demo).
Chapter 1 Introduction to Kotlin
- Kotlin overview
- Comparison of Kotlin and Java
- Using Android Studio skillfully
- Know the basic types of Kotlin
- Walk into Kotlin's array
- Walk into the Kotlin collection
- Collection problem
- Complete code
- Basic grammar
Chapter 2 Kotlin Actual Combat Avoiding Pit Guide
- Method input parameters are constants and cannot be modified
- No Companion, INSTANCE?
- Java overloading, how do you make a clever transition in Kotlin?
- Empty posture in Kotlin
- Kotlin overwrites the methods in the Java parent class
- Kotlin gets "ruthless" and even TODO doesn't let it go!
- pit in is, as`
- Understanding of Property in Kotlin
- also keyword
- takeIf keyword
- takeIf keyword
- Singleton mode
Chapter 3 Project Actual Combat "Kotlin Jetpack Actual Combat"
- Starting from a demo that worships a great god
- What kind of experience is Kotlin writing Gradle scripts?
- The triple realm of Kotlin programming
- Kotlin higher-order functions
- Kotlin generics
- Kotlin extension
- Kotlin commission
- "Unknown" debugging skills for coroutines
- Graphical coroutine: suspend
Friends who need this PDF document can join the communication skirt here, front: 1102, middle: 405, and finally: 044. There are students and big guys in the skirt, and the resources are free to share.