Android协程

协程


在介绍协程之前我们先了解几个概念

1.讲协程之前的一些相关概念

1.并发与并行

在操作系统中我们曾经学到过并发并行

并发是同一个时刻只有一条指令在执行,其他指令没有再执行,但是由于CPU的时间片特别短,导致多个指令来回切换的时间间隔特别短,就好像是同一时间多条指令在执行单核CPU与多核CPU都可以进行并发

并行就不一样了,在同一个时刻,多条指令在执行,这个不用想,只能在多核CPU中进行

2.同步与异步

同步操作很常见,我们一般运行一个程序,只能等该程序执行完毕后才能执行其他的程序。

而异步操作,是如果我们程序遇到一个循环10次的函数,我们的程序可能不会直接循环10次,而是跳过这个程序执行其他的程序。

在okhttp3中不就有同步的和异步两种请求方式

同步如下

        OkHttpClient okHttpClient = new OkHttpClient();//1.定义一个client
        Request request = new Request.Builder().url("http://www.baidu.com").build();//2.定义一个request
        Call call = okHttpClient.newCall(request);//3.使用client去请求
        try {
    
    
            String result = call.execute().body().string();//4.获得返回结果
            System.out.println(result);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }

异步如下

        OkHttpClient okHttpClient = new OkHttpClient();//1.定义一个client
        Request request = new Request.Builder().url("http://www.baidu.com").build();//2.定义一个request
        Call call = okHttpClient.newCall(request);//3.使用client去请求
        call.enqueue(new Callback() {
    
    //4.回调方法
            @Override
            public void onFailure(Call call, IOException e) {
    
    
 
            }
 
            @Override
            public void onResponse(Call call, Response response) throws IOException {
    
    
                String result = response.body().string();//5.获得网络数据
                System.out.println(result);
            }
        });

我们可以看到同步获得call之后,就通过call来获得Response然后再将Response转化为String类型

但是我们异步的时候,获得call,就调用call的enqueue方法,然后在OnReponse中进行处理

对了这个enqueue中在handler的post方法中其实也用到过,我们post之后会调用它的sendMessageDelay然后调用sendMessageAtTime然后调用enqueueMessage,所以handler的post方法就是一个异步方法

3.阻塞

3.1Looper的阻塞

