Coroutines, or coroutines, are essentially lightweight threads whose scheduling switches are collaborative and can be actively suspended and resumed
Retrofit2 support for coroutines
Let’s start with retroFIT2, our most common retrofit2. What’s the difference between code that uses coroutines and code that doesn’t
Note that Retrofit2 only started supporting coroutines in 2.6.0, so be sure to upgrade Retrofit2 to 2.6.0 and above
First, define two apis, one that combines the use of RxJavA2 and one that combines the use of coroutines
interface TestApi {
@GET("api/4/news/latest")
fun getLatestNews(): Flowable<LatestNews>
@GET("api/4/news/latest")
suspend fun getLatestNews2(): LatestNews
}
Copy the code
As you can see, retrofit2 supports suspend defining the getLatestNews2api as a suspend function, which you can use in your coroutine
How do you use two different apis
class CoroutineActivity : AppCompatActivity() {... // This is the most common way we use RetroFIT2 to request data + switch threads funrequestData1() {
testApi.getLatestNews()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : DisposableSubscriber<LatestNews>() {
override fun onComplete() {}
override fun onNext(t: LatestNews) {
tv_text.text = Gson().toJson(t)
}
override fun onError(t: Throwable?) {
tv_text.text = "error"} // Use coroutine request + render data funrequestData2() {
GlobalScope.launch(Dispatchers.Main) {
try {
tv_text.text = Gson().toJson(testApi.getLatestNews2())
} catch (e: Exception) {
tv_text.text = "error"}}}}Copy the code
It is well known that RXJava2 renders data using callbacks
Coroutines need to start a coroutine using GlobalScope.launch (there are many ways to start coroutines, please check out the official documentation). Use dispatchers. Main to specify the coroutine scheduler as the Main thread (i.e., the UI thread) and then handle the normal and abnormal cases with a try catch. (Start the coroutine using the GlobalScope context for the moment.
It seems that using coroutines can simplify a lot of code and make it look more elegant
Let’s look at the concurrent and serial cases of multiple requests
Add a few more apis for easy operation
interface TestApi {
@GET("api/3/news/latest")
fun getLatestNews(): Flowable<LatestNews>
@GET("api/3/news/{id}")
fun getNewsDetail(@Path("id") id: Long): Flowable<News>
@GET("api/4/news/latest")
suspend fun getLatestNews2(): LatestNews
@GET("api/3/news/{id}")
suspend fun getNewsDetail2(@Path("id") id: Long): News
}
Copy the code
For example, we call getLatestNews() to request a list of stories, and then call getNewsDetail to request details of the first story, as shown below
// Non-coroutine usagetestApi.getLatestNews()
.flatMap {
testApi.getNewsDetail(it.stories!! [0].id!!) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : DisposableSubscriber<News>() { override funonComplete() {}
override fun onNext(t: News) {
tv_text.text = t.title
}
override fun onError(t: Throwable?) {
tv_text.text = "error"}}) // Coroutine usage globalscope.launch (dispatchers.main) {try {val lastedNews =testApi.getLatestNews2()
val detail = testApi.getNewsDetail2(lastedNews.stories!! [0].id!!) tv_text.text = detail.title } catch(e: Exception) { tv_text.text ="error"}}Copy the code
Another example is if we want to call getNewsDetail to request multiple news details at the same time
// Non-coroutine usagetestApi.getLatestNews()
.flatMap {
Flowable.zip(
testApi.getNewsDetail(it.stories!! [0].id!!) .testApi.getNewsDetail(it.stories!! [1].id!!) , BiFunction<News, News, List<News>> { news1, news2-> listOf(news1, news2) } ) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : DisposableSubscriber<List<News>>() { override funonComplete() {}
override fun onNext(t: List<News>) {
tv_text.text = t[0].title + t[1].title
}
override fun onError(t: Throwable?) {
tv_text.text = "error"Globalscope.launch (dispatchers.main) {try {val lastedNews =testApi.getlatestnews2 () // Use async to concurrently request details of the first and second news items val detail1 = async {testApi.getNewsDetail2(lastedNews.stories!! [0].id!!) } val detail2 = async {testApi.getNewsDetail2(lastedNews.stories!! [1].id!!) } tv_text.text = detail1.await().title + detail2.await().title } catch(e: Exception) { tv_text.text ="error"}}Copy the code
Coroutines can make your code more concise and elegant than non-coroutines (using RXJava2 in your code). They can clearly describe what you want to do first, second, and so on
Room database support for coroutines
The Room database began supporting coroutines in 2.1.0, and room-KTX dependencies needed to be imported
implementation "Androidx. Room: a room - KTX: 2.1.0." "
Copy the code
Then use suspend in the Dao to define the suspend function
@Dao
abstract class UserDao {
@Query("select * from tab_user")
abstract suspend fun getAll(): List<User>
}
Copy the code
Finally, use coroutines just like retroFIT2 did above
class RoomActivity : AppCompatActivity() {
private var adapter: RoomAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_room) ... }... private funloadUser() { GlobalScope.launch(Dispatchers.Main) { adapter!! .data = AppDataBase.getInstance().userDao().getAll() } } }Copy the code
The Android Jetpack Room database can be used in conjunction with other libraries. The Android Jetpack Room database can be used in conjunction with other libraries
Coroutines in Android applications
The examples above all use the GlobalScope context to launch the coroutine. In android, it is generally not recommended to use GlobalScope directly because with GlobalScope. Launch, we create a top-level coroutine. Although it is lightweight, it still consumes some memory resources when it runs, and it will continue to run if we forget to keep references to newly launched coroutines, so we must keep all references to globalScope.Launch launch coroutines, Then cancel out all coroutines when the activity destory (or something else that needs to cancel) is called, otherwise you can cause a memory leak and other problems
Such as:
class CoroutineActivity : AppCompatActivity() {
private lateinit var testApi: TestApi
private var job1: Job? = null
private var job2: Job? = null
private var job3: Job? = null
...
override fun onDestroy() { super.onDestroy() job1? .cancel() job2? .cancel() job3? .cancel() } ... // Start the first top-level coroutine funrequestData1() {
job1 = GlobalScope.launch(Dispatchers.Main) {
try {
val lastedNews = testApi.getLatestNews2() tv_text.text = lastedNews.stories!! [0].title } catch(e: Exception) { tv_text.text ="error"// Start the second top-level coroutine funrequestData2() {
job2 = GlobalScope.launch(Dispatchers.Main) {
try {
val lastedNews = testApi.getLatestNews2() // Start the third top-level coroutine inside the coroutine job3 = globalscope.launch (dispatchers.main) {try {val detail =testApi.getNewsDetail2(lastedNews.stories!! [0].id!!) tv_text.text = detail.title } catch (e: Exception) { tv_text.text ="error"
}
}
} catch(e: Exception) {
tv_text.text = "error"}}}}Copy the code
As you can see, if more coroutines are started using GlobalScope, the more variables must be defined to hold references to the start coroutine and cancel all of them when onDestroy is done
Let’s take a look at MainScope instead of GlobalScope
class CoroutineActivity : AppCompatActivity() {
private var mainScope = MainScope()
private lateinit var testApi: TestApi
...
override fun onDestroy() {super.ondestroy () // Simply call mainscope.cancel to cancel all coroutines mainscope.cancel ()} funrequestData1() {
mainScope.launch {
try {
val lastedNews = testApi.getLatestNews2() tv_text.text = lastedNews.stories!! [0].title } catch(e: Exception) { tv_text.text ="error"
}
}
}
fun requestData2() {
mainScope.launch {
try {
val lastedNews = testApi.getLatestNews2()
val detail = testApi.getNewsDetail2(lastedNews.stories!! [0].id!!) tv_text.text = detail.title } catch (e: Exception) { tv_text.text ="error"}}}}Copy the code
Or use the Kotlin delegate pattern to implement the following:
class CoroutineActivity : AppCompatActivity(), CoroutineScope by MainScope() {
private lateinit var testApi: TestApi
...
override fun onDestroy() {
super.onDestroy()
cancel()
}
fun requestData1() {
launch {
try {
val lastedNews = testApi.getLatestNews2() tv_text.text = lastedNews.stories!! [0].title } catch(e: Exception) { tv_text.text ="error"
}
}
}
fun requestData2() {
launch {
try {
val lastedNews = testApi.getLatestNews2()
val detail = testApi.getNewsDetail2(lastedNews.stories!! [0].id!!) tv_text.text = detail.title } catch (e: Exception) { tv_text.text ="error"}}}}Copy the code
Meanwhile, let’s first look at the definition of MainScope
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Copy the code
Using MainScope is very simple. Just call MainScope’s Cancel method in the Activity onDestroy. There is no need to define references to other coroutines, and MainScope’s scheduler is dispatchers. Main, so there is no need to manually specify the Main scheduler
Lifecycle support for coroutines
Lifecycle component library has support for coroutines in the alpha release of 2.2.0
Lifecycle – Run-time – KTX dependency needs to be added (please use the official version when it is available)
implementation "Androidx. Lifecycle: lifecycle - runtime - KTX: 2.2.0 - alpha05"
Copy the code
Lifecycle runtime-ktx adds lifecycleScope extension property (class MainScope) to LifecycleOwner for easy operation of coroutines.
Let’s look at the source code
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
if(existing ! = null) {returnexisting } val newScope = LifecycleCoroutineScopeImpl( this, / / SupervisorJob specified coroutines scope is a one-way transfer / / Dispatchers. Main. The immediate assign coroutines In the Main thread that executes / / Dispatchers. Main. Immediate with Dispatchers. The Main difference is that only if the current in the Main thread, it immediately execute coroutines, rather than walk the Dispatcher distribution process SupervisorJob () + Dispatchers. Main. Immediate)if (mInternalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
}
}
Copy the code
LifecycleCoroutineScope also provides a way to bind startup coroutines to the LifecycleOwner lifecycle (typically activities and fragments). As follows:
abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope { internal abstract val lifecycle: Lifecycle // Execute the coroutine body fun launchWhenCreated(block:suspendCoroutineScope.() -> Unit): Job = launch {lifecycle. WhenCreated (block)}suspendCoroutineScope.() -> Unit): Job = launch {lifecycle. WhenStarted (block)}suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenResumed(block)
}
}
Copy the code
Because the method above that starts the coroutine is bound to the activity life cycle, it also automatically cancels the coroutine when the Activity destroys
So the CoroutineActivity Demo code can be written more simply as follows:
class CoroutineActivity : AppCompatActivity() {
private lateinit var testApi: TestApi
...
fun requestData1() {
lifecycleScope.launchWhenResumed {
try {
val lastedNews = testApi.getLatestNews2() tv_text.text = lastedNews.stories!! [0].title } catch(e: Exception) { tv_text.text ="error"}}}}Copy the code
LiveData support for coroutines
LiveData is also supported with coroutines, but with the addition of the lifecycle- Livedata-ktx dependency
// Now it is still 'alpha'. When the official version is released, please replace it with official version"Androidx. Lifecycle: lifecycle - livedata - KTX: 2.2.0 - alpha05"
Copy the code
Lifecycle – Livedata – KTX dependency adds a top-level liveData function that returns CoroutineLiveData
The source code is as follows:
. internal const val DEFAULT_TIMEOUT = 5000L ... fun <T> liveData( context: CoroutineContext = EmptyCoroutineContext, timeoutInMs: Long = DEFAULT_TIMEOUT, @BuilderInference block:suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)
Copy the code
When does coroutine EliveData start coroutine and execute coroutine body?
internal class CoroutineLiveData<T>( context: CoroutineContext = EmptyCoroutineContext, timeoutInMs: Long = DEFAULT_TIMEOUT, block: Block<T> ) : MediatorLiveData<T>() { private var blockRunner: BlockRunner<T>? private var emittedSource: EmittedSource? = null init { val scope = CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob) blockRunner = BlockRunner( liveData = this, block = block, timeoutInMs = timeoutInMs, scope = scope ) { blockRunner = null } } ... Override Fun is executed when observe or observeForever is called for the first timeonActive() {super.onactive () // Start the coroutine and execute the coroutine body blockRunner? .mayberun ()} override Fun is invoked when removeObserver is calledonInactive() {super.oninactive () // blockRunner? .cancel() } }Copy the code
CoroutineLiveData starts the coroutine on onActive() and removes it on onInactive()
So using LiveData support for coroutines, the CoroutineActivity Demo code looks like this
class CoroutineActivity : AppCompatActivity() {
private lateinit var testApi: TestApi
...
fun requestData1() {
liveData {
try {
val lastedNews = testApi.getLatestNews2() emit(lastedNews.stories!! [0].title!!) } catch(e: Exception) { emit("error")
}
}.observe(this, Observer {
tv_text.text = it
})
}
}
Copy the code
We’ve covered some of the most common uses of coroutines in Android. Here are some of the basics
Coroutine context
The CoroutineContext is represented by CoroutineContext, In kotlin, Job, CoroutineDispatcher, and ContinuationInterceptor are subclasses of CoroutineContext, that is, they are coroutine contexts
Take a look at CoroutineContext’s more important plus method, which is an overloaded (+) operator method fixed with operator
@SinceKotlin("1.3")
public interface CoroutineContext {
/**
* Returns a context containing elements from this context and elements from other [context].
* The elements from this context with the same key as in the other one are dropped.
*/
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
}
Copy the code
For example, the above MainScope definition uses the + operator
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Copy the code
If you look at the source code for launching coroutines, you’ll see that kotlin uses the + operator a lot, so most CoroutineContext objects in Kotlin are CombinedContext objects
The example above uses the launch method to launch a coroutine with three parameters: the coroutine context, the coroutine launch mode, and the coroutine body
Public fun coroutinescope.launch (context: CoroutineContext = EmptyCoroutineContext, // CoroutineContext start: CoroutineStart = coroutinestart. DEFAULT, // coroutine startup mode block:suspend() -> Unit // coroutine body): Job {val newContext = newCoroutineContext(context) val coroutine =if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
Copy the code
Coroutine start mode
-
DEFAULT
Execute the coroutine body immediately
runBlocking { val job = GlobalScope.launch(start = CoroutineStart.DEFAULT) { println("1."+ thread.currentThread ()} // job.join()}Copy the code
Print the result
1: DefaultDispatcher-worker-1 Copy the code
The CoroutineStart.DEFAULT startup mode does not require manual calls to join or start methods. Instead, the code of the coroutine body is automatically executed when the launch method is called
-
LAZY
The coroutine body is executed only if needed
runBlocking { val job = GlobalScope.launch(start = CoroutineStart.LAZY) { println("1."+ thread.currentThread () {job.join()}Copy the code
Print the result
1: DefaultDispatcher-worker-1 Copy the code
CoroutineStart.LAZY startup mode must manually call methods such as join or start, otherwise the coroutine body will not execute
-
ATOMIC
The coroutine body is executed immediately, but cannot be cancelled before it starts running; that is, starting the coroutine ignores the cancelling state
runBlocking { val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) { println("1." + Thread.currentThread().name) delay(1000) println("2." + Thread.currentThread().name) } job.cancel() delay(2000) } Copy the code
Print the result
1: DefaultDispatcher-worker-1 Copy the code
CoroutineStart. ATOMIC start mode coroutines must be executed even if cancel is called, because opening coroutines ignores the cancelling state. JobCancellationException occurs when delay(1000) is executed and the following code is not executed. If delay(1000) catches an exception with a try catch, the following code continues
-
UNDISPATCHED
The coroutine body is executed immediately in the current thread until the thread following the suspend call depends on the scheduler in context
runBlocking { println("0."+ thread.currentThread ().name) + thread.currentThread ().name) Launch (context = dispatchers.default, start = CoroutineStart.UNDISPATCHED) { println("1." + Thread.currentThread().name) delay(1000) println("2." + Thread.currentThread().name) } delay(2000) } Copy the code
Print the result
0: main 1: main 2: DefaultDispatcher-worker-1 Copy the code
It can be seen that 0 and 1 are executed on the same thread. After delay(1000) is executed, the following code execution thread depends on the thread specified by the Dispatchers.default scheduler, so 2 is executed in another thread
Coroutine scheduler
The coroutine scheduler is also a coroutine context
The coroutine scheduler is used to specify which thread to execute coroutine code blocks. Kotlin provides several Default coroutine schedulers, Default, Main, and Unconfined, and a specific IO scheduler for the JVM
-
Dispatchers.Default
Specifies that the code block is executed in a thread pool
GlobalScope.launch(Dispatchers.Default) { println("1."+ thread.currentThread ().name) launch (dispatchers.default) {delay(1000)"2." + Thread.currentThread().name) } println("3." + Thread.currentThread().name) } Copy the code
The print result is as follows
1: DefaultDispatcher-worker-1 3: DefaultDispatcher-worker-1 2: DefaultDispatcher-worker-1 Copy the code
-
Dispatchers.Main
Specifies that the code block executes in the main thread (UI thread for Android)
GlobalScope.launch(Dispatchers.Default) { println("1."+ thread.currentThread ().name) launch (dispatchers.main) {delay(1000)"2." + Thread.currentThread().name) } println("3." + Thread.currentThread().name) } Copy the code
The print result is as follows:
1: DefaultDispatcher-worker-1 3: DefaultDispatcher-worker-1 2: main Copy the code
Dispatchers.Main specifies that coroutine blocks are executed in the Main thread
-
Dispatchers.Unconfined
No specific thread is specified in which the coroutine code will be executed, i.e. the thread in which it is currently executed, and the next code in the code block will be executed.
GlobalScope.launch(Dispatchers.Default) { println("1." + Thread.currentThread().name) launch (Dispatchers.Unconfined) { println("2."+ thread.currentThread ().name) requestApi() // delay(1000)"3." + Thread.currentThread().name) } println("4."+ thread.currentThread ().name)} // Define a suspend function to execute private in a new child Threadsuspend fun requestApi() = suspendCancellableCoroutine<String> { Thread { println("5: requestApi: " + Thread.currentThread().name) it.resume("success") }.start() } Copy the code
The print result is as follows:
1: DefaultDispatcher-worker-1 2: DefaultDispatcher-worker-1 5: requestApi: Thread-3 4: DefaultDispatcher-worker-1 3: Thread-3 Copy the code
You can see that the threads of code execution for 2 and 3 are significantly different; When executing in the requestApi, the code is switched to a child Thread (thread-3), and the next block of coroutine code is executed in Thread-3
-
Dispatchers.IO
It is based on the thread pool behind the Default scheduler and implements independent queues and limits, so the coroutine scheduler switching from Default to IO does not trigger a thread switch
GlobalScope.launch(Dispatchers.Default) { println("1." + Thread.currentThread().name) launch (Dispatchers.IO) { println("2." + Thread.currentThread().name) requestApi() // delay(1000) println("3." + Thread.currentThread().name) } println("4." + Thread.currentThread().name) } Copy the code
The print result is as follows:
1: DefaultDispatcher-worker-1 4: DefaultDispatcher-worker-1 2: DefaultDispatcher-worker-1 5: requestApi: Thread-3 3: DefaultDispatcher-worker-1 Copy the code
-
A scheduler bound to any custom thread (use this approach with caution)
You can create a Dispatcher using kotlin’s built-in newSingleThreadContext method or an ExecutorService extension called asCoroutineDispatcher
Val dispatcher = newSingleThreadContext("custom thread") / / the second method / / val dispatcher. = Executors newSingleThreadExecutor {r - > Thread (r,"custom thread") }.asCoroutineDispatcher() GlobalScope.launch(dispatcher) { println("1." + Thread.currentThread().name) delay(1000) println("2."+ thread.currentThread ()} runBlocking {delay(2000L)}Copy the code
The print result is as follows:
1: custom thread 2: custom thread Copy the code
As you can see, you can create your own thread and bind it to the coroutine scheduler, but this is not recommended because once you create a thread manually you need to close it manually, otherwise the thread will never terminate, which is dangerous
CoroutineScope GlobalScope, coroutineScope, container scope
Coroutine scope is a very heavy thing
-
GlobeScope
GlobeScope launches coroutines that start a separate scope and cannot inherit the scope of an outside coroutine, while its internal child coroutines follow the default scope rules
-
coroutineScope
A coroutine started by coroutineScope inherits the scope of the parent coroutine, its internal cancel operations propagate in both directions, and exceptions not caught by the child coroutine are passed up to the parent coroutine
-
supervisorScope
The container it is running on inherits the scope of its parent coroutine, which is different from the coroutineScope in that it is uni-directionalized, meaning that internal cancel operations and exception runs can only be propagated from the parent coroutine to its child coroutine, not its parent coroutine
The MainScope is the container that is used for the scope, so it’s only necessary that a child coroutine error or cancel won’t affect the parent coroutine, and therefore won’t affect the sibling coroutine
Coroutine exception pass mode
The exception handling of the coroutine is related to the scope of the coroutine, which is either two-way like the coroutineScope or unidirectional like the parent coroutine and the child coroutine it is designed to handle
It is designed for one-way circulation in the container
runBlocking {
println("1")
supervisorScope {
println("2"Launch {1/0} delay(100) println("3")
}
println("4")}Copy the code
The print result is as follows:
1
2
Exception in thread "main @coroutine#2" java.lang.ArithmeticException: / by zero
3
4
Copy the code
It’s safe to see that the child coroutine that is launched in the container is running properly, and it’s not going to cause the parent coroutine to run properly
We’re also going to verify that the parent coroutine exception is passed on to the child coroutine in the container it’s handling
runBlocking {
println("1")
supervisorScope {
println("2"Launch {try {delay(1000) println("3")
} catch (e: Exception) {
println("error"}} delay(100) 1/0 println("3")}}Copy the code
1
2
error
java.lang.ArithmeticException: / by zero
Copy the code
It’s clear that the parent coroutine is actually passing exceptions to its children in the container it’s handling
Bidirectional delivery for coroutineScope
runBlocking {
println("1")
try {
coroutineScope {
println("2"Launch {1/0} delay(100) println("3")
}
} catch (e: Exception) {
println("error")}}Copy the code
The print result is as follows:
1
2
error
Copy the code
You can see that a child coroutine started in the coroutineScope scope is passed to the parent coroutine if an exception occurs
Let’s verify again that the parent coroutine exception is passed to the child coroutine in the coroutineScope scope
runBlocking {
println("1")
coroutineScope {
println("2") // Launch a launch {try {delay(1000) println("3")
} catch (e: Exception) {
println("error")
}
}
delay(100)
1/0
println("3")}}Copy the code
The print result is as follows:
1
2
error
java.lang.ArithmeticException: / by zero
Copy the code
You can see that the parent coroutine does pass exceptions to the child coroutine in the coroutineScope scope
Coroutines cancel
Let’s take a look at some code
GlobalScope.launch {
println("1"Val job = launch {println("2"Delay (1000)} catch (e: Exception) {println(e: Exception) {delay(1000)}"error")
}
println("3")
if(isActive) {// If the coroutine cancels, isActive isfalse
println("4"} delay(1000) // If no exception is caught, execute println("5")
}
delay(100)
job.cancel()
}
Copy the code
The print result is as follows:
1
2
error
3
Copy the code
When the coroutine is started first and then cancel, the following situations occur:
- If code executed into the coroutine body depends on the coroutine’s Cancel state (such as the Delay method), it throws an exception, continues execution if it catches an exception, and terminates execution of the coroutine body if it does not
- If the code inside the coroutine does not depend on the coroutine’s Cancel state (the println method), execution continues
That is, the cancellation of a coroutine causes the coroutine to stop running by throwing an exception. If the coroutine’s code does not depend on the coroutine’s Cancel state (i.e. there is no error), then the cancellation of the coroutine generally has no effect on the execution of the coroutine
Such as:
GlobalScope.launch {
val job = launch {
println("==start==")
var i = 0
while (i <= 10) {
Thread.sleep(100)
println(i++)
}
println("==end==")
}
delay(100)
job.cancel()
}
Copy the code
The print result is as follows:
==start==
0
1
2
3
4
5
6
7
8
9
10
==end==
Copy the code
So even though the coroutine is canceled, the coroutine body continues to run
What do I do if I want to end the coroutine body?
At this point, you can use the isActive field of the CoroutineScope to determine whether the status of the coroutine has been canceled
GlobalScope.launch {
val job = launch {
println("==start==")
var i = 0
while (i <= 10 && isActive) {
Thread.sleep(100)
println(i++)
}
println("==end==")
}
delay(200)
job.cancel()
}
Copy the code
Print the result
==start==
0
1
==end==
Copy the code
If the coroutine is cancelled, you can use the isActive field to determine whether a piece of code in the coroutine body needs to be executed
withContext
When executing the coroutine body, it is convenient to switch the thread of code execution using withContext. Such as
Globalscope.launch (dispatchers.default) {// Execute println("1."+ thread.currentThread () withContext(dispatchers.main) {"2."+ thread.currentThread ()} // Execute println() in dispatchers.default Thread pool"3." + Thread.currentThread().name)
val dispatcher = newSingleThreadContext("custom thread"WithContext (dispatcher) {// Execute println(dispatcher)"4."+ thread.currentThread ()} dispatcher.close()"5." + Thread.currentThread().name)
}
Copy the code
Print the result
1: DefaultDispatcher-worker-1
2: main
3: DefaultDispatcher-worker-2
4: custom thread
5: DefaultDispatcher-worker-2
Copy the code
So you can easily switch the thread in which the code is running using withContext
The withContext can also work with the NonCancellable context to ensure that code blocks cannot be cancelled
GlobalScope.launch(Dispatchers.Default) {
val job = launch {
println("1."+ Thread.currentThread().name) try { delay(1000) } catch (e: Exception) {withContext(NonCancellable) {// Work with the NonCancellable context to ensure that the coroutine body cannot be cancelled println("error: "+ e.message) delay(100) // If withContext(NonCancellable) is not wrapped, delay(100) will cause the following code not to execute println("2." + Thread.currentThread().name)
}
}
}
delay(100)
job.cancel()
}
Copy the code
Print the result
1: DefaultDispatcher-worker-1
error: Job was cancelled
2: DefaultDispatcher-worker-1
Copy the code
Structured concurrency
What is structured concurrency?
In fact, it is very simple to ensure that the coroutines started are in the same scope.
A top-level coroutine is created when we launch a coroutine using GlobalScope.launch. If we launch a coroutine each time using GlobalScope.launch, many top-level coroutines are created without interfering with each other, that is, even if one coroutine fails or is cancelled, The other coroutine will continue to run because they are not in the same coroutine scope
Globalscope.launch (dispatchers.default) {val a1 = globalscope.async {launch async with launch delay(1000) println("1."+ thread.currentThread ()} val a2 = globalscope.async {delay(100) 1/0"2."+ thread.currentThread ().name)} a1.await() a2.await() // a2.cancel() can also use cancel}Copy the code
The print result is as follows
1: DefaultDispatcher-worker-1
Exception in thread "DefaultDispatcher-worker-1" java.lang.ArithmeticException: / by zero
Copy the code
If a2 reports an error or cancel, A1 will not be affected
What exactly is the problem?
For example, it is common for an activity to have multiple concurrent network requests requesting data (that is, multiple coroutines are started), and when one of the network requests fails (that is, the coroutine fails), we want to close the other parallel network requests and not process them (that is, we want to close the other coroutines), but this is not the case
If we always start coroutines with GlobalScope, we would have to keep referencing each coroutine and cancel all of them when the Activity destroys. Otherwise, the asynchronous request code in the coroutine will continue to execute even if the activity is destroyed, making it prone to errors or memory leaks
How can we solve this problem conveniently?
In fact, we can solve this problem by using structured concurrency (coroutine scope), which ensures that multiple coroutines are started in the same scope. If the scope context is cancelled, all subcoroutines started in this scope are cancelled. It can also coordinate with the coroutineScope, container coroutineScope scope to handle abnormal passing problems
So the above code can be changed like this
GlobalScope.launch(Dispatchers.Default) {
val a1 = async {
delay(1000)
println("1."+ thread.currentThread ().name)} val a2 = async {delay(100) 1/0"2." + Thread.currentThread().name)
}
a1.await()
a2.await()
}
Copy the code
That is, remove the GlobalScope that starts a1 and A2 coroutines to ensure that a1 and A2 are in the same coroutine scope
Analysis of the principle of coroutine suspension function
Let’s take a look at the retroFIT compatible coroutine implementation source code
suspendFun <T: Any> Call<T>.await(): T {// usesuspendCancellableCoroutine defines a suspend function that takes a Continuation objectreturn suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
val invocation = call.request().tag(Invocation::class.java)!!
val method = invocation.method()
val e = KotlinNullPointerException("Response from " +
method.declaringClass.name +
'. ' +
method.name +
" was null but response body type was declared as non-null") / / if the result is unusual, call the Continuation of resumeWithException callback Continuation. ResumeWithException (e)}else{// If the result is normal, the resume callback for the Continuation is called continuation.resume(body)}}else{// If the result is abnormal, Call the Continuation of resumeWithException callback Continuation. ResumeWithException (HttpException (response))}} override fun onFailure(call: Call<T>, t: Throwable) {/ / if the abnormal results, it is called a Continuation of resumeWithException callback Continuation. ResumeWithException (t)}}}})Copy the code
The source code and extension functions for continuations are shown below
@SinceKotlin("1.3")
@InlineOnly
public inline fun <T> Continuation<T>.resume(value: T): Unit =
resumeWith(Result.success(value))
/**
* Resumes the execution of the corresponding coroutine so that the [exception] is re-thrown right after the
* last suspension point.
*/
@SinceKotlin("1.3")
@InlineOnly
public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
resumeWith(Result.failure(exception))
@SinceKotlin("1.3")
public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/
public val context: CoroutineContext
/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/
public fun resumeWith(result: Result<T>)
}
Copy the code
As you can see, the coroutine suspend function uses a Callback internally to return the result. Continuation calls Resume to return the result if it returns normally, or resumeWithException to throw an exception otherwise, just like Callback
The reason we write coroutines that look synchronous is because the compiler does a lot of work for you.
Note: When decompiling kotlin coroutine code using AndroidStudio, it will cause serious ide lag, and the decompiled Java code will have countless nested layers. It is impossible to decompile the coroutine code. A bug in AndroidStudio that doesn’t work with kotlin’s decomcompiled Java code
State transitions of coroutines
We have already done some analysis of the coroutine suspend function principle. If we use multiple suspend functions, how do they work together?
Note: the code below is someone else’s code that I copied
suspend fun main() {
log(1) / /returnSuspended () is asuspendfunctionlog(returnSuspended())
log(2) // Delay is another onesuspendDelay function (1000).log(3) / /returnImmediately is also asuspendfunctionlog(returnImmediately())
log(4)}Copy the code
The corresponding Java implementation code logic is as follows (note that the following code logic is not very rigorous, only for learning to understand the use of coroutines)
Public class ContinuationImpl implements Continuation<Object> {// Label state default is 0 private int label = 0; private final Continuation<Unit> completion; public ContinuationImpl(Continuation<Unit> completion) { this.completion = completion; } @Override public CoroutineContextgetContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
try {
Object result = o;
switch (label) {
case0: { LogKt.log(1); . / / in SuspendFunctionsKt returnSuspended internal to callback to invoke this resumeWith method result = SuspendFunctionsKt. ReturnSuspended (this); // Label status + 1 label++;if (isSuspended(result)) return;
}
case1: { LogKt.log(result); LogKt.log(2); Result = delaykt. delay(1000, this); // Call this resumeWith method within delaykt. delay as a callback result = delaykt. delay(1000, this); // Label status + 1 label++;if (isSuspended(result)) return;
}
case2: { LogKt.log(3); . / / in SuspendFunctionsKt returnImmediately internal to callback to invoke this resumeWith method result = SuspendFunctionsKt. ReturnImmediately ( this); // Label status + 1 label++;if (isSuspended(result)) return;
}
case 3:{
LogKt.log(result);
LogKt.log(4);
}
}
completion.resumeWith(Unit.INSTANCE);
} catch (Exception e) {
completion.resumeWith(e);
}
}
private boolean isSuspended(Object result) {
returnresult == IntrinsicsKt.getCOROUTINE_SUSPENDED(); }}Copy the code
As you can see, the coordination between multiple suspended functions is achieved by incrementing the label status field by one and calling the resumeWith method repeatedly
The summary is as follows:
- The suspended function of a coroutine is essentially a callback, and the callback type is a Continuation
- The execution of the coroutine body is a state machine, and every time it encounters a suspended function, it is a state transition, just as the label in our previous example increments to achieve the state transition
Finally, thank you very much for cracking the Kotlin Coroutine blog. This is a very good article about learning Coroutine