本文主要剖析 Kotlin 编译后的字节码文件对应的 Smali,从本质上理解 Kotlin 的一些语法特性。
Kotlin 和 Java 一样,其实也是先编译成 Java Class 文件,再转换成 Dex 文件,最终运行在 Android 虚拟机上。
本文主要以 Smali 进行讲解。Smali 是 Android 平台特有的,它是 Dex 字节码的一种解释,是一个较低级的汇编语言。使用工具 baksmali 反汇编后,能获得后缀为 .smali 的文件。
如何查看 Smali?
Build->Build Bundle(s)/APK(s)->Build APK(s)
构建 apk 文件
- 点击
analyze
- 逐个翻阅 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$1
、FunctionKt$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$delegate
和 tom$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$2
和 Lazy$jojo$2
这两个类就不多做介绍了。主要是用来提供实际需要初始化的类型(即 Person)的初始化相关函数。感兴趣的可以自己编译相关代码看看。
创建一些像 Person 这样小的对象的时候,当然是不需要用到 by lazy 的,这里仅仅是为了举例说明。
另外,kotlin.LazyKt 的 lazy 函数有两个可选版本。注意,单参数的版本,用的就是 SynchronizedLazyImpl。如果我们不需要担心线程安全,应该优先使用两个参数的版本,并传参 LazyThreadSafetyMode.NONE。