从字节码的角度重学 Kotlin

本文主要剖析 Kotlin 编译后的字节码文件对应的 Smali,从本质上理解 Kotlin 的一些语法特性。

Kotlin 和 Java 一样,其实也是先编译成 Java Class 文件,再转换成 Dex 文件,最终运行在 Android 虚拟机上。

本文主要以 Smali 进行讲解。Smali 是 Android 平台特有的,它是 Dex 字节码的一种解释,是一个较低级的汇编语言。使用工具 baksmali 反汇编后,能获得后缀为 .smali 的文件。

如何查看 Smali?

  1. Build->Build Bundle(s)/APK(s)->Build APK(s) 构建 apk 文件
  1. 点击 analyze
  1. 逐个翻阅 dex 文件,比如 classes4.dex。找到目标文件,比如 ValAndVar,右键选中 Show Bytecode

如上图,我们要是左键点击 ValAndVar 类,还可以看见编译器为它生成的各种变量/函数,其中包括了我们自己定义的和编译器自动生成的。这时,我们也可以右键某个函数,查看它对应的 smali 代码,比如 getI():

smali 复制代码
.method public final getI()I
    .registers 2

    .line 8
    iget v0, p0, Lcom/example/kotlinbytecode/ValAndVar;->i:I

    return v0
.end method

val 和 var

kotlin 复制代码
class ValAndVar {
    val i = 0;
    var j = 0;
}

对于 val 变量,会为它生成 get/set 函数。对于 var 变量,只会为它生成 get 函数。

另外,查看 smali,会发现 val 变量会被 final 修饰,不允许被修改:

smali 复制代码
.field public final i:I

.field private j:I

如果你不希望 val/var 生成 get/set 函数,用 @JvmField 注解即可:

kotlin 复制代码
@JvmField
val i = 0;

顶层变量和函数

在 Kotlin 中,顶层函数和顶层变量是一种在任何类之外定义的函数和变量。与 Java 不同,Kotlin 不要求你将所有的函数和变量定义在类中。

下面是定义在 TopVariableAndFunc.kt 文件中的代码:

kotlin 复制代码
var variable:Int = 0


fun sum(a: Int, b: Int): Int {
    return a + b
}

fun main() {
    sum(0,1)
}

编译器会生成一个根据顶层变量/函数所在的文件,根据 文件名 + Kt 的规则,生成一个 TopVariableAndFuncKt类。顶层变量/函数,其实是被定义在该类里的静态变量/静态函数:

我们可以查看 main() 对应的 smali 代码,会发现有一句关键代码:

smali 复制代码
invoke-static {v0, v1}, Lcom/example/kotlinbytecode/TopVariableAndFuncKt;->sum(II)I

invoke-static 表明调用的是静态函数。

Lcom/example/kotlinbytecode/TopVariableAndFuncKt 就是 TopVariableAndFuncKt 的全路径名。

sum(II)I 指定了类中的方法名和方法描述符。sum 是方法名,括号内的 II 表示该方法接受两个整型参数,后面的 I 表示该方法返回一个整型结果。

所以调用顶层变量/函数,实际是通过编译器自动为它们生成的类去调用它们的,本质还是遵守 Java 规则。

const

const 只能修饰基本类型和 String。

下面是在名为 Const.kt 文件的顶层定义的一些变量,不在任何类或对象内部。

kotlin 复制代码
const val num = 0
const val str = "abc"
//编译不通过
//const val tom = Person("Tom")

编译器会为这些变量,生成一个名为 ConstKt 的类,持有这些变量:

从上面可以看出,和非 const 变量不同,编译器不会为 const 变量生成对应的 get/set 函数:

这些被 const 修饰的变量,最终会编译成 static final 字段:

smali 复制代码
.field public static final num:I = 0x0

.field public static final str:Ljava/lang/String; = "abc"

object

object 可以用来代替 class,声明一个类。编译器会将该类设为单例。

在关键字 class 声明的类中,不允许有 const 修饰的变量。但是 object 声明的类是可以的。

kotlin 复制代码
object Object {
    const val num = 0
}

我们来看编译后的结果:

我们重点关注一下编译器是怎么实现 Object 的单例的。这里先介绍几个重要的变量/函数:

  • INSTANCE:编译器生成的静态变量,是 Ojbect 的实例。
  • <clinit>:编译器会将类的静态代码块、静态变量的初始化,收集到该函数里。类加载的时候,会调用一次 <clinit>
  • <init>:编译器会将类的构造函数、init 代码块、成员变量的初始化,收集到该函数里,它才是类的实际构造函数。如果有多个构造函数,就会有多个<init>函数。

查看对应的 smali 文件:

smali 复制代码
.field public static final INSTANCE:Lcom/example/kotlinbytecode/Object;

