kotlin中的协程使用和原理

一些问题

程序什么时候需要切线程?

  • 工作比较耗时:放在后台
  • 工作特殊:需要放在指定线程(ui刷新、计算、io)

kotlin的协程是什么?

  • 线程框架

  • 可以用同步代码写出异步操作

suspend 关键字是什么?

  • 并不是切线程关键字
  • 用于标记和提醒

协程的优势是什么?

  • 耗时代码自动后台,提高软件性能

  • 线程执行完毕自动切回起始线程

协程的使用

基本使用

  1. launch()开启一段协程,一般需要指定Dispatchers.Main
  2. 把要在后台工作的函数,用suspend标记,需要在内部调用其他suspend函数真正切线程
  3. 按照代码书写顺序,线程自动切换
    GlobalScope.launch (Dispatchers.Main){
          
        }

上面代码会在主线程开启一个协程。

   private suspend fun ioCode1(){
       withContext(Dispatchers.IO){
                    Thread.sleep(1000)
           Log.d(Companion.TAG, "onCreate:ioCode1=${Thread.currentThread().name} ")
       }
    }

上面函数用suspend关键字修饰,并在函数内通过withContext(Dispatchers.IO)切换到IO线程执行。

    private  fun uiCode1(){
            Log.d(Companion.TAG, "onCreate:uiCode1=${Thread.currentThread().name} ")
    }

这是一个普通函数。

   GlobalScope.launch (Dispatchers.Main){
            ioCode1()
            uiCode1()
        }

把两个函数放在协程里执行:

01-21 14:17:49.960 10221-10256/com.example.coroutines D/MainActivity: onCreate:ioCode1=DefaultDispatcher-worker-2 
01-21 14:17:49.963 10221-10221/com.example.coroutines D/MainActivity: onCreate:uiCode1=main 

虽然ioCode1是在io线程,但是ioCode1uiCode1还是同步执行,如果ioCode1方法体和uiCode1一样,没有切换线程,那么编辑器会提示suspend无用,也就是说,需要切换线程才需要suspend关键字标记。

上面也回答了协程优势的问题,当你要执行耗时代码的时候,要用suspend标记,执行的时候自动切换到对应切换的线程,执行完毕,线程又切回了当前开启协程的线程。

与 Retrofit 结合使用

Retrofit turns your HTTP API into a Java interface.

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

The Retrofit class generates an implementation of the GitHubService interface.

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

Each Call from the created GitHubService can make a synchronous or asynchronous HTTP request to the remote webserver.

Call<List<Repo>> repos = service.listRepos("octocat");

上面是 Retrofit 官网的示例,我们也添加 Retrofit 依赖,完成上述代码:

添加依赖:

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'

声明 API :

interface GitHubService {
    @GET("users/{user}/repos")
    fun listRepos(@Path("user") user: String): Call<List<Repo>>
}


data class Repo(
    val name:String
)

请求:

        val retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        val api = retrofit.create(GitHubService::class.java)
        api.listRepos("JakeWharton")
            .enqueue(object : Callback<List<Repo>> {
                override fun onResponse(call: Call<List<Repo>>, response: Response<List<Repo>>) {
                    Log.d(TAG, "onResponse: ")
                }

                override fun onFailure(call: Call<List<Repo>>, t: Throwable) {
                    Log.d(TAG, "onFailure: ")
                }
            })

别忘了添加网络权限。

上面代码使用协程实现:

    @GET("users/{user}/repos")
    suspend fun listReposKt(@Path("user") user: String): List<Repo>

首先使用suspend标记协程函数,然后去掉返回值的回调。

     GlobalScope.launch(Dispatchers.Main) {
            Log.d(TAG, "launch: ")
            try {
                val repos = api.listReposKt("JakeWharton")
                Log.d(TAG, "listReposKt: ${repos[0].name}")
            } catch (e: Exception) {
                Log.d(TAG, "catch: ${e?.message}")

            }

        }

