一文快速入门 Kotlin 协程(1)

log(“GlobalScope”)

}

log(“end”)

Thread.sleep(500)

}

[main] start

[main] end

[DefaultDispatcher-worker-1 @coroutine#1] GlobalScope

[DefaultDispatcher-worker-3 @coroutine#3] launch B

[DefaultDispatcher-worker-3 @coroutine#2] launch A

GlobalScope.launch 会创建一个顶级协程,尽管它很轻量级,但在运行时还是会消耗一些内存资源,且可以一直运行直到整个应用程序停止(只要任务还未结束),这可能会导致内存泄露,所以在日常开发中应该谨慎使用 GlobalScope

  • 2、runBlocking

也可以使用 runBlocking 这个顶层函数来启动协程,runBlocking 函数的第二个参数即协程的执行体,该参数被声明为 CoroutineScope 的扩展函数,因此执行体就包含了一个隐式的 CoroutineScope,所以在 runBlocking 内部可以来直接启动协程

public fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T

runBlocking 的一个方便之处就是:只有当内部相同作用域的所有协程都运行结束后,声明在 runBlocking 之后的代码才能执行,即 runBlocking 会阻塞其所在线程

看以下代码。runBlocking 内部启动的两个协程会各自做耗时操作,从输出结果可以看出来两个协程还是在交叉并发执行,且 runBlocking 会等到两个协程都执行结束后才会退出,外部的日志输出结果有明确的先后顺序。即 runBlocking 内部启动的协程是非阻塞式的,但 runBlocking 阻塞了其所在线程。此外,runBlocking 只会等待相同作用域的协程完成才会退出,而不会等待 GlobalScope 等其它作用域启动的协程

所以说,runBlocking 本身带有阻塞线程的意味,但其内部运行的协程又是非阻塞的,读者需要意会这两者的区别

fun main() {

log(“start”)

runBlocking {

launch {

repeat(3) {

delay(100)

log(“launchA - $it”)

}

}

launch {

repeat(3) {

delay(100)

log(“launchB - $it”)

}

}

GlobalScope.launch {

repeat(3) {

delay(120)

log(“GlobalScope - $it”)

}

}

}

log(“end”)

}

[main] start

[main] launchA - 0

[main] launchB - 0

[DefaultDispatcher-worker-1] GlobalScope - 0

[main] launchA - 1

[main] launchB - 1

[DefaultDispatcher-worker-1] GlobalScope - 1

[main] launchA - 2

[main] launchB - 2

[main] end

基于是否会阻塞线程的区别,以下代码中 runBlocking 会早于 GlobalScope 输出日志

fun main() {

GlobalScope.launch(Dispatchers.IO) {

delay(600)

log(“GlobalScope”)

}

runBlocking {

delay(500)

log(“runBlocking”)

}

//主动休眠两百毫秒,使得和 runBlocking 加起来的延迟时间少于六百毫秒

Thread.sleep(200)

log(“after sleep”)

}

[main] runBlocking

[DefaultDispatcher-worker-1] GlobalScope

[main] after sleep

  • 3、coroutineScope

coroutineScope 函数用于创建一个独立的协程作用域,直到所有启动的协程都完成后才结束自身。runBlocking 和 coroutineScope 看起来很像,因为它们都需要等待其内部所有相同作用域的协程结束后才会结束自己。两者的主要区别在于 runBlocking 方法会阻塞当前线程,而 coroutineScope不会阻塞线程,而是会挂起并释放底层线程以供其它协程使用。由于这个差别,runBlocking 是一个普通函数,而 coroutineScope 是一个挂起函数

fun main() = runBlocking {

launch {

delay(100)

log(“Task from runBlocking”)

}

coroutineScope {

launch {

delay(500)

log(“Task from nested launch”)

}

delay(100)

log(“Task from coroutine scope”)

}

log(“Coroutine scope is over”)

}

[main] Task from coroutine scope

[main] Task from runBlocking

[main] Task from nested launch

[main] Coroutine scope is over

  • 4、supervisorScope

supervisorScope 函数用于创建一个使用了 SupervisorJob 的 coroutineScope,该作用域的特点就是抛出的异常不会连锁取消同级协程和父协程

fun main() = runBlocking {

launch {

delay(100)

log(“Task from runBlocking”)

}

supervisorScope {

launch {

delay(500)

log(“Task throw Exception”)

throw Exception(“failed”)

}

launch {

delay(600)

log(“Task from nested launch”)

}

}

log(“Coroutine scope is over”)

}

[main @coroutine#2] Task from runBlocking

[main @coroutine#3] Task throw Exception

[main @coroutine#4] Task from nested launch

[main @coroutine#1] Coroutine scope is over

  • 5、自定义 CoroutineScope

假设我们在 Activity 中先后启动了多个协程用于执行异步耗时操作,那么当 Activity 退出时,必须取消所有协程以避免内存泄漏。我们可以通过保留每一个 Job 引用然后在 onDestroy方法里来手动取消,但这种方式相当来说会比较繁琐和低效。kotlinx.coroutines 提供了 CoroutineScope 来管理多个协程的生命周期

我们可以通过创建与 Activity 生命周期相关联的协程作用域的实例来管理协程的生命周期。CoroutineScope 的实例可以通过 CoroutineScope() 或 MainScope() 的工厂函数来构建。前者创建通用作用域,后者创建 UI 应用程序的作用域并使用 Dispatchers.Main 作为默认的调度器

class Activity {

private val mainScope = MainScope()

fun onCreate() {

mainScope.launch {

repeat(5) {

delay(1000L * it)

}

}

}

fun onDestroy() {

mainScope.cancel()

}}

或者,我们可以通过委托模式来让 Activity 实现 CoroutineScope 接口,从而可以在 Activity 内直接启动协程而不必显示地指定它们的上下文,并且在 onDestroy()中自动取消所有协程

class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {

fun onCreate() {

launch {

repeat(5) {

delay(200L * it)

log(it)

}

}

log(“Activity Created”)

}

fun onDestroy() {

cancel()

log(“Activity Destroyed”)

}}

从输出结果可以看出,当回调了onDestroy()方法后协程就不会再输出日志了

fun main() = runBlocking {

val activity = Activity()

activity.onCreate()

delay(1000)

activity.onDestroy()

delay(1000)

}

[main @coroutine#1] Activity Created

[DefaultDispatcher-worker-1 @coroutine#2] 0

[DefaultDispatcher-worker-1 @coroutine#2] 1

[DefaultDispatcher-worker-1 @coroutine#2] 2

[main @coroutine#1] Activity Destroyed

已取消的作用域无法再创建协程。因此,仅当控制其生命周期的类被销毁时,才应调用 scope.cancel()。例如,使用 viewModelScope 时,ViewModel 类会在 ViewModel 的 onCleared() 方法中自动取消作用域

六、CoroutineBuilder


  • 1、launch

看下 launch 函数的方法签名。launch 是一个作用于 CoroutineScope 的扩展函数,用于在不阻塞当前线程的情况下启动一个协程,并返回对该协程任务的引用,即 Job 对象

public fun CoroutineScope.launch(

context: CoroutineContext = EmptyCoroutineContext,

start: CoroutineStart = CoroutineStart.DEFAULT,

block: suspend CoroutineScope.() -> Unit

): Job

launch 函数共包含三个参数:

context。用于指定协程的上下文

start。用于指定协程的启动方式。默认值为 CoroutineStart.DEFAULT,即协程会在声明的同时就立即进入等待调度的状态,即可以立即执行的状态。可以通过将其设置为CoroutineStart.LAZY来实现延迟启动,即懒加载

block。用于传递协程的执行体,即希望交由协程执行的任务

可以看到 launchA 和 launchB 是并行交叉执行的

fun main() = runBlocking {

val launchA = launch {

repeat(3) {

delay(100)

log(“launchA - $it”)

}

}

val launchB = launch {

repeat(3) {

delay(100)

log(“launchB - $it”)

}

}

}

[main] launchA - 0

[main] launchB - 0

[main] launchA - 1

[main] launchB - 1

[main] launchA - 2

[main] launchB - 2

  • 2、Job

Job 是协程的句柄。使用 launch 或 async 创建的每个协程都会返回一个 Job 实例,该实例唯一标识协程并管理其生命周期。Job 是一个接口类型,这里列举 Job 几个比较有用的属性和函数

//当 Job 处于活动状态时为 true

//如果 Job 未被取消或没有失败,则均处于 active 状态

public val isActive: Boolean

//当 Job 正常结束或者由于异常结束,均返回 true

public val isCompleted: Boolean

//当 Job 被主动取消或者由于异常结束,均返回 true

public val isCancelled: Boolean

//启动 Job

//如果此调用的确启动了 Job,则返回 true

//如果 Job 调用前就已处于 started 或者是 completed 状态,则返回 false

public fun start(): Boolean

//用于取消 Job,可同时通过传入 Exception 来标明取消原因

public fun cancel(cause: CancellationException? = null)

//阻塞等待直到此 Job 结束运行

public suspend fun join()

//当 Job 结束运行时(不管由于什么原因)回调此方法,可用于接收可能存在的运行异常

public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle

Job 具有以下几种状态值,每种状态对应的属性值各不相同

StateisActiveisCompletedisCancelledNew (optional initial state)falsefalsefalseActive (default initial state)truefalsefalseCompleting (transient state)truefalsefalseCancelling (transient state)falsefalsetrueCancelled (final state)falsetruetrueCompleted (final state)falsetruefalse

fun main() {

//将协程设置为延迟启动

val job = GlobalScope.launch(start = CoroutineStart.LAZY) {

for (i in 0…100) {

//每循环一次均延迟一百毫秒

delay(100)

}

}

job.invokeOnCompletion {

log(“invokeOnCompletion:$it”)

}

log(“1. job.isActive:${job.isActive}”)

log(“1. job.isCancelled:${job.isCancelled}”)

log(“1. job.isCompleted:${job.isCompleted}”)

job.start()

log(“2. job.isActive:${job.isActive}”)

log(“2. job.isCancelled:${job.isCancelled}”)

log(“2. job.isCompleted:${job.isCompleted}”)

//休眠四百毫秒后再主动取消协程

Thread.sleep(400)

job.cancel(CancellationException(“test”))

//休眠四百毫秒防止JVM过快停止导致 invokeOnCompletion 来不及回调

Thread.sleep(400)

log(“3. job.isActive:${job.isActive}”)

log(“3. job.isCancelled:${job.isCancelled}”)

log(“3. job.isCompleted:${job.isCompleted}”)

}

[main] 1. job.isActive:false

[main] 1. job.isCancelled:false

[main] 1. job.isCompleted:false

[main] 2. job.isActive:true

[main] 2. job.isCancelled:false

[main] 2. job.isCompleted:false

[DefaultDispatcher-worker-2] invokeOnCompletion:java.util.concurrent.CancellationException: test

[main] 3. job.isActive:false

[main] 3. job.isCancelled:true

[main] 3. job.isCompleted:true

  • 3、async

看下 async 函数的方法签名。async 也是一个作用于 CoroutineScope 的扩展函数,和 launch 的区别主要就在于:async 可以返回协程的执行结果,而 launch 不行

public fun CoroutineScope.async(

context: CoroutineContext = EmptyCoroutineContext,

start: CoroutineStart = CoroutineStart.DEFAULT,

block: suspend CoroutineScope.() -> T

): Deferred

通过await()方法可以拿到 async 协程的执行结果,可以看到两个协程的总耗时是远少于七秒的,总耗时基本等于耗时最长的协程

fun main() {

val time = measureTimeMillis {

runBlocking {

val asyncA = async {

delay(3000)

1

}

val asyncB = async {

delay(4000)

2

}

log(asyncA.await() + asyncB.await())

}

}

log(time)

}

[main] 3

[main] 4070

由于 launch 和 async 仅能够在 CouroutineScope 中使用,所以任何创建的协程都会被该 scope 追踪。Kotlin 禁止创建不能够被追踪的协程,从而避免协程泄漏

  • 4、async 的错误用法

修改下上述代码,可以发现两个协程的总耗时就会变为七秒左右

fun main() {

val time = measureTimeMillis {

runBlocking {

val asyncA = async(start = CoroutineStart.LAZY) {

delay(3000)

1

}

val asyncB = async(start = CoroutineStart.LAZY) {

delay(4000)

2

}

log(asyncA.await() + asyncB.await())

}

}

log(time)

}

[main] 3

[main] 7077

会造成这不同区别是因为 CoroutineStart.LAZY不会主动启动协程,而是直到调用async.await()或者async.satrt()后才会启动(即懒加载模式),所以asyncA.await() + asyncB.await()会导致两个协程其实是在顺序执行。而默认值 CoroutineStart.DEFAULT 参数会使得协程在声明的同时就被启动了(实际上还需要等待被调度执行,但可以看做是立即就执行了),所以即使 async.await()会阻塞当前线程直到协程返回结果值,但两个协程其实都是处于运行状态,所以总耗时就是四秒左右

此时可以通过先调用start()再调用await()来实现第一个例子的效果

asyncA.start()

asyncB.start()

log(asyncA.await() + asyncB.await())

  • 5、async 并行分解

由 suspend 函数启动的所有协程都必须在该函数返回结果时停止,因此你可能需要保证这些协程在返回结果之前完成。借助 Kotlin 中的结构化并发机制,你可以定义用于启动一个或多个协程的 coroutineScope。然后,你可以使用 await()(针对单个协程)或 awaitAll()(针对多个协程)保证这些协程在从函数返回结果之前完成

例如,假设我们定义一个用于异步获取两个文档的 coroutineScope。通过对每个延迟引用调用 await(),我们可以保证这两项 async 操作在返回值之前完成:

suspend fun fetchTwoDocs() =

coroutineScope {

val deferredOne = async { fetchDoc(1) }

val deferredTwo = async { fetchDoc(2) }

deferredOne.await()

deferredTwo.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

}

虽然 fetchTwoDocs() 使用 async 启动新协程,但该函数使用 awaitAll() 等待启动的协程完成后才会返回结果。不过请注意,即使我们没有调用 awaitAll(),coroutineScope 构建器也会等到所有新协程都完成后才恢复名为 fetchTwoDocs 的协程。此外,coroutineScope 会捕获协程抛出的所有异常,并将其传送回调用方

  • 6、Deferred

async 函数的返回值是一个 Deferred 对象。Deferred 是一个接口类型,继承于 Job 接口,所以 Job 包含的属性和方法 Deferred 都有,其主要就是在 Job 的基础上扩展了 await()方法

七、CoroutineContext


CoroutineContext 使用以下元素集定义协程的行为:

Job:控制协程的生命周期

CoroutineDispatcher:将工作分派到适当的线程

CoroutineName:协程的名称,可用于调试

CoroutineExceptionHandler:处理未捕获的异常

  • 1、Job

协程中的 Job 是其上下文 CoroutineContext 中的一部分,可以通过 coroutineContext[Job] 表达式从上下文中获取到

以下两个 log 语句虽然是运行在不同的协程上,但是其指向的 Job 其实是同个对象

fun main() = runBlocking {

val job = launch {

log(“My job is ${coroutineContext[Job]}”)

}

log(“My job is $job”)

}

[main @coroutine#1] My job is “coroutine#2”:StandaloneCoroutine{Active}@75a1cd57

[main @coroutine#2] My job is “coroutine#2”:StandaloneCoroutine{Active}@75a1cd57

实际上 CoroutineScope 的 isActive 这个扩展属性只是 coroutineContext[Job]?.isActive == true 的一种简便写法

public val CoroutineScope.isActive: Boolean

get() = coroutineContext[Job]?.isActive ?: true

  • 2、CoroutineDispatcher

CoroutineContext 包含一个 CoroutineDispatcher(协程调度器)用于指定执行协程的目标载体,即运行于哪个线程。CoroutineDispatcher 可以将协程的执行操作限制在特定线程上,也可以将其分派到线程池中,或者让它无限制地运行。所有的协程构造器(如 launch 和 async)都接受一个可选参数,即 CoroutineContext ,该参数可用于显式指定要创建的协程和其它上下文元素所要使用的 CoroutineDispatcher

要在主线程之外运行代码,可以让 Kotlin 协程在 Default 或 IO 调度程序上执行工作。在 Kotlin 中,所有协程都必须在 CoroutineDispatcher 中运行,即使它们在主线程上运行也是如此。协程可以自行暂停,而 CoroutineDispatcher 负责将其恢复

Kotlin 协程库提供了四个 Dispatcher 用于指定在何处运行协程,大部分情况下我们只会接触以下三个:

Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用 suspend 函数、运行 Android 界面框架操作,以及更新 LiveData 对象

Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作

Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON

fun main() = runBlocking {

launch {

log(“main runBlocking”)

}

launch(Dispatchers.Default) {

log(“Default”)

}

launch(Dispatchers.IO) {

log(“IO”)

}

launch(newSingleThreadContext(“MyOwnThread”)) {

log(“newSingleThreadContext”)

}

}

[DefaultDispatcher-worker-1 @coroutine#3] Default

[DefaultDispatcher-worker-2 @coroutine#4] IO

[MyOwnThread @coroutine#5] newSingleThreadContext

[main @coroutine#2] main runBlocking

当 launch {…} 在不带参数的情况下使用时,它从外部的协程作用域继承上下文和调度器,即和 runBlocking 保持一致。而在 GlobalScope 中启动协程时默认使用的调度器是 Dispatchers.default,并使用共享的后台线程池,因此 launch(Dispatchers.default){…} 与 GlobalScope.launch{…} 是使用相同的调度器。newSingleThreadContext 用于为协程专门创建一个新的线程来运行,专用线程是一种成本非常昂贵的资源,在实际的应用程序中必须在不再需要时释放掉,或者存储在顶级变量中以便在整个应用程序中进行重用

**

- 3、withContext

**

对于以下代码,get方法内使用withContext(Dispatchers.IO) 创建了一个指定在 IO 线程池中运行的代码块,该区间内的任何代码都始终通过 IO 线程来执行。由于 withContext 方法本身就是一个挂起函数,因此 get 方法也必须定义为挂起函数

suspend fun fetchDocs() { // Dispatchers.Main

val result = get(“developer.android.com”) // Dispatchers.Main

show(result) // Dispatchers.Main

}

suspend fun get(url: String) = // Dispatchers.Main

withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)

/* perform network IO here */ // Dispatchers.IO (main-safety block)

} // Dispatchers.Main

}

借助协程,你可以细粒度地来调度线程。由于withContext()支持让你在不引入回调的情况下控制任何代码的执行线程池,因此你可以将其应用于非常小的函数,例如从数据库中读取数据或执行网络请求。一种不错的做法是使用 withContext() 来确保每个函数都是主线程安全的,这意味着,你可以从主线程调用每个函数。这样,调用方就从不需要考虑应该使用哪个线程来执行函数了

在前面的示例中,fetchDocs() 方法在主线程上执行;不过,它可以安全地调用 get方法,这样会在后台执行网络请求。由于协程支持 suspend 和 resume,因此 withContext 块完成后,主线程上的协程会立即根据 get 结果恢复

与基于回调的等效实现相比,withContext() 不会增加额外的开销。此外在某些情况下,还可以优化 withContext() 调用,使其超越基于回调的等效实现。例如,如果某个函数对一个网络进行十次调用,你可以使用外部 withContext() 让 Kotlin 只切换一次线程。这样,即使网络库多次使用 withContext(),它也会留在同一调度程序上,并避免切换线程。此外,Kotlin 还优化了 Dispatchers.Default 与 Dispatchers.IO 之间的切换,以尽可能避免线程切换

利用一个使用线程池的调度程序(例如 Dispatchers.IO 或 Dispatchers.Default)不能保证代码块一直在同一线程上从上到下执行。在某些情况下,Kotlin 协程在 suspend 和 resume 后可能会将执行工作移交给另一个线程。这意味着,对于整个 withContext() 块,线程局部变量可能并不指向同一个值

  • 4、CoroutineName

CoroutineName 用于为协程指定一个名字,方便调试和定位问题

fun main() = runBlocking(CoroutineName(“RunBlocking”)) {

log(“start”)

launch(CoroutineName(“MainCoroutine”)) {

launch(CoroutineName(“Coroutine#A”)) {

delay(400)

log(“launch A”)

}

launch(CoroutineName(“Coroutine#B”)) {

delay(300)

log(“launch B”)

}

}

}

[main @RunBlocking#1] start

[main @Coroutine#B#4] launch B

[main @Coroutine#A#3] launch A

  • 5、CoroutineExceptionHandler

在下文的异常处理会讲到

6、组合上下文元素

有时我们需要为协程上下文定义多个元素,那就可以用 + 运算符。例如,我们可以同时为协程指定 Dispatcher 和 CoroutineName

fun main() = runBlocking {

launch(Dispatchers.Default + CoroutineName(“test”)) {

log(“Hello World”)

}

}

[DefaultDispatcher-worker-1 @test#2] Hello World

此外,由于 CoroutineContext 是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext。比如,(Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)

八、取消协程


如果用户退出某个启动了协程的 Activity/Fragment 的话,那么大部分情况下就应该取消所有协程

job.cancel()就用于取消协程,job.join()用于阻塞等待协程运行结束。因为 cancel() 函数调用后会马上返回而不是等待协程结束后再返回,所以此时协程不一定就是已经停止运行了。如果需要确保协程结束运行后再执行后续代码,就需要调用 join() 方法来阻塞等待。也可以通过调用 Job 的扩展函数 cancelAndJoin() 来完成相同操作,它结合了 cancel 和 join两个操作

un main() = runBlocking {

fval job = launch {

repeat(1000) { i ->

log(“job: I’m sleeping $i …”)

delay(500L)

}

}

delay(1300L)

log(“main: I’m tired of waiting!”)

job.cancel()

job.join()

log(“main: Now I can quit.”)

}

[main] job: I’m sleeping 0 …

[main] job: I’m sleeping 1 …

[main] job: I’m sleeping 2 …

[main] main: I’m tired of waiting!

[main] main: Now I can quit.

  • 1、协程可能无法取消

并不是所有协程都可以响应取消操作,协程的取消操作是需要协作(cooperative)完成的,协程必须协作才能取消。协程库中的所有挂起函数都是可取消的,它们在运行时会检查协程是否被取消了,并在取消时抛出 CancellationException 从而结束整个任务。但如果协程正在执行计算任务并且未检查是否已处于取消状态的话,就无法取消协程

所以即使以下代码主动取消了协程,协程也只会在完成既定循环后才结束运行,因为协程没有在每次循环前先进行检查,导致任务不受取消操作的影响

fun main() = runBlocking {

val startTime = System.currentTimeMillis()

val job = launch(Dispatchers.Default) {

var nextPrintTime = startTime

var i = 0

while (i < 5) {

if (System.currentTimeMillis() >= nextPrintTime) {

log(“job: I’m sleeping ${i++} …”)

nextPrintTime += 500L

}

}

}

delay(1300L)

log(“main: I’m tired of waiting!”)

job.cancelAndJoin()

log(“main: Now I can quit.”)

}

[DefaultDispatcher-worker-1] job: I’m sleeping 0 …

[DefaultDispatcher-worker-1] job: I’m sleeping 1 …

[DefaultDispatcher-worker-1] job: I’m sleeping 2 …

[main] main: I’m tired of waiting!

[DefaultDispatcher-worker-1] job: I’m sleeping 3 …

[DefaultDispatcher-worker-1] job: I’m sleeping 4 …

[main] main: Now I can quit.

为了实现取消协程的目的,就需要为上述代码加上判断协程是否还处于可运行状态的逻辑,当不可运行时就主动退出协程。isActive 是 CoroutineScope 的扩展属性,就用于判断协程是否还处于可运行状态

fun main() = runBlocking {

val startTime = System.currentTimeMillis()

val job = launch(Dispatchers.Default) {

var nextPrintTime = startTime

var i = 0

while (i < 5) {

if (isActive) {

if (System.currentTimeMillis() >= nextPrintTime) {

log(“job: I’m sleeping ${i++} …”)

nextPrintTime += 500L

}

} else {

return@launch

}

}

}

delay(1300L)

log(“main: I’m tired of waiting!”)

job.cancelAndJoin()

log(“main: Now I can quit.”)

}

取消协程这个操作类似于在 Java 中调用Thread.interrupt()方法来向线程发起中断请求,这两个操作都不会强制停止协程和线程,外部只是相当于发起一个停止运行的请求,需要依靠协程和线程响应请求后主动停止运行。Kotlin 和 Java 之所以均没有提供一个可以直接强制停止协程或线程的方法,是因为这个操作可能会带来各种意想不到的情况。在停止协程和线程的时候,它们可能还持有着某些排他性资源(例如:锁,数据库链接),如果强制性地停止,它们持有的锁就会一直无法得到释放,导致其他协程和线程一直无法得到目标资源,最终可能导致线程死锁。所以Thread.stop()方法目前也是处于废弃状态,Java 官方并没有提供可靠的停止线程的方法

  • 2、用 finally 释放资源

可取消的挂起函数在取消时会抛出 CancellationException,可以依靠try {…} finally {…} 或者 Kotlin 的 use 函数在取消协程后释放持有的资源

fun main() = runBlocking {

val job = launch {

try {

repeat(1000) { i ->

log(“job: I’m sleeping $i …”)

delay(500L)

}

} catch (e: Throwable) {

log(e.message)

} finally {

log(“job: I’m running finally”)

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

面试宝典

面试必问知识点、BATJ历年历年面试真题+解析

学习经验总结

(一)调整好心态
心态是一个人能否成功的关键,如果不调整好自己的心态,是很难静下心来学习的,尤其是现在这么浮躁的社会,大部分的程序员的现状就是三点一线,感觉很累,一些大龄的程序员更多的会感到焦虑,而且随着年龄的增长,这种焦虑感会越来越强烈,那么唯一的解决办法就是调整好自己的心态,要做到自信、年轻、勤奋。这样的调整,一方面对自己学习有帮助,另一方面让自己应对面试更从容,更顺利。

(二)时间挤一挤,制定好计划
一旦下定决心要提升自己,那么再忙的情况下也要每天挤一挤时间,切记不可“两天打渔三天晒网”。另外,制定好学习计划也是很有必要的,有逻辑有条理的复习,先查漏补缺,然后再系统复习,这样才能够做到事半功倍,效果才会立竿见影。

(三)不断学习技术知识,更新自己的知识储备
对于一名程序员来说,技术知识方面是非常重要的,可以说是重中之重。**要面试大厂,自己的知识储备一定要非常丰富,若缺胳膊少腿,别说在实际工作当中,光是面试这一关就过不了。**对于技术方面,首先基础知识一定要扎实,包括自己方向的语言基础、计算机基础、算法以及编程等等。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

nally")

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-vPflEZfg-1711932973104)]

[外链图片转存中…(img-PmS2FjrW-1711932973104)]

[外链图片转存中…(img-1XeBa0VV-1711932973105)]

[外链图片转存中…(img-pAXGVXxs-1711932973105)]

[外链图片转存中…(img-tstwdrkx-1711932973105)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

面试宝典

面试必问知识点、BATJ历年历年面试真题+解析

[外链图片转存中…(img-A2AvWdCX-1711932973106)]

学习经验总结

(一)调整好心态
心态是一个人能否成功的关键,如果不调整好自己的心态,是很难静下心来学习的,尤其是现在这么浮躁的社会,大部分的程序员的现状就是三点一线,感觉很累,一些大龄的程序员更多的会感到焦虑,而且随着年龄的增长,这种焦虑感会越来越强烈,那么唯一的解决办法就是调整好自己的心态,要做到自信、年轻、勤奋。这样的调整,一方面对自己学习有帮助,另一方面让自己应对面试更从容,更顺利。

(二)时间挤一挤,制定好计划
一旦下定决心要提升自己,那么再忙的情况下也要每天挤一挤时间,切记不可“两天打渔三天晒网”。另外,制定好学习计划也是很有必要的,有逻辑有条理的复习,先查漏补缺,然后再系统复习,这样才能够做到事半功倍,效果才会立竿见影。

(三)不断学习技术知识,更新自己的知识储备
对于一名程序员来说,技术知识方面是非常重要的,可以说是重中之重。**要面试大厂,自己的知识储备一定要非常丰富,若缺胳膊少腿,别说在实际工作当中,光是面试这一关就过不了。**对于技术方面,首先基础知识一定要扎实,包括自己方向的语言基础、计算机基础、算法以及编程等等。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值