从字节码的角度重学 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。

相关推荐
一丝晨光1 小时前
Java、PHP、ASP、JSP、Kotlin、.NET、Go
java·kotlin·go·php·.net·jsp·asp
GEEKVIP1 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model20053 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏6893 小时前
Android广播
android·java·开发语言
与衫4 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
500了10 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵11 小时前
Android Debug Bridge(ADB)完全指南
android·adb
小雨cc5566ru16 小时前
uniapp+Android面向网络学习的时间管理工具软件 微信小程序
android·微信小程序·uni-app
bianshaopeng17 小时前
android 原生加载pdf
android·pdf
hhzz17 小时前
Linux Shell编程快速入门以及案例(Linux一键批量启动、停止、重启Jar包Shell脚本)
android·linux·jar