美文网首页
Kotlin协程学习总结

Kotlin协程学习总结

作者: James999 | 来源:发表于2020-06-23 16:27 被阅读0次

    Kotlin VS JAVA

    1. 变量与常量
       //java
       String name = "Test";
       final String name = "Test";
    
      //kotlin
      var name = "Test"
    
    1. 空判断
     //java
    if (text != null) {
        int length = text.length();
    }
    
    //kotlin
    val length = text?.length()
    
    1. case 语句
    //java
    int score = // some score;
    String grade;
    switch (score) {
        case 9:
            grade = "Excellent";
            break;
        case 6:
            grade = "Good";
            break;
        case 5:
        case 4:
            grade = "OK";
            break;
        case 1:
            grade = "Fail";
            break;
        default:
            grade = "Fail";
    }
    
    // Kotlin
    var score = // some score
    var grade = when (score) {
        9, 10 -> "Excellent"
        in 6..8 -> "Good"
        4, 5 -> "OK"
        in 1..3 -> "Fail"
        else -> "Fail"
    }
    
    1. 方法定义
    // java
    int getScore() {
       // logic here
       return score;
    }
    
    // Kotlin
    fun getScore(): Int {
       // logic here
       return score
    }
    
    1. 类,和接口的继承
    // java
    public class Child extends Parent implements IHome {
    //
    }
    
    // kotlin
    class Child : Parent(), IHome {
    //
    }
    
    1. java协程和kotlin线程
    // java 线程
       void threadFun(){
            Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                       
                    }
                });
            thread.start();
       }
    
    // kotlin 协程
        fun coroutineFun() {
            GlobalScope.launch(Dispatchers.Main) {
                //delay(8000L) 
                println("CoroutineTest World!") 
            }
        }
    

    Java VS Kotlin 生成 Bytecode

    Java Kotlin编译过程对比

    image.png

    生成的Bytecode方式

    1. *.java code 由Javac 生成Bytecode
    2. *.kt code由 kotlinc-jvm

    生成的 Bytecode 比对

    Kotlin code 反编译回Java code的 汇编字节码对比。

    1. 如下Kotlin code Main.kt
    class Main {
        fun  main() {
            val m = "Hello"
        }
    }
    
    1. 选择菜单 Tools-> Kotlin-> show Kotlin Bytecode(在Kotlin Bytecode窗口右上方点击 Decompile)
      反编译后的code 如下
    import kotlin.Metadata;
    @Metadata(
       mv = {1, 1, 16},
       bv = {1, 0, 3},
       k = 1,
       d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"},
       d2 = {"Lcom/yy/kotlindemo/kotlinbasic/Main;", "", "()V", "main", "", "app_debug"}
    )
    public final class Main {
       public final void main() {
          String m = "Hello";
       }
    }
    

    3.然后分别查看上述两段code的字节码

     public class Main {
       public void main() {
          String m = "Hello";
       }
     }
    
    • 通过Java Output字节码汇编比对, kotlin使用kotlin 1.3.50 compiler, Java使用OpenJDK 6u79


      image.png
    • Constant pool 比较,右图Kotlin,左图Java


      image.png
    1. Kotlin @Metadata 解析
    Metadata KotlinClassHeader 相关描述
    k kind 注解标注目标类型,例如类、文件等等
    mv metadataVersion 该元数据的版本
    bv bytecodeVersion 字节码版本
    d1 data 自定义元数据
    d2 strings 自定义元数据补充字段
    xi extraInt 加入的附加标记,标记类文件的来源类型

    简要说明Kotlin Class 文件结构

    Main.class
    Classfile /tmp/1405968806779468952/classes/Main.class
      Last modified May 28, 2020; size 566 bytes
      MD5 checksum b1d7f50abdea6527c5c67d5b34b227a0
      Compiled from "Main.kt"
    public final class Main
    SourceFile: "Main.kt"
      minor version: 0      // 副版本号
      major version: 50     //主版本号
      flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER //  flags 为一种掩码的访问标志,用于表示某个类,接口
      // ACC_PUBLIC: 声明为public,可以被包外访问,ACC_FINAL:声明为final,不允许有子类,[ACC_SUPER](https://blog.csdn.net/jokkkkk/article/details/86648610): 这个标志是为了纠正 invokespecial 在调用父类方法存在的版本问题。invokespecial是一个调用方法的字节码指令,用在调用构造方法、本类private方法、父类非虚方法3种场景
    Constant pool:
       #1 = Utf8               Main
       #2 = Class              #1             // Main
       #3 = Utf8               java/lang/Object
       #4 = Class              #3             // java/lang/Object
       #5 = Utf8               main
       #6 = Utf8               ()V
       #7 = Utf8               Hello
       #8 = String             #7             // Hello
       #9 = Utf8               m
      #10 = Utf8               Ljava/lang/String;
      #11 = Utf8               this
      #12 = Utf8               LMain;
      #13 = Utf8               <init>
      #14 = NameAndType        #13:#6         // "<init>":()V
      #15 = Methodref          #4.#14         // java/lang/Object."<init>":()V
      #16 = Utf8               Lkotlin/Metadata;
      #17 = Utf8               mv
      #18 = Integer            1
      #19 = Integer            15
      #20 = Utf8               bv
      #21 = Integer            0
      #22 = Integer            3
      #23 = Utf8               k
      #24 = Utf8               d1
      #25 = Utf8               \u0000�\n���\n��\u0000\n��\n����\u00002�0�B�¢����J�����0�
      #26 = Utf8               d2
      #27 = Utf8
      #28 = Utf8               Main.kt
      #29 = Utf8               Code
      #30 = Utf8               LineNumberTable
      #31 = Utf8               LocalVariableTable
      #32 = Utf8               SourceFile
      #33 = Utf8               RuntimeVisibleAnnotations
    {
     public Main();
        descriptor: ()V
        // 括号是空的,代表无参数 ,V 代表无返回值。
        // 如果是 descriptor: I  代表该变量的类型 是 int。 
        // 或者 descriptor: java.lang.string ?: 代表该变量的类型 是string。
        flags: ACC_PUBLIC  // flags: 代表该方法的修饰情况,ACC_PUBLIC 声明为public,可以从包外访问。
        Code:
          stack=1, locals=1, args_size=1
            start local 0 // Main this
             0: aload_0  // 从局部变量中加载索引为0的变量的值 (也指引用类型值)。 即this 的引用,压入栈 。
             1: invokespecial #15                 // Method java/lang/Object."<init>":()V
             // invokespecial 出栈,调用java/lang/Object."<init>":()V 初始化对象,就是this指定的对象的init()方法完成初始化。
             4: return
            end local 0 // Main this
          LineNumberTable:
            line 4: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   LMain;
      public final void main();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_FINAL
        Code:
          stack=1, locals=2, args_size=1
          // stack = 1 表示操作数栈深度为2;locals=2, 本地变量为2个;args_size=1 表示有一个参数(默认为this)
            start local 0 // Main this
             0: ldc           #8                  // String Hello
             // ldc 将String类型 “Hello” 常量值,从常量池 #8 推送至栈顶
             2: astore_1
             //将栈顶引用型数值存入第二个变量
            start local 1 // java.lang.String m
             3: return
            end local 0 // Main this
            end local 1 // java.lang.String m
          LineNumberTable:
          // LineNumberTable:指令与代码行数的偏移对应关系,每一行第一个数字对应代码行数,第二个数字对应前面code中指令
          // (注意: Javap -c中显示的东西叫做 指令。)
          // 也就是说Main.java文件中 第6行的代码 在执行顺序第 0 行。 Line 6: 就是第6行
            line 6: 0
            line 7: 3
          LocalVariableTable: 
          // 代表它是  局部变量表,start+length表示这个变量在字节码中的生命周期起始和结束的偏移位置(this生命周期从头0
          // 到结尾10)slot就是这个变量在局部变量表中的槽位(槽位可复用),name是变量名称,Signatur是局部变量类型描述
            Start  Length  Slot  Name   Signature
                3       1     1     m   Ljava/lang/String;
                0       4     0  this   LMain;
    }
    
    RuntimeVisibleAnnotations:
      0: #16(#17=[I#18,I#18,I#19],#20=[I#18,I#21,I#22],#23=I#18,#24=[s#25],#26=[s#12,s#27,s#6,s#5,s#27])
    

    Kotlin的协程在虚拟机上实现的原理

    Kotlin 协程

    1. 默认情况,协程运行在一个共享的线程池中。 线程仍然存在于基于协程的程序中,但是一个线程可以运行大量的协程,所以这里不需要太多线程。
    2. 本质上,协程是轻量级的线程。 它们在某些 CoroutineScope 上下文中与 launch 协程构建器 一起启动。
      这里我们在 GlobalScope 中启动了一个新的协程,这意味着新协程的生命周期只受整个应用程序的生命周期限制。
    import kotlinx.coroutines.*
    fun main() {
        GlobalScope.launch { // 在后台启动一个新的协程并继续
            delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
            println("World!") // 在延迟后打印输出
        }
        println("Hello,") // 协程已在等待时主线程还在继续
        Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
    }
    

    这是因为 delay 是一个特殊的 挂起函数 ,它不会造成线程阻塞,但是会 挂起 协程,并且只能在协程中使用。
    delay 为什么不会造成线程阻塞。

    1. 协程术语
    • 协程: 可挂起计算的实例。
    • 挂起函数 : suspend 修饰符标记的函数。
      它就像一个普通的函数类型,但具有 suspend 修饰符。
    • 挂起 lambda 表达式 : 必须在协程中运行的代码块。
    • 挂起函数类型 : 表示挂起函数以及挂起 lambda 表达式的函数类型,
      举个例子,suspend () -> Int 是一个没有参数、返回 Int 的挂起函数的函数类型。
      一个声明为 suspend fun foo() : Int 的挂起函数符合上述函数类型。
    • 协程构建器 : 使用一些挂起 lambda 表达式作为参数来创建一个协程的函数,并且可选地,还提供某种形式以访问协程的结果。
      例如,用例中的 launch{}、future{} 以及 sequence{} 就是协程构建器。
    • 挂起点 : 协程执行过程中可能被挂起的位置。
      从语法上说,挂起点是对一个挂起函数的调用,但实际的挂起在挂起函数调用了标准库中的原始挂起函数时发生。
    • 续体 : 是挂起的协程在挂起点时的状态.


      image.png

    Suspend 挂起函数原理

    挂起函数不会阻塞线程,其原理是kotlin编译层面的设计

    Lambda 表达式

    1. Kotlin中 Lambda 表达式的约定。


      image.png

    线程和协程

    1. 线程 线程拥有独立的栈、局部变量,基于进程的共享内存,因此数据共享比较容易,但是多线程时需要加锁来进行访问控制,不加锁就容易导致数据错误,但加锁过多又容易出现死锁。
      线程之间的调度由内核控制(时间片竞争机制),程序员无法介入控制。线程之间的切换需要深入到内核级别,因此线程的切换代价比较大,表现在:

      • 线程对象的创建和初始化
      • 线程上下文切换
      • 线程状态的切换由系统内核完成
      • 对变量的操作需要加锁
        image.png
    2. 协程 协程是跑在线程上的优化产物,被称为轻量级 Thread,拥有自己的栈内存和局部变量,共享成员变量。
      Coroutine 可以用来直接标记方法,由程序员自己实现切换,调度,不再采用传统的时间段竞争机制。
      在一个线程上可以同时跑多个协程,同一时间只有一个协程被执行,在单线程上模拟多线程并发,协程何时运行,
      何时暂停,都是有程序员自己决定的,使用: yield/resume API,优势如下:

      • 因为在同一个线程里,协程之间的切换不涉及线程上下文的切换和线程状态的改变,不存在资源、数据并发,所以不用加锁,只需要判断状态就OK,所以执行效率比多线程高很多。
      • 协程是非阻塞式的(也有阻塞API),一个协程在进入阻塞后不会阻塞当前线程,当前线程会去执行其他协程任务。
        image.png

    参考

    在线查看字节码
    注解 Metedate 参考
    jvms7.pdf
    协程相关参考
    [协程参考] (https://github.com/Kotlin-zh/KEEP/blob/master/proposals/coroutines.md)

    相关文章

      网友评论

          本文标题:Kotlin协程学习总结

          本文链接:https://www.haomeiwen.com/subject/wduhfktx.html