.method static constructor <clinit>()V
    .registers 1
    // 创建一个 Object 实例
    new-instance v0, Lcom/example/kotlinbytecode/Object;
    // 调用 <init>() 执行对象的初始化
    invoke-direct {v0}, Lcom/example/kotlinbytecode/Object;-><init>()V
    // 将新建的 Ojbect 实例,赋值给 INSTANCE 变量
    sput-object v0, Lcom/example/kotlinbytecode/Object;->INSTANCE:Lcom/example/kotlinbytecode/Object;

    sget-object v0, Lcom/example/kotlinbytecode/LiveLiterals$ObjectKt;->INSTANCE:Lcom/example/kotlinbytecode/LiveLiterals$ObjectKt;

    invoke-virtual {v0}, Lcom/example/kotlinbytecode/LiveLiterals$ObjectKt;->Int$class-Object()I

    move-result v0

    sput v0, Lcom/example/kotlinbytecode/Object;->$stable:I

    return-void
.end method

// 注意,编译器生成的 <init>() 被设置成为了 private。
.method private constructor <init>()V
    .registers 1

    .line 7
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method

关键字 object 能将一个类设置为单例的关键就是编译器新增的这些代码:

  • <init> 设置为 private
  • <clinit> 里,创建一个类对象,并赋值给 INSTANCE 变量

data

关键字 data 主要用来声明数据类。

kotlin 复制代码
data class Person(val name:String)

编译后:

编译器会为我们自动生成:

  • 变量的 get/set 函数
  • equals/hashCode 函数
  • copy 函数:注意,只支持浅拷贝

sealed

sealed 可以用来修饰 class 或者 interface。它通常配合 when 使用:

kotlin 复制代码
when (phone) {
    is Phone.Apple -> phone.siri
    is Phone.Oppo -> phone.heartRate
}

优先考虑使用 sealed interface

通常建议用 sealed interface,而不是 sealed class。比如,实现同一个功能的时候:

用 sealed interface:

kotlin 复制代码
sealed interface Phone {
    val price: Int

    class Apple(override val price: Int, val siri: String) : Phone
    class Oppo(override val price: Int, val heartRate: String) : Phone
}

Phone 的编译结果:

用 sealed class:

kotlin 复制代码
sealed class Phone(open val price: Int) {
    class Apple(override val price: Int, val siri: String) : Phone(price)
    class Oppo(override val price: Int, val heartRate: String) : Phone(price)
}

Phone 的编译结果:

可以看到,使用 sealed class 的时候,编译器会生成一些通常用不到的函数。

问题又来了:如果我们启用混淆呢?这些用不到的函数,通常不是应该被裁剪掉吗?

部分会。比如 getPrice(),没有将子类向上转型为 Phone 去调用它的话,getPrice() 会被裁剪掉。

部分不会。比如 <init> 函数。子类在调用自己的 <init> 时,会先去调用父类的 <init>,完成父类的初始化。无法被裁剪掉。

sealed 与枚举

如果用枚举,描述上述的两种类型的手机:

kotlin 复制代码
enum class Phone(val price: Int) {
    Apple(8888), Oppo(6666)
}

很容易发现 sealed 和枚举的区别:

  • 枚举都是单例。
  • sealed 更自由。子类就是普通的类。可以创建多个 Apple 实例,每个实例都可以设定自己的 price。而且,可以定义新状态,或者说变量,如 siri。

受限的 sealed

密封类要求子类必须定义在同一个 package 下。这个限制似乎是编译器实现的。从 sealed interface Phone 的 smali 文件找的信息来看,它只是一个普通的 public 接口:

smali 复制代码
.class public interface abstract Lcom/example/kotlinbytecode/Phone

函数作为参数

Kotlin 支持函数式编程,可以把函数当作值使用,可以用变量保存它,把它当作参数传递,或者当作其他函数的返回值。

使用示例如下:

kotlin 复制代码
fun add(a: Int, b: Int): Int {
    return a + b
}

fun sub(a: Int, b: Int): Int {
    return a - b
}
// 用变量保存函数 add、sub
val addF = ::add
val subF = ::sub
// 将函数作为参数传递
fun calculate(a: Int, b: Int, func: (Int, Int) -> Int): Int {
    return func(a, b)
}

fun test() {
    val a = 2
    val b = 1
    val c = calculate(a, b, addF)
    val d = calculate(a, b, subF)
}

上面的代码是定义在 Function.kt 里的,所以我们去寻找 FunctionKt 类:

addF、subF 这两个函数变量的类型是:kotlin/reflect/KFunction。

kotlin 复制代码
public actual interface KFunction<out R> : KCallable<R>, Function<R> {
   ...
}

calculate 函数的函数变量入参类型被定义成了:kotlin/jvm/functions/Function2。