3.1.1 loop的源码
public static void loop() {
    
    
    final Looper me = myLooper();
    if (me == null) {
    
    
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    if (me.mInLoop) {
    
    
        Slog.w(TAG, "Loop again would have the queued messages be executed"
                + " before this one completed.");
    }

    me.mInLoop = true;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    // Allow overriding a threshold with a system prop. e.g.
    // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
    final int thresholdOverride =
            SystemProperties.getInt("log.looper."
                    + Process.myUid() + "."
                    + Thread.currentThread().getName()
                    + ".slow", 0);

    me.mSlowDeliveryDetected = false;

    for (;;) {
    
    
        if (!loopOnce(me, ident, thresholdOverride)) {
    
    
            return;
        }
    }
}

我们可以看到在最后它有一个for死循环,只有当

!loopOnce(me, ident, thresholdOverride)

的时候才会退出

我们再看看loopOnce的源码

3.1.2loopOnce源码
private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    
    
    Message msg = me.mQueue.next(); // might block
    if (msg == null) {
    
    
        // No message indicates that the message queue is quitting.
        return false;
    }

    // This must be in a local variable, in case a UI event sets the logger
    final Printer logging = me.mLogging;
    if (logging != null) {
    
    
        logging.println(">>>>> Dispatching to " + msg.target + " "
                + msg.callback + ": " + msg.what);
    }
    // Make sure the observer won't change while processing a transaction.
    final Observer observer = sObserver;

    final long traceTag = me.mTraceTag;
    long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
    long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
    if (thresholdOverride > 0) {
    
    
        slowDispatchThresholdMs = thresholdOverride;
        slowDeliveryThresholdMs = thresholdOverride;
    }
    final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
    final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

    final boolean needStartTime = logSlowDelivery || logSlowDispatch;
    final boolean needEndTime = logSlowDispatch;

    if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
    
    
        Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
    }

    final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
    final long dispatchEnd;
    Object token = null;
    if (observer != null) {
    
    
        token = observer.messageDispatchStarting();
    }
    long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
    try {
    
    
        msg.target.dispatchMessage(msg);
        if (observer != null) {
    
    
            observer.messageDispatched(token, msg);
        }
        dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
    } catch (Exception exception) {
    
    
        if (observer != null) {
    
    
            observer.dispatchingThrewException(token, msg, exception);
        }
        throw exception;
    } finally {
    
    
        ThreadLocalWorkSource.restore(origWorkSource);
        if (traceTag != 0) {
    
    
            Trace.traceEnd(traceTag);
        }
    }
    if (logSlowDelivery) {
    
    
        if (me.mSlowDeliveryDetected) {
    
    
            if ((dispatchStart - msg.when) <= 10) {
    
    
                Slog.w(TAG, "Drained");
                me.mSlowDeliveryDetected = false;
            }
        } else {
    
    
            if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                    msg)) {
    
    
                // Once we write a slow delivery log, suppress until the queue drains.
                me.mSlowDeliveryDetected = true;
            }
        }
    }
    if (logSlowDispatch) {
    
    
        showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
    }

    if (logging != null) {
    
    
        logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
    }

    // Make sure that during the course of dispatching the
    // identity of the thread wasn't corrupted.
    final long newIdent = Binder.clearCallingIdentity();
    if (ident != newIdent) {
    
    
        Log.wtf(TAG, "Thread identity changed from 0x"
                + Long.toHexString(ident) + " to 0x"
                + Long.toHexString(newIdent) + " while dispatching to "
                + msg.target.getClass().getName() + " "
                + msg.callback + " what=" + msg.what);
    }

    msg.recycleUnchecked();

    return true;
}

我们不用看这么多代码,我们只需要知道什么时候返回true,什么时候返回false

我们就会发现当

  1. 当成功从消息队列中获取到一条消息时,会执行消息的分发和处理,并在最后通过 return true; 表示成功处理了一条消息,准备继续下一次循环。
  2. 当从消息队列中获取到的消息为 null,即消息队列正在退出时,会通过 return false; 表示没有更多的消息需要处理,退出循环。

这时候我们大概就明白,当消息队列不断有message传过来的时候,looper.loop会一直进行下去

当没有message传过来的时候looper.loop的for循环会退出

3.1.3注意

我当时把阻塞和死循环化成等号了,所以一直理解的就是在有消息传过来的时候,就会阻塞Looper,没有消息传过来的时候就不会阻塞Looper。但是阻塞的理解通俗的来说就是这个线程啥都不干,光等着,该线程处于休眠状态了或则长时间执行不完,卡在那里了

所以根据这个理解的话,当没有Message传过来的时候,Looper处于阻塞状态。当有Message传来的时候,Looper不处于阻塞状态

现在我们来说说:Looper处于死循环是否会导致ANR?

3.2Looper处于死循环是否会导致ANR

3.2.1ANR是什么

在 Android 中,ANR(Application Not Responding)是指应用程序未响应用户交互事件(如触摸屏幕、按键等)的情况。当应用程序长时间未响应事件时,系统会显示一个 ANR 对话框,通知用户该应用程序已停止响应,然后给用户选择 “Force Close”(强制关闭)或 “Wait”(等待)的选项。

简单的说当一个任务占用太多资源就容易造成ANR

3.2.2Looper的死循环是否会导致ANR

面试中常考点是:Looper的死循环,会不会导致ANR?

答案是否定的,looper循环不会导致ANR ,只可能会阻塞主线程(当没有消息传过来) 它有一个消息队列 当消息队列里有消息的时候就会循环去取 当没有消息的时候就会调用epoll.await然后阻塞主线程 当重新有消息的时候会唤醒然后执行 他的阻塞和ANR不是一个概念的 ANR是指应用无响应 假如说我一个点击事件它长时间无响应就会导致ANR 但是阻塞的话是因为没有消息 而不是无响应。

