background
To solve the callback hell created by asynchronous threads
// Traditional callback
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){... }}); }});Copy the code
// After using coroutines
val user=api.login(phone,psd)
api.submitAddress(address)
...
Copy the code
What is a coroutine
In essence, coroutines are lightweight threads.
Coroutine key nouns
val job = GlobalScope.launch {
delay(1000)
println("World World!")}Copy the code
-
CoroutineScope (Scope of action)
Controls the threads, lifecycleScope, viewModelScope, and other custom CoroutineScope blocks for execution
GlobeScope: global scope that does not automatically end execution
LifecycleScope: Lifecycle range used for activities and other life-cycle components that are automatically DESTROYED and need to be added
ViewModelScope: The scope of the viewModel, used in the viewModel, which is automatically terminated when the viewModel is reclaimed and needs to be added
-
Job
A measure of coroutines equivalent to a work task. The launch method returns a new Job by default
-
Suspend
Acting on a method means that the method is a time-consuming task, such as the delay method above
public suspend fun delay(timeMillis: Long){... }Copy the code
Introduction of coroutines
Main framework ($coroutines_version replaced with the latest version, e.g. 1.3.9, likewise below)
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
Copy the code
LifecycleScope (Optional, version 2.2.0)
implementation 'androidx.activity:activity-ktx:$lifecycle_scope_version'
Copy the code
ViewModelScope (optional, version 2.3-beta01)
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$coroutines_viewmodel_version"
Copy the code
Simple to use
Let’s start with a simple example
lifecycleScope.launch {
delay(2000)
tvTest.text="Test"
}
Copy the code
This example waits for 2 seconds and changes the text value of the TextView control with id tvTest to Test
Custom delayed return methods
In Kotlin, methods that require a delay to return results need to be labeled suspend
lifecycleScope.launch {
val text=getText()
tvTest.text = text
}
Copy the code
suspend fun getText(a):String{
delay(2000)
return "getText"
}
Copy the code
In other threads, if need to use Continuation thread, can use suspendCancellableCoroutine or suspendCoroutine package (the former can be cancelled, the equivalent of the latter’s extension), a successful call it. The resume (), Failure to call it.resumeWithexception (Exception()) to throw an Exception
suspend fun getTextInOtherThread(a) = suspendCancellableCoroutine<String> {
thread {
Thread.sleep(2000)
it.resume("getText")}}Copy the code
Exception handling
All failures in coroutines can be handled uniformly by exception catching
lifecycleScope.launch {
try {
val text=getText()
tvTest.text = text
} catch (e:Exception){
e.printStackTrace()
}
}
Copy the code
Cancel the function
The following two jobs are executed, the first is raw and the second cancels the first job after 1 second, which causes the text of tvText to remain unchanged
val job = lifecycleScope.launch {
try {
val text=getText()
tvTest.text = text
} catch (e:Exception){
e.printStackTrace()
}
}
lifecycleScope.launch {
delay(1000)
job.cancel()
}
Copy the code
Set the 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()
}
}
Copy the code
Job with return value
Similar to Launch, an async method returns a Deferred object, which is an extension of Job. Deferred gets the result returned
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())
}
Copy the code
Senior advanced
Custom CoroutineScope
First look at the CoroutineScope source code
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
Copy the code
CoroutineScope mainly contains a coroutineContext object, we want to customize just implement coroutineContext get method
class TestScope() : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = TODO("Not yet implemented")}Copy the code
CoroutineContext = coroutineContext = coroutineContext = 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 {. }}Copy the code
It is essentially a collection of elements, except that unlike sets and maps, it implements its own get, fold, subtraction, and object composition (plus, Such as val coroutineContext = coroutineContext1 + coroutineContext2)
Its main content is Element, and Element’s implementation has
- The Job tasks
- ContinuationInterceptor interceptor
- AbstractCoroutineContextElement
- CoroutineExceptionHandler
- ThreadContextElement
- DownstreamExceptionElement
- .
You can see that Element is implemented in many places, and its main purpose is to limit scope and exception handling. Here we first understand two important elements, one is Job, the other is CoroutineDispatcher
Job
- Job: If the child Job is cancelled, the parent Job and other child jobs are cancelled. The parent job is cancelled and all child jobs are cancelled
- The parent job is canceled, while all child jobs are canceled
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
}
Copy the code
We’ll be able to create instances in our activity if we want to replace the lifecycleScope for our activity
val testScope=TestScope()
Copy the code
Then cancel all jobs when the activity is destroyed
override fun onDestroy(a) {
testScope.cancel()
super.onDestroy()
}
Copy the code
Other uses are the same as lifecycleScope, e.g
testScope.launch{
val text = getText()
tvTest.text = text
}
Copy the code
In-depth understanding of Job
The CoroutineScope contains a main Job, and the jobs created by launch or other methods later belong to the child jobs of the CoroutineScope. Each Job has its own state. It includes isActive, isCompleted, isCancelled, and some basic operations start(), cancel(), and join(). The specific conversion process is as follows
We’ll start by creating the job. By default, launch is called with three parameters: CoroutineContext, CoroutineStart, and code block.
- Context: The CoroutineContext object, which by DEFAULT is coroutinestart. DEFAULT, collapses with the CoroutineScope context
- Start: indicates the object of the CoroutineStart. The DEFAULT value is coroutinestart.default, indicating that the execution is immediate.LAZY indicates that the execution is not immediate
val job2= lifecycleScope.launch(start = CoroutineStart.LAZY) {
delay(2000)
Log.i("scope test"."lazy")
}
job2.start()
Copy the code
When using this model to create the default state is new, so isActive isCompleted, isCancelled is false, when calling start, converted into the active state, only isActive is true, Completing the Completing task, it enters the Completing state, and waits for sub-jobs to complete. In this state, only isActive is true. Completing all sub-jobs isCompleted, and only isCompleted is true. If cancellation or abnormality occurs in active or Completing states, the Cancelling state will enter. If parent job and other child jobs need to be Cancelled, only isCancelled will be true, and finally Cancelled will be Cancelled. IsCancelled and isCompleted are 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 |
CancelAndJoin () and cancelAndJoin() are required for different job interactions.
- Join () : Adds the current job to other coroutine tasks
- CancelAndJoin () : Cancels the operation, only after it has been added
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")}Copy the code
Suspend deep understanding
Suspend is a new kotlin method modifier, and is ultimately implemented in Java, so let’s look at the differences
suspend fun test1(a){}
fun test2(a){}
Copy the code
Corresponding Java code
public final Object test1(@NotNull Continuation $completion) {
return Unit.INSTANCE;
}
public final void test2(a) {}Copy the code
Corresponding bytecode
public final test1(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; . L0 LINENUMBER6 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(a)V
L0
LINENUMBER 9 L0
RETURN
L1
LOCALVARIABLE this Lcom/lieni/android_c/ui/test/TestActivity; L0 L1 0
MAXSTACK = 0
MAXLOCALS = 1
Copy the code
As you can see, the suspend method is essentially the same as the normal method, except that it is passed in with a Continuation object and returns the Unit.instance object
Continuation is an interface that contains the context object and the resumeWith method
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
Copy the code
The implementation of continuations is in the 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}}}}... }Copy the code
When we call resumeWith, it will continue through a loop, calling invokeSuspend(param) and releaseIntercepted(), until the top-level completion execution returns and the coroutine’s interceptor is released
The final release is implemented in ContinuationImpl
internal abstract class ContinuationImpl(...). : BaseContinuationImpl(completion) { ...protected override fun releaseIntercepted(a) {
val intercepted = intercepted
if(intercepted ! =null&& intercepted ! = =this) { context[ContinuationInterceptor]!! .releaseInterceptedContinuation(intercepted) }this.intercepted = CompletedContinuation
}
}
Copy the code
From here the release is finally implemented through the Element of the ContinuationInterceptor in CoroutineContext
Same thing with pausing, suspendCoroutine
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T- > >)Unit): T =
suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
val safe = SafeContinuation(c.intercepted())
...
}
Copy the code
The intercepted() method of the Continuation is called by default
internal abstract class ContinuationImpl(...). : BaseContinuationImpl(completion) { ...public fun intercepted(a): Continuation<Any? > =intercepted ? : (context[ContinuationInterceptor]? .interceptContinuation(this) ?: this)
.also { intercepted = it }
}
Copy the code
As you can see, pauses are ultimately implemented through the Element of the ContinuationInterceptor in CoroutineContext
Process Summary (Thread switching)
- Creating a new Continuation
- Calling the interceptContinuation method of the ContinuationInterceptor of the Context in the CoroutineScope suspends the parent task
- Execute the subtask (in a new thread, if a thread is specified, passing in a Continuation object)
- After execution, the user calls resume or resumeWith with the Continuation and returns the result
- Call the CoroutineScope ContinuationInterceptor releaseInterceptedContinuation method to restore the parent task context
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 on the main thread, you will get a white screen for 2s
runBlocking {
delay(2000)
Log.i("scope test"."runBlocking is completed")}Copy the code
Blocking principle: Executing runBlocking creates BlockingCoroutine by default. BlockingCoroutine executes a loop until the current Job is isCompleted
public fun <T> runBlocking(...).: T {
...
val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine.joinBlocking()
}
Copy the code
private class BlockingCoroutine<T>(...). : AbstractCoroutine<T>(parentContext,true) {...fun joinBlocking(a): T {
...
while (true) {...if (isCompleted) break. }... }}Copy the code