因为协程去掉了回调,所以需要 try catch 捕获异常。

使用 async 并发

有时候我们需要异步调用多个接口,然后在拿到所有结果后执行下一步,协程也可以轻松实现:

        GlobalScope.launch(Dispatchers.Main) {
                            val one = async{api.listReposKt("JakeWharton")}
                            val two = async{api.listReposKt("JakeWharton")}
                        Log.d(TAG, "launch:${ one.await()[0].name}== ${ two.await()[0].name}")

        }

在概念上,async 就类似于 launch。它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于 launch 返回一个 Job 并且不附带任何结果值,而 async 返回一个 Deferred —— 一个轻量级的非阻塞 future, 这代表了一个将会在稍后提供结果的 promise。你可以使用 .await() 在一个延期的值上得到它的最终结果, 但是 Deferred 也是一个 Job,所以如果需要的话,你可以取消它。

协程泄漏

和线程一样,当退出activity的时候,协程的后台线程还未执行完毕,那么就会发生内存泄漏。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job 

launch的返回值是Job,可以使用job.cancel()取消协程。

    job = GlobalScope.launch(Dispatchers.Main) {
            ioCode1()
            uiCode1()
        }
   override fun onDestroy() {
        job?.cancel()
        super.onDestroy()
    }

CoroutineScope

如果一个页面有多个协程,需要取消多次,并不优雅,协程需要在协程作用域里执行,上面的GlobalScope就是一个全局作用域,通常,我们需要声明一个 CoroutineScope ,onDestroy的时候取消这个作用域,就可以取消所有运行其中的协程。

private val scope: CoroutineScope= MainScope()

启动协程:

     scope.launch() {
            ioCode1()
            uiCode1()
        }

取消:

    override fun onDestroy() {
//        job?.cancel()
        scope.cancel()
        super.onDestroy()
    }

这样就可以启动多个协程,而取消一次。

将 Kotlin 协程与架构组件一起使用

上面例子通过 Kotlin 协程,我们可以定义 CoroutineScope,管理运行协程的作用域和取消。

Android 的架构组件针对应用中的逻辑范围以及与 LiveData 的互操作层为协程提供了非常棒的支持。

生命周期感知型协程范围

在 lifecycle 组件中使用协程

在 lifecycle 组件中,比如一个activity,我们可以直接使用协程,并不需要自己取消:

    lifecycleScope.launch {
                ioCode1()
                uiCode1()
            }

lifecycleScope 并不需要我们声明,扩展库提供的支持:

    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'

在 ViewModel 组件中使用协程

在 ViewModel 组件中,我们一样可以方便的直接使用协程:

    viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }

viewModelScope也不需要我们声明,扩展库提供的支持:

    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

将协程与 LiveData 一起使用

使用 LiveData 时,有时候可能需要异步计算值。可以使用 liveData 构建器函数调用 suspend 函数,并将结果作为 LiveData 对象传送。

在以下示例中,loadUser() 是在其他位置声明的协程函数。使用 liveData 构建器函数异步调用 loadUser(),然后使用 emit() 发出结果:

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

liveData 构建块用作协程和 LiveData 之间的结构化并发基元。当 LiveData 变为活动状态时,代码块开始执行;当 LiveData 变为非活动状态时,代码块会在可配置的超时过后自动取消。如果代码块在完成前取消,则会在 LiveData 再次变为活动状态后重启;如果在上次运行中成功完成,则不会重启。注意,代码块只有在自动取消的情况下才会重启。如果代码块由于任何其他原因(例如,抛出 CancellationException)而取消,则不会重启。

协程的本质

协程怎么切换线程?

协程本质是对线程的上层封装,是线程切换,我们走读下源码。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): 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
}

调到AbstractCoroutine

   public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
        initParentJob()
        start(block, receiver, this)
    }