我最开始想不通的一点是假如我looper处理一个特别大的任务,然后为什么不能反驳Looper的死循环是否导致ANR?
后来想了一下处理一个特别大的事件这是它的dispatchMessage处理的和我的死循环没有关系啊,我的死循环只负责把找消息又不负责处理消息。

这是一个学姐给我讲的,我原本以为这样就代表阻塞主线程之后也不会导致ANR

记得我们刚才说的两种导致阻塞的情况

1.没有消息传来等待消息传来

2.处理耗时量大的任务

我们现在是第一种情况,在没有收到消息时,它实际上是在等待新的消息到达。(相当于一个休眠状态)它并不会占用过多的CPU资源或者阻塞其他操作。在这种情况下,主线程依然可以处理用户界面事件和其他操作。所以,当Looper在收不到消息时阻塞主线程,它不会造成ANR。但是,如果Handler在处理消息时执行了耗时操作,这有可能导致ANR。

但这道题其实主要想问的是Looper的epoll与ANR的概念。我想的有点多,有点钻牛角尖了

3.2.3总结

Looper 的死循环中,它会不断地从消息队列中取出消息,并将消息分发给对应的 Handler 处理。当消息队列为空时,Looper 会一直循环等待新的消息到达。

在等待消息期间,Looper 的死循环会阻塞主线程,因为它会一直占用主线程的执行时间片。这意味着主线程无法继续执行其他任务或响应用户的输入事件或系统事件。

只有当主线程长时间占用了 CPU 或其他系统资源,并且长时间无法响应用户输入事件或完成关键操作时,才会触发 ANR 错误。

耗时操作本身并不会导致主线程卡死,导致主线程卡死的真正原因是耗时操作之后的操作, 没有在规定的时间内被分发

4.挂起

挂起就是保存当前状态,等待恢复执行,在 Android 中的体现,挂起就是不影响主线程的工作,更贴切的说法可以理解为切换到了一个指定的线程。

4.1阻塞和挂起的区别

它们两个主要的不同在于释放CPU

挂起是协作式的,即在挂起时会主动将线程让出,使得线程可以执行其他任务。同时,在协程挂起时,协程的堆栈和上下文会被保存下来,等待挂起结束后恢复执行。这种方式可以避免线程的切换开销,提高程序的性能和响应速度。

阻塞是强制式的,即阻塞时会将线程一直占用,直到阻塞结束后才会继续执行。在阻塞时,线程无法执行其他任务,因此会浪费 CPU 资源。同时,阻塞也会增加线程切换的开销,降低程序的性能和响应速度。

5.多任务

多任务就是操作系统能够同时处理多个任务

多任务中又分出了2个,一个是抢占式多任务,一个是协作式多任务

  1. 抢占式多任务就是操作系统自己来指定每个任务的CPU的占有时间,超过这个时间后。当前的这个任务就无法占有CPU了,交给下一个任务
  2. 但是协作式多任务是除非你那个任务自己放弃对CPU的占有,否则别的任务无法用那个CPU

了解了这几个概念之后我们再来说协程

2.协程

1.协程的作用

我们之前学习的线程是非常重量级的,他需要依靠操作系统的调度才能实现不同线程的切换。但是协程却可以仅在编程语言的层面就能实现不同协程之间的切换

协程允许我们在单线程模式下模拟多线程编程的效果

2.协程的基本用法

 implementation'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
    implementation'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'

先导入包

2.1GlobalScope.launch

然后创建一个包,注意它的包后缀是.kt

fun main(){
    
    
GlobalScope.launch {
    
    
    println("nihao")
    Log.d("TAG","NIHAO")
}
}

然后在MainActivity中调用main()

我们会发现nihaoNIHAO都打印不出来

这是因为:Global.launch每次创建的都是一个顶层协程,这种协程当应用程序结束的时候会跟着一起结束。刚才日志无法打印就是因为代码块的代码还没来的及执行,应用程序就结束了

我们只需要让程序延迟一段时间结束就可以了