kotlin 复制代码
public interface Function2<in P1, in P2, out R> : kotlin.Function<R> {
    public abstract operator fun invoke(p1: P1, p2: P2): R
}

可以看到 KFunction、Function2 这两个接口,都继承自 Function。

我们先来看看 test 函数里,是怎么调用 calculate 函数的:

smali 复制代码
.method public static final test()V
    ...
    // 获取 addF 变量
    sget-object v2, Lcom/example/kotlinbytecode/FunctionKt;->addF:Lkotlin/reflect/KFunction;
    // 检查是否可以转换为 Function2 类型
    check-cast v2, Lkotlin/jvm/functions/Function2;
    // 调用 calculate()
    invoke-static {v0, v1, v2}, Lcom/example/kotlinbytecode/FunctionKt;->calculate(IILkotlin/jvm/functions/Function2;)I
    ...
    return-void
.end method

接着看看 calculate 函数:

smali 复制代码
.method public static final calculate(IILkotlin/jvm/functions/Function2;)I
    ...
    // 调用 Function2 的 invoke 函数
    invoke-interface {p2, v0, v1}, Lkotlin/jvm/functions/Function2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
    ...
    return v0
.end method

来看看 FunctionKt 类的 <clinit>,看看 addF、subF 这两个变量是怎么初始化的:

smali 复制代码
.method static constructor <clinit>()V
    .registers 1

    .line 15
    // 获取 FunctionKt$addF$1 的实例
    sget-object v0, Lcom/example/kotlinbytecode/FunctionKt$addF$1;->INSTANCE:Lcom/example/kotlinbytecode/FunctionKt$addF$1;
    // 检查是否可以转换为 KFunction 类型
    check-cast v0, Lkotlin/reflect/KFunction;
    // 赋值给 addF 变量
    sput-object v0, Lcom/example/kotlinbytecode/FunctionKt;->addF:Lkotlin/reflect/KFunction;

    .line 16
    // 获取 FunctionKt$subF$1 的实例
    sget-object v0, Lcom/example/kotlinbytecode/FunctionKt$subF$1;->INSTANCE:Lcom/example/kotlinbytecode/FunctionKt$subF$1;
    // 检查是否可以转换为 KFunction 类型
    check-cast v0, Lkotlin/reflect/KFunction;
     // 赋值给 subF 变量
    sput-object v0, Lcom/example/kotlinbytecode/FunctionKt;->subF:Lkotlin/reflect/KFunction;

    return-void
.end method

可以看到,addF 和 subF 这两个变量的实际类型分别是 FunctionKt$addF$1FunctionKt$subF$1。很明显,这两个类都是编译器自动生成的:

我们来看看 FunctionKt$addF$1 的 smali 代码:

smali 复制代码
.class final synthetic Lcom/example/kotlinbytecode/FunctionKt$addF$1;
// 父类是 FunctionReferenceImpl
.super Lkotlin/jvm/internal/FunctionReferenceImpl;
.source "Function.kt"

# interfaces
// 实现了接口 Function2
.implements Lkotlin/jvm/functions/Function2;

...

# virtual methods
// invoke 函数实现
.method public final invoke(II)Ljava/lang/Integer;
    ...
    
    // 调用 FunctionKt$addF$1 的 invoke 函数时,最终会调用 FunctionKt 的 add 函数
    invoke-static {p1, p2}, Lcom/example/kotlinbytecode/FunctionKt;->add(II)I

    ...

    return-object v0
.end method

综上,FunctionKt$addF$1的类继承图如下:

这里可能有点绕。不过将上述代码,转成对应的 Java 代码,就不难理解了:

Java 复制代码
public final class FunctionKt {
   private static final KFunction addF;
   private static final KFunction subF;

   public static final int add(int a, int b) {
      return a + b;
   }

   public static final int sub(int a, int b) {
      return a - b;
   }

   public static final KFunction getAddF() {
      return addF;
   }

   public static final KFunction getSubF() {
      return subF;
   }

   public static final int calculate(int a, int b, @NotNull Function2 func) {
      return func.invoke(a, b);
   }

   public static final void test() {
      int a = 2;
      int b = 1;
      calculate(a, b, (Function2)addF);
      calculate(a, b, (Function2)subF);
   }

   static {
      addF = FunctionKt$addF$1.INSTANCE;
      subF = FunctionKt$subF$1.INSTANCE;
   }
}

Studio 的 Tools -> Kotlin -> Show Kotlin Bytecode,会在右侧显示 Kotlin 的字节码,其中有个 Decompile 的选项。点击它,就能看见对应的 Java 代码。不过该代码只是伪代码,有时候和我们在 smali 看到的结果有差异。上述代码是经过调整后的结果。