执行了start(),这是一个operator方法,CoroutineStart

    @InternalCoroutinesApi
    public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit =
        when (this) {
            DEFAULT -> block.startCoroutineCancellable(completion)
            ATOMIC -> block.startCoroutine(completion)
            UNDISPATCHED -> block.startCoroutineUndispatched(completion)
            LAZY -> Unit // will start lazily
        }

Cancellable

@InternalCoroutinesApi
public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) {
    createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
}

然后到DispatchedContinuation:

@InternalCoroutinesApi
public fun <T> Continuation<T>.resumeCancellableWith(result: Result<T>): Unit = when (this) {
    is DispatchedContinuation -> resumeCancellableWith(result)
    else -> resumeWith(result)
}
  @Suppress("NOTHING_TO_INLINE")
    inline fun resumeCancellableWith(result: Result<T>) {
        val state = result.toState()
        if (dispatcher.isDispatchNeeded(context)) {
            _state = state
            resumeMode = MODE_CANCELLABLE
            dispatcher.dispatch(context, this)
        } else {
            executeUnconfined(state, MODE_CANCELLABLE) {
                if (!resumeCancelled()) {
                    resumeUndispatchedWith(result)
                }
            }
        }
    }

dispatcher.dispatch(context, this),这里应该都不陌生, Okhttp 和 Rxjava 里都出现过,调度线程用的,我们找到CoroutineDispatcher的实现类HandlerDispatcher,查看:

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        handler.post(block)
    }

关键代码就在这里,利用handler把任务post到对应的线程。

suspend函数执行的本质

通过编译后的字节码我们看下ioCode1()

    private suspend fun ioCode1() {
        withContext(Dispatchers.IO) {
            Thread.sleep(1000)
            Log.d(TAG, "onCreate:ioCode1=${Thread.currentThread().name} ")
        }
    }
final Object ioCode1(Continuation $completion) {
      Object var10000 = BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
         int label;

         @Nullable
         public final Object invokeSuspend(@NotNull Object var1) {
            Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch(this.label) {
            case 0:
               ResultKt.throwOnFailure(var1);
               Thread.sleep(1000L);
               StringBuilder var10001 = (new StringBuilder()).append("onCreate:ioCode1=");
               Thread var10002 = Thread.currentThread();
               Intrinsics.checkNotNullExpressionValue(var10002, "Thread.currentThread()");
               return Boxing.boxInt(Log.d("MainActivity", var10001.append(var10002.getName()).append(' ').toString()));
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }
         }

         @NotNull
         public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
            Intrinsics.checkNotNullParameter(completion, "completion");
            Function2 var3 = new <anonymous constructor>(completion);
            return var3;
         }

         public final Object invoke(Object var1, Object var2) {
            return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
         }
      }), $completion);
      return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
   }

在协程里调用的时候,每个都对应一个label,进入 Switch 判断执行。

协程挂起为什么不卡线程?

        job = GlobalScope.launch(Dispatchers.Main) {
            ioCode1()
            uiCode1()
        }

上面的协程是在主线程启动的,并且uiCode1是没有切到子线程的,uiCode1ioCode1之后执行,为什么主线程不会卡住?

其实没什么神奇,也是 Hanler 机制,执行完后会调用Handler().post { }把任务 Post 到主线程。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容

  • 在今年的三月份,我因为需要为项目搭建一个新的网络请求框架开始接触 Kotlin 协程。那时我司项目中同时存在着两种...
    业志陈阅读 1,025评论 0 5
  • 在今年的三月份,我因为需要为项目搭建一个新的网络请求框架开始接触 Kotlin 协程。那时我司项目中同时存在着两种...
    Android开发指南阅读 813评论 0 2
  • Why 简化异步代码的编写。 执行严格主线程安全确保你的代码永远不会意外阻塞主线程,并增强了代码的可读性。 提升代...
    熹哥阅读 549评论 0 3
  • 本文主要介绍协程的用法, 以及使用协程能带来什么好处. 另外, 也会粗略提一下协程的大致原理.本文的意义可能仅仅是...
    登高而望远阅读 35,177评论 18 140
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,494评论 16 22