fun main(){
    
    
GlobalScope.launch {
    
    
    println("nihao")
    Log.d("TAG","NIHAO")
}
    Thread.sleep(1000)
}

我们让这个main所在的线程阻塞1000ms,让主线程处于休眠状态,这时候我们会发现打印出来了

这种存在一个问题,那么就是如果代码在1000ms内没办法结束,那么就会被强制中断

比如现在

fun main(){
    
    
GlobalScope.launch {
    
    
    println("nihao")
    delay(1500)
    Log.d("TAG","NIHAO")
}
    Thread.sleep(1000)
}

我们现在的逻辑是让main所在的线程休眠1000ms然后执行GlobalScope.launch里面的代码,我们设置了在println()之后延迟1500ms才会执行Log.d()里面的东西

这时候我们会发现只会打印出println()里面的东西,但是打不出**Log.d()**里面的东西

2.2runBlocking

刚才我们用GlobalScope.launch的时候,因为它是一个顶层协程,会随程序的结束而结束

那么有没有一种协程,等它协程中的代码都执行完后,才会结束程序呢?

这就是runBlocking

它的基本格式就是

fun main1(){
    
    
    runBlocking {
    
    
        println("runBlocking")
    }
}

可以成功打印

runBlocking

runBlocking可以保证协程作用域内所有的代码和子协程中没有全部执行完之前,当前线程一直被阻塞

一般runBlocking在正式开发中性能可能出现问题,所以我们一般只在测试环境下用

2.3创建多个协程

在刚才的runBlocking里面加上launch就行了

如下图所示:

fun main2(){
    
    
    runBlocking{
    
    
        launch {
    
    
            println("launch_0")
        }
        launch {
    
    
            println("launch_1")
        }
    }
}

可以打印出

launch_0
launch_1

这个还看不出来啥,我们在后面加一点代码

fun main2(){
    
    
    runBlocking{
    
    
        launch {
    
    
            println("launch_0")
            delay(1000)
            println("launch_0 finished")
        }
        launch {
    
    
            println("launch_1")
            delay(1000)
            println("launch_1 finished")
        }
    }
}

我们在两个launch打印出来后加上delay(1000),然后再打印

我们会发现

launch_0
launch_1
launch_0 finished
launch_1 finished

它并不是等launch_0打印完后打印launch_0 finshed而是接着打印launch_1

很像多线程中的并发操作

还有一点要注意:这里的launchGlobalScope.launch不同,前者必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程(子协程是一种当外层作用域的协程结束了,该作用域下所有的子协程也会一同结束)

但是GlobalScope.launch创建的永远是顶层协程,这种协程在应用程序结束的时候会跟着一起结束

2.4suspend关键字

我们刚才曾经说过,launch必须在协程的作用域下才能被使用,如果我们把所有的launch都写在runBlocking里面,那么如果launch特别多的话,那么runBlocking所占的行数就更多了

那么可不可以最前面写了runBlocking后面调用方法

比如

fun main3(){
    
    
    runBlocking {
    
    
        main4()
        main5()
    }
}
fun main4(){
    
    
    println("没有协程作用域_0")
}
fun main5(){
    
    
    println("没有协程作用域_1")
}

我们可以成功打印出

没有协程作用域_0
没有协程作用域_1

但是呢,你会发现没办法调用类似delay()这种挂起函数,因为没有协程的作用域

我们可以通过suspend关键字解决这个问题

fun main3(){
    
    
    runBlocking {
    
    
        main4()
        main5()
    }
}
suspend fun main4(){
    
    
    delay(1000)
    println("没有协程作用域_0")
}
fun main5(){
    
    
    println("没有协程作用域_1")
}

这样就成功实现了**delay()**这类挂起函数

这样虽然可以实现delay这类挂起函数,但是我们不能调用launch函数,它只能在协程的作用域中才能调用

2.5coroutineScope

coroutineScope函数是一个挂起函数,可以在任何其他挂起函数中调用。它的特点是会继承外部的协程的作用域并创建一个子协程