by 和 lazy

Kotlin 中,可以用 by lazy 实现懒加载:

kotlin 复制代码
class Lazy {
    val tom by lazy { Person("Tom") }
    val jojo by lazy(LazyThreadSafetyMode.NONE) { Person("JoJo") }
}

编译结果:

可以看到,生成的两个变量是 jojo$delegatetom$delegate,都是 kotlin.Lazy 类型,不是 Person 类型(也不是上面的定义的 com.example.kotlinbytecode.Lazy 类)。

来看 com.example.kotlinbytecode.Lazy 的 <init>

smali 复制代码
.method public constructor <init>()V
    // 获取 Lazy$tom$2 实例,存储在寄存器 v0 中
    sget-object v0, Lcom/example/kotlinbytecode/Lazy$tom$2;->INSTANCE:Lcom/example/kotlinbytecode/Lazy$tom$2;
    // 检查是否可以转换为 Function0 类型
    check-cast v0, Lkotlin/jvm/functions/Function0;
    // 调用 kotlin.LazyKt 类中的 lazy 函数,参数是 v0 中的值,即 Lazy$tom$2 实例
    // 返回值是 kotlin/Lazy 实例
    invoke-static {v0}, Lkotlin/LazyKt;->lazy(Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    // 将返回值赋值给寄存器 v0
    move-result-object v0
    // 将 v0 的值赋值给 tom$delegate 变量(p0 就是 this)
    iput-object v0, p0, Lcom/example/kotlinbytecode/Lazy;->tom$delegate:Lkotlin/Lazy;
    // 获取 kotlin/LazyThreadSafetyMode.NONE,存储在寄存器 v0 中
    sget-object v0, Lkotlin/LazyThreadSafetyMode;->NONE:Lkotlin/LazyThreadSafetyMode;
    // 获取 Lazy$jojo$2 实例,存储在寄存器 v1 中
    sget-object v1, Lcom/example/kotlinbytecode/Lazy$jojo$2;->INSTANCE:Lcom/example/kotlinbytecode/Lazy$jojo$2;
    // 检查是否可以转换为 Function0 类型
    check-cast v1, Lkotlin/jvm/functions/Function0;
    // 用寄存器 v0、v1 的值,调用 kotlin/LazyKt 类的静态函数 lazy
    invoke-static {v0, v1}, Lkotlin/LazyKt;->lazy(Lkotlin/LazyThreadSafetyMode;Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    // 将返回值赋值给寄存器 v0
    move-result-object v0
    // 将 v0 的值赋值给 jojo$delegate 变量(p0 就是 this)
    iput-object v0, p0, Lcom/example/kotlinbytecode/Lazy;->jojo$delegate:Lkotlin/Lazy;
    return-void
.end method

懒加载的秘密,主要是通过 kotlin.LazyKt 这个类的 lazy 函数实现的。相关定义在 LazyJVM.kt 中:

kotlin 复制代码
// 生成的顶级类会被命名为 LazyKt
@file:kotlin.jvm.JvmName("LazyKt")
@file:kotlin.jvm.JvmMultifileClass

package kotlin
// 初始化 tom$delegate 时,会调用该函数。
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
// 初始化 jojo$delegate 时,会调用该函数。
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        // 用 synchronized 实现的线程安全
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        // 用 CAS 实现的线程安全
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        // 线程不安全
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

SynchronizedLazyImpl 的实现比较简单,主要确保了值在第一次被 get 函数获取的时候初始化,且只会初始化一次:

kotlin 复制代码
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
        
    ...
}

Lazy$tom$2Lazy$jojo$2 这两个类就不多做介绍了。主要是用来提供实际需要初始化的类型(即 Person)的初始化相关函数。感兴趣的可以自己编译相关代码看看。

创建一些像 Person 这样小的对象的时候,当然是不需要用到 by lazy 的,这里仅仅是为了举例说明。

另外,kotlin.LazyKt 的 lazy 函数有两个可选版本。注意,单参数的版本,用的就是 SynchronizedLazyImpl。如果我们不需要担心线程安全,应该优先使用两个参数的版本,并传参 LazyThreadSafetyMode.NONE。

相关推荐
lw向北.2 分钟前
Qt For Android之环境搭建(Qt 5.12.11 Qt下载SDK的处理方案)
android·开发语言·qt
不爱学习的啊Biao11 分钟前
【13】MySQL如何选择合适的索引?
android·数据库·mysql
Clockwiseee38 分钟前
PHP伪协议总结
android·开发语言·php
mmsx7 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
众拾达人10 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌11 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley12 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
hedalei14 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576
锋风Fengfeng14 小时前
安卓多渠道apk配置不同签名
android
枫_feng15 小时前
AOSP开发环境配置
android·安卓