End

Kotlin 朱涛-3 原理 编译器 反编译 字节码

本文地址


目录

03 | Kotlin原理:编译器在幕后干了什么

Kotlin 的编译流程

Kotlin 代码在运行之前,要先经过 Kotlin 编译器 的编译,编译后会变成 Java 字节码

Kotlin 和 Java 能够兼容的原因就在于,它们用的都是 Java 字节码。

如何研究 Kotlin 代码

两种方法

  • 直接研究 Kotlin 编译后的字节码
  • 将 Kotlin 转换成字节码后,再将字节码 反编译成等价的 Java 代码
println("Hello world.")

LDC "Hello world."
INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V

String var0 = "Hello world.";
System.out.println(var0);

IDEA 反编译步骤

  • 打开 Kotlin 源文件,例如 xxx.kt
  • 点击菜单栏 Tools -> Kotlin -> Show Kotlin Bytecode,这时候右边窗口中就可以直接展示对应的字节码
  • 点击右边窗口中的 Decompile,这时会打开一个新窗口,用来展示反编译出来的 Java 代码 xxx_decompiled.java

分析 Kotlin 中的基础类型

Kotlin 在 语法层面 摒弃了原始类型,难道就不担心由此带来的性能问题吗?

Kotlin 源码

下面这段代码的思路,是将 Kotlin 的 Long 类型所有可能的使用情况都列举出来,然后去研究反编译后对应的 Java 代码。

// 用 val 定义可为空、不可为空的 Long,并且赋值
val a: Long = 1L
val b: Long? = 2L

// 用 var 定义可为空、不可为空的 Long,并且赋值
var c: Long = 3L
var d: Long? = 4L

// 用 var 定义可为空的 Long,先赋值,然后改为 null
var e: Long? = 5L
e = null

// 用 val 定义可为空的 Long,直接赋值 null
val f: Long? = null

// 用 var 定义可为空的 Long,先赋值 null,然后赋值数字
var g: Long? = null
g = 6L

Java 源码

反编译后的 Java 代码

long a = 1L;
long b = 2L;

long c = 3L;
long d = 4L;

Long e = 5L;
e = (Long)null;

Long f = (Long)null;

Long g = (Long)null;
g = 6L;

源码分析

a、b、c、d 被转换成了 Java 的原始类型 long

  • a、c:因为它们的类型是不可为空的,所以 Kotlin 编译器会直接将它们优化成原始类型
  • b、d:它们的类型虽然可能为空,但是编译器对上下文分析后发现,他们不存在变成空的情况,所以也会将它们优化成原始类型

e、f、g 被转换成了 Java 里的包装类型 Long

  • 因为它们都被赋值过 null,所以 Kotlin 无法将它们优化成原始类型,因为 Java 中只有对象才能被赋值为 null

总结:

  • 只要基础类型的变量可能为空,那么这个变量就会被转换成 Java 的包装类型
  • 只要基础类型的变量不可能为空,那么这个变量就会被转换成 Java 的原始类型

分析 Kotlin 接口的新特性

Kotlin 接口有两个新特性:支持成员属性、支持方法默认实现。

下面就来分析下 Kotlin 接口语法的实现原理,从而找出它的局限性。

Kotlin 接口源码分析

interface Behavior {
    val canWalk: Boolean  // 接口内可以有成员属性
    fun walk() {          // 接口方法可以有默认实现
        if (canWalk) {
            println(canWalk)
        }
    }
}
public interface Behavior {
   boolean getCanWalk();  // 接口的成员属性变成了普通的接口方法
   void walk();           // 接口方法的默认实现消失了

   // 多了一个静态内部类,接口方法的默认实现被放到了静态内部类当中去了
   public static final class DefaultImpls {
      public static void walk(Behavior $this) {
         if ($this.getCanWalk()) {
            boolean var1 = $this.getCanWalk();
            System.out.println(var1);
         }
      }
   }
}
  • Kotlin 接口的 成员属性 本质上并不是一个真正的属性,当转换成 Java 以后,就变成了一个普通的接口方法
  • Kotlin 接口中 方法的默认实现,本质上也没有直接提供实现的代码,而只是在接口当中定义了一个静态内部类,然后将默认实现的代码放到了静态内部类当中去了

Kotlin 接口实现类源码分析

class Man: Behavior {                     // 实现 Behavior 接口
    override val canWalk: Boolean = true  // 重写 canWalk 属性
}
public final class Man implements Behavior {
   private final boolean canWalk = true; // 重写了 canWalk 属性,属性值为 true

   public boolean getCanWalk() {         // 新增了一个访问属性的方法
      return this.canWalk;               // 返回的是它内部 canWalk 属性的值
   }

   public void walk() {                 // 最终还是实现了接口当中的方法
      Behavior.DefaultImpls.walk(this); // 默认实现转到了接口的静态内部类中
   }
}

Kotlin 接口实现类调用逻辑

private fun testInterface() {
    val man = Man()
    man.walk()
    man.canWalk
}
private static final void testInterface() {
   Man man = new Man();
   man.walk();          // 调用方法保持不变
   man.getCanWalk();    // 调用属性也变成了调用方法
}

总结

  • 箭头①:Kotlin 接口属性,实际上会被当中接口方法来看待
  • 箭头②:Kotlin 接口默认实现,实际上还是一个普通的方法
  • 箭头③:Kotlin 接口默认实现的逻辑,被放在了静态内部类中
  • 箭头④:Kotlin 接口的实现类中,接口属性被转化成了一个接口方法
  • 箭头⑤:Kotlin 接口的实现类中,仍然会实现接口默认实现的方法,并且执行流程转给了接口的静态内部类

因此,Kotlin 接口中的属性,它既不能真正存储任何状态,也不能被赋予初始值,因为它本质上还是一个接口方法。

  • 接口属性如果被赋予了初始值,会提示:Property initializers are not allowed in interfaces
  • 接口属性如果没被实现类重写,会提示:Class xxx is not abstract and does not implement abstract member xxx
  • 在 Kotlin 接口中定义类似 Java 中的 public static final 常量,需要使用伴生对象:
interface Behavior {
    companion object {               // 在接口中定义常量
        @JvmField val canWalk = true // 会提示: 'const' might be used instead of '@JvmField'
    }
}

Behavior.canWalk // 访问接口中定义的常量

小结

Kotlin 的每一个语法,最终都会被翻译成对应的 Java 字节码。所有 Kotlin 的新特性,最终都被转换成了一种 Java 能认识的语法。

正是因为 Kotlin 编译器在背后做的这些翻译工作,才可以让我们写出的 Kotlin 代码更加简洁、更加安全。

  • 类型推导:编译器会将省略的变量类型,补充回来
  • 原始类型:编译器会根据每一个变量的可空性,将它们转换成原始类型或者包装类型
  • 字符串模板:编译器会将字符串模板转换成 Java 拼接的形式
  • when 表达式:编译器会其转换成 switch case 语句
  • 类默认 public:编译器会将省略的 public,补充回来
  • 嵌套类默认 static:编译器会给 Kotlin 中的嵌套类,默认添加 static 关键字
  • 数据类:编译器会自动给数据类添加 equals、copy、toString 等方法

2018-06-09

posted @ 2018-06-09 20:51  白乾涛  阅读(3182)  评论(0编辑  收藏  举报