fun main6(){
    
    
    runBlocking {
    
    
        main7()
        main8()
    }
}
suspend fun main7() = coroutineScope{
    
    
   launch {
    
    
       println("coroutineScope_0")
       delay(1000)
   }
}
suspend fun main8() = coroutineScope{
    
    
    launch {
    
    
        println("coroutineScope_1")
        delay(1000)
    }
}

这样就可以了

它可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起

coroutineScoperunBlocking不太一样

前者只会挂起当前协程,不影响其他协程,也不影响任何线程

但是后者会挂起外部线程,如果在主线程中使用 runBlocking 函数来启动一个长时间运行的协程,就会导致主线程被阻塞,从而导致界面卡死的问题。

2.5.1注意

我当时就是这块没有理解,然后又去看了一眼Looper的死循环是否会导致ANR,我当时候的理解是Looper当没有收到消息,epoll会发出一个await指令,导致主线程被阻塞。但是阻塞不会导致ANR

然后我又看这里:runBlocking会挂起外部线程,如果在主线程中使用 runBlocking 函数来启动一个长时间运行的协程,就会导致主线程被阻塞,从而导致界面卡死的问题。

然后我就昏了,不是阻塞不会导致ANR嘛,这里为什么又导致了ANR,事实证明我当时的理解是错的,具体解析在上面写了。

这里因为处理耗时操作而导致主线程阻塞,事件太长的话就会导致ANR。

3.协程作用域

在讲构造器作用域之前,我们先来了解了解什么叫协程作用域

GlobalScope.launch中,它创建的是一个顶层协程,它里面的代码会随着应用程序的结束而结束

runBlocking中我们说:runBlocking可以保证协程作用域里面的所有得到代码和子协程中没有全部执行完之前当前线程一直被阻塞

然后launch,它必须依赖协程作用域才能执行

coroutineScoperunBlocking的作用效果差不多,它可以在协程作用域或者挂起函数中调用。只不过coroutineScope只会挂起外部协程不会影响其他,而runBlocking会导致直接把线程阻塞。

那么什么叫协程作用域?

给出的解释是:

协程作用域(Coroutine Scope)是指协程的生命周期和作用域,用于管理协程的执行范围和生命周期,以确保协程的安全和正确性。

在协程中,通常使用 CoroutineScope 接口来定义协程作用域。CoroutineScope 接口提供了一组协程构建器(Coroutine Builder)和协程上下文(Coroutine Context),用于创建和管理协程的生命周期和作用域。协程作用域可以是全局的,也可以是局部的,它们的生命周期可以是短暂的,也可以是长久的。

在协程作用域中,可以创建一个或多个协程,并将它们组织成一个层次结构。每个协程都有自己的作用域和生命周期,它们可以访问和共享作用域内的资源和状态。协程作用域还可以定义协程的取消策略和异常处理方式,以确保协程的安全和正确性。

协程必须在协程作用域中才能启动,协程作用域中定义了一些父子协程的规则,Kotlin 协程通过协程作用域来管控域中的所有协程

协程作用域间可并列或包含,组成一个树状结构,这就是 Kotlin 协程中的结构化并发。

作用域细分有下述三种:

顶级作用域:没有父协程的协程所在的作用域(GlobalScope.launch)

协同作用域:协程中启动新协程(即子协程),此时子协程所在的作用域默认为协同作用域,子协程抛出的未捕获异常都将传递给父协程处理,父协程同时也会被取消;(coroutineScope)

主从作用域:与协同作用域父子关系一致,区别在于子协程出现未捕获异常时不会向上传递给父协程

父子协程间的规则

父协程如果取消或结束了,那么它下面的所有子协程均被取消或结束

父协程需等待子协程执行完毕后才会最终进入完成状态,而不管父协程本身的代码块是否已执行完

子协程会继承父协程上下文中的元素,如果自身有相同 Key 的成员,则覆盖对应 Key,覆盖效果仅在自身范围内有效

4.作用域构造器

我们在使用顶层协程进行网络加载操作的时候,可能会遇到这样的问题。我们要关闭一个应用,但是该应用中启动了一个协程来执行网络请求,而这个请求在应用关闭之前还没有完成,那么这个请求将会继续执行,这可能会浪费系统资源,例如网络带宽和 CPU 时间,而且还可能会导致请求结果无法正确处理或者造成其它一些问题。

所以我们需要在退出应用之前,先把协程关了。

协程要怎么取消呢?不管是GlobalScope.launch函数还是launch函数,它们都会返回一个Job对象,只需要调用Job对象的**cancle()**方法就可以取消协程了

val job = GlobalScope.launch {
    
     
    //  处理具体逻辑
}
job.cancel()

如果我们每次创建的都是顶层协程,那么当Activity关闭时,就需要逐个调用所有已创建协程的cancel()方法,这种情况代码就很难维护了。因此,GlobalScope.launch这种协程作用域构建器,在实际项目中也是不太常用的。

4.1实际项目常用的写法

val job = Job()
val scope = CoroutineScope(job)
scope.launch {
    
     
    //  处理具体逻辑
}
job.cancel()

先创建一个Job对象,传入CoroutineScope()函数,CoroutineScope()函数会返回一个CoroutineScope对象,有了这个对象,就可以调用这个CoroutineScope对象的launch函数来创建一个协程了。

所以调用CoroutineScope的launch函数所创建的协程,都会被关联在Job对象的作用域下面,这样只需调用一次job.cancel()方法,就可以将同一作用域内的所有协程全部取消,这就大大降低了协程管理的成本。

而不用像GlobalScope那样,用多个GlobalScope.launch创建job对象,得每个job都**cancel()**一下才能把所有的协程关闭

但是像CoroutineScope,因为我们说过

无论是GlobalScope.launch函数还是launch函数,它们都会返回一个Job对象,所以我们的首先的目的就是创建一个Job对象

然后我们将job传进CoroutineScope()获得一个CoroutineScope对象,然后将这个对象.launch。

最后只用将job.cancel()就可以全部取消了

4.1.1为什么GlobalScope不可以一次性取消所有的协程,而CoroutineScope可以

简单来说就是GlobalScope一次性只能创建一个launch,但是CoroutineScope一次性可以创建多个launch

4.2async

一般情况下我们就是用CoroutineScope.launch但是获得的是Job对象

想要创建一个协程并获取它的执行结果,就要用到async函数

这句话我最开始没有特别理解它的意思,我以为是在launch里面没办法执行运算操作,但是我试了一下是可以的呀

最后才明白

比如

   var job = Job()
        var c = String()
        var coroutineScope = CoroutineScope(job)
        coroutineScope.launch {
            c = "nihao"
        }
        println(job)

打印出来是:

JobImpl{
    
    Active}@660a746

但是如果你用async的话

fun main00(){
    runBlocking {
        val result = async {
             5+5
        }.await()
        println(result)
    }
}

然后在Main中调用main00()

然后会发现它能打印出

10

在这里我们就可以明白了一些东西

4.2.1async的一些性质

1.async函数必须在协程作用域当中才能调用,它会创建一个新的子协程,并且返回一个Deferred对象

2.在调用完async函数之后代码块中的代码就会立即执行。当调用await()方法之后,如果代码块的代码还没有执行完,那么await()方法会将当前协程阻塞住,指导获得async函数的执行结果

即如果async函数里面的代码块还没有执行完的话,因为有await()方法,所以async所在的协程作用域不会在async执行完前结束,而是先将runBlocking这个协程阻塞,等async 函数执行完成之后再结束

我最开始在这上面栽了两个跟头

第一个是没加await,无论你加不加await()的话,它会先直接产生一个Deferred对象,然后5+5那个操作它在后台里面进行,并不会直接算。如果没加**await()**的话,5+5可能被丢弃而不会被输出出来

然后因为async很快的就产生了一个Deferred对象,我以为它一定会最先执行所以最先打印的也是10

runBlocking {
    
    
    launch {
    
    
        println("哈哈哈")
    }
    val result = async {
    
    
         5+5
    }.await()
    println(result)
}

但是,这里面先打印的是

哈哈哈

然后才打印

10

但我的前面的理解是对的,他确实先产生一个Deferred对象,但是这样之后不会直接打印10,而还是先打印哈哈哈

只是当它执行到await的时候才会阻塞他所在的协程,而不是直接就执行await()

第二个是我没加runBlocking,这就导致我的async是在一个没有协程作用域的环境下运行,它会爆红。

4.2.2async是串行的还是并行的

fun main333()
{
    
    
    val start = System.currentTimeMillis()
    runBlocking {
    
    
        val result1 = async {
    
    
            delay(1000)
            5 + 5
        }.await()

        val result2 = async {
    
    
            delay(1000)
            4 + 6
        }.await()
        println("result is ${
      
      result1+result2}")
        val end = System.currentTimeMillis()
        println("时间 ${
      
      end-start}")
    }
    }

最后打印出来的结果是

result is 20
时间 2004

如果是2004就可以验证一件事情,async串行不是并行的,如果是并行的话,它的时间肯定少于2000ms

在第一个delay()的时候,它会直接执行result2里面的东西,而不是一直在等待

但是呢,如果我们使用这个代码

fun main44(){
    
    
        runBlocking {
    
    
            val start = System.currentTimeMillis()
            val deferred1 = async {
    
    
                delay(1000)
                5 + 5
            }

            val deferred2 = async {
    
    
                delay(1000)
                4 + 6
            }
            println("result is ${
      
      deferred1.await() + deferred2.await()}")
            val end = System.currentTimeMillis()
            println("cost: ${
      
      end - start} ms.")
        }
}

我们跑出来的结果

result is 20
cost: 1002 ms.

这就是并行的了

在遇到delay(1000)的时候,它会执行deffered2里面的代码,然后一块delay(1000)

我们来看看两个有什么差别,主要就是await的地方,第一个串行,我们是在每个async后面加一个await()。但是在第二个并行,我们是在最后打印的时候才加上await()

之前我们没有加await()launch的时候就是并行的,所以总结

4.2.2.1总结

如果你在每次async后面加上**await()**就会是串行

如果是最后加上**await()**就是并行

因为await()会阻塞async所在的协程直到async的所创建的子协程执行完后才会继续

4.2.3withContext简化async

我本来以为这块就讲完了

没想到还有一个withContext简化async

fun main() {
    
    
    runBlocking {
    
    
        val result = withContext(Dispatchers.Default) {
    
    
            5 + 5
        }
        println(result)
    }
}

调用withContext()函数之后,会立即执行代码块中的代码同时将外部协程挂起当代码块中的代码全部执行完之后,会将最后一行的执行结果作为withContext()函数的返回值返回

我们可以发现withContext的用法和使用async差不多

我们先把async实现这个的代码写出来

fun main() {
    
    
    runBlocking {
    
    
        val result = async {
    
    
            5 + 5
        }.await()
        println(result)
    }
}

我们来比较这两个的相同点

async withContext
在协程作用域下才能执行 在协程作用域下才能执行
是挂起函数 是挂起函数
会先直接产生一个Deferred对象 不会产生Deferred对象
在有await存在的情况下,运行async代码块的时候会把它外面所在的协程阻塞 运行withContext代码块的时候会将外部协程挂起,当代码块中的所有代码执行完之后,将最后一行的结果作为**withContext()**函数的结果返回

运行这段代码

val result1 = withContext(Dispatchers.Default){
    
    
    5
    10
}
println("${
      
      result1}")

最后打印出来的结果是

10

这个会将最后一行的结果返回和前面讲的**高阶函数with(),run()**一样

上面说了那么多地方,

withContext与async最大的不同是

Dispatchers.Default

这个,withContext()函数会强制要求指定一个线程参数

协程虽然是很狠很轻量级别的线程,且多个协程可以运行在一个线程里面,但是这并代表着我们就不开线程了

比如网络请求,我们都知道它是一个耗时很大的操作,在java中我们处理网络请求通常都是另开一个线程,让它在新的线程里面执行,因为我们知道,在主线程里面执行网络请求,容易因为它的耗时大导致ANR

我们来看看Dispatchers里面给了我们多少种

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yYbBS5dd-1686663619434)(../../assets/QQ图片20230613201644.png)]

分别有Default,Main,IO,Unconfined

Dispatchers.Default 表示会使用一种默认低并发的线程策略,当你要执行的代码属于 计算密集型 任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。 Dispatchers.Default 调度器使用的线程池是共享的后台线程池,它包含的线程数通常等于可用处理器数目,用于执行 CPU 密集型的计算任务。
Dispatchers.IO 表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中**,比如说执行网络请求时,为了能够支持更高的并发数量**,此时就可以使用Dispatchers.IO。 Dispatchers.IO 调度器使用的线程池是专门用于 IO 操作的线程池,它包含的线程数通常比 Default 调度器的线程数要多,用于执行 IO 密集型任务,例如网络请求、数据库操作等。
Dispatchers.Main 表示不会开启子线程,而是在Android主线程中执行代码,但是这个值只能在Android项目中使用,纯Kotlin程序使用这种类型的线程参数会出现错误。 调度器会在 Android 应用程序的主线程中执行协程,它通常用于更新 UI 界面。只适用于 Android 平台
Dispatchers.Unconfined 它不会将协程限制在任何特定的线程或线程池中,而是在调用协程的线程中立即执行协程,直到第一个挂起点。在第一个挂起点之后,Unconfined 调度器会自动将协程切换到默认的调度器中,继续执行协程的剩余部分。 Dispatchers.Unconfined 调度器不会将协程限制在任何特定的线程或线程池中,而是在调用协程的线程中立即执行协程,直到第一个挂起点。在第一个挂起点之后,Unconfined 调度器会自动将协程切换到默认的调度器中,继续执行协程的剩余部分

至于为什么CPU密集型的计算任务在Dispatcher.Default中执行,因为CPU密集型的计算任务不能在并发特别高的情况下进行

在我们刚才所学的协程作用域构建器中,除了coroutineScope函数之外,其他所有的函数都是可以指定这样一个线程参数的,只不过withContext()函数是强制要求指定的,而其他函数则是可选的。

4.3使用协程(suspendCoroutine函数)简化回调的写法

我们来想想Handler的回调

val handler = object : Handler(Looper.getMainLooper()){
    
    
    override fun handleMessage(msg: Message) {
    
    
        super.handleMessage(msg)
    }
}

会发现Handler的回调我们弄了一个匿名内部类Looper.getMainLooper()

发起多少个网络请求,就要编写多少次这样的匿名类实现。现在,Kotlin的协程使我们能有更简单的写法:

1.必须在 协程作用域或挂起函数 中调用
2.接收一个Lambda表达式参数
3.主要作用是将当前协程立即挂起,然后在一个普通的线程中执行Lambda表达式中的代码。
Lambda表达式的参数列表上会传入一个Continuation参数,调用它的**resume()方法或resumeWithException()**可让协程恢复

我们用suspendCoroutine函数简化

class MainActivity : AppCompatActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        coroutineScope.launch(Dispatchers.Main) {
    
    
            postToMainThread{
    
    
                var text:TextView = findViewById(R.id.text_22)
                text.setText("年后")
            }
        }
    }
    suspend fun postToMainThread(block: () -> Unit) {
    
    
        return suspendCoroutine {
    
     continuation ->
            Handler(Looper.getMainLooper()).post {
    
    
                block()
                continuation.resume(Unit)
            }
        }
    }
}

我先创建了一个coroutineScope.launch协程作用域.然后给它指定让它在主线程执行(因为要修改UI线程),然后

postToMainThread{
    
    xxx}

xxx是里面的具体实现

然后把postToMainThread挂起来(不挂起来就用不了delay那些方法),然后在 Handler 的主线程中执行传入的 block 函数。在 block 函数执行完成后,我们使用 continuation.resume(Unit) 方法恢复协程的执行,并将结果作为 Unit 类型的返回值返回给协程。

猜你喜欢

转载自blog.csdn.net/m0_61130325/article/details/131196864