在 2019 年 Google I/O 大會上,Google 宣布了今后 Android 開發將優先使用 Kotlin ,即 Kotlin-first,隨之在 Android 開發界興起了一陣全民學習 Kotlin 的熱潮。之后 Google 也推出了一系列用 Kotlin 實現的 ktx 擴展庫,例如 activity-ktx
、fragment-ktx
、core-ktx
等,提供了各種方便的擴展方法用于簡化開發者的工作,Kotlin 協程目前也是官方在 Android 上進行異步編程的推薦解決方案
Google 推薦優先使用 Kotlin,也宣稱不會放棄 Java,但目前各種 ktx 擴展庫還是需要由 Kotlin 代碼進行使用才能最大化地享受到其便利性,Java 代碼來調用顯得有點不倫不類。作為 Jetpack 主要組件之一的 Paging 3.x 版本目前也已經完全用 Kotlin 實現,為 Kotlin 協程提供了一流的支持。剛出正式版本不久的 Jetpack Compose 也只支持 Kotlin,Java 無緣聲明式 UI
開發者可以感受到 Kotlin 在 Android 開發中的重要性在不斷提高,雖然 Google 說不會放棄 Java,但以后的事誰說得準呢?開發者還是需要盡早遷移到 Kotlin,這也是必不可擋的技術趨勢
Kotlin 在設計理念上有很多和 Java 不同的地方,開發者能夠直觀感受到的是語法層面上的差異性,背后也包含有一系列隱藏的性能開銷以及一些隱藏得很深的“坑”,本篇文章就來介紹在使用 Kotlin 過程中存在的隱藏性能開銷,幫助讀者避坑,希望對你有所幫助 ????
慎用 @JvmOverloads
@JvmOverloads
注解大家應該不陌生,其作用在具有默認參數的方法上,用于向 Java 代碼生成多個重載方法
例如,以下的 println
方法對于 Java 代碼來說就相當于兩個重載方法,默認使用空字符串作為入參參數
//Kotlin
@JvmOverloads
fun println(log: String = "") {
}
//Java
public void println(String log) {
}
public void println() {
println("");
}
@JvmOverloads
很方便,減少了 Java 代碼調用 Kotlin 代碼時的調用成本,使得 Java 代碼也可以享受到默認參數的便利,但在某些特殊場景下也會引發一個隱藏得很深的 bug
舉個例子
我們知道 Android 系統的 View 類包含有多個構造函數,我們在實現自定義 View 時至少就要聲明一個包含有兩個參數的構造函數,參數類型必須依次是 Context 和 AttributeSet,這樣該自定義 View 才能在布局文件中使用。而 View 類的構造函數最多包含有四個入參參數,最少只有一個,為了省事,我們在用 Kotlin 代碼實現自定義 View 時,就可以用 @JvmOverloads
來很方便地繼承 View 類,就像以下代碼
open class BaseView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0, defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes)
如果我們是像 BaseView 一樣直接繼承于 View 的話,此時使用@JvmOverloads
就不會產生任何問題,可如果我們繼承的是 TextView 的話,那么問題就來了
直接繼承于 TextView 不做任何修改,在布局文件中分別使用 MyTextView 和 TextView,給它們完全一樣的參數,看看運行效果
open class MyTextView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0, defStyleRes: Int = 0
) : TextView(context, attrs, defStyleAttr, defStyleRes)
<github.leavesc.demo.MyTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="伯羽君"
android:textSize="42sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="伯羽君"
android:textSize="42sp" />
此時兩個 TextView 就會呈現出不一樣的文本顏色了,十分神奇
[圖片上傳失敗...(image-fa1450-1637567285526)]
這就是 @JvmOverloads
帶來的一個隱藏問題。因為 TextView 的 defStyleAttr
實際上是有一個默認值的,即 R.attr.textViewStyle
,當中就包含了 TextView 的默認文本顏色,而由于 MyTextView 為 defStyleAttr
指定了一個默認值 0,這就導致 MyTextView 丟失了一些默認風格屬性
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
因此,如果我們要直接繼承的是 View 類的話可以直接使用@JvmOverloads
,此時不會有任何問題,而如果我們要繼承的是現有控件的話,就需要考慮應該如何設置默認值了
慎用 解構聲明
有時我們會有把一個對象拆解成多個變量的需求,Kotlin 也提供了這類語法糖支持,稱為解構聲明
例如,以下代碼就將 People 變量解構為了兩個變量:name 和 nickname,變量名可以隨意取,每個變量就按順序對應著 People 中的字段
data class People(val name: String, val nickname: String)
private fun printInfo(people: People) {
val (name, nickname) = people
println(name)
println(nickname)
}
每個解構聲明其實都會被編譯成以下代碼,解構操作其實就是在按照順序獲取特定方法的返回值
String name = people.component1();
String nickname = people.component2();
component1()
和 component2()
函數是 Kotlin 為數據類自動生成的方法,People 反編譯為 Java 代碼后就可以看到,每個方法返回的其實都是成員變量,方法名包含的數字對應的就是成員變量在數據類中的聲明順序
public final class People {
@NotNull
private final String name;
@NotNull
private final String nickname;
@NotNull
public final String component1() {
return this.name;
}
@NotNull
public final String component2() {
return this.nickname;
}
}
解構聲明和數據類配套使用時就有一個隱藏的知識點,看以下例子
假設后續我們為 People 添加了一個新字段 city,此時 printInfo
方法一樣可以正常調用,但 nickname 指向的其實就變成了 people 變量內的 city 字段了,含義悄悄發生了變化,此時就會導致邏輯錯誤了
data class People(val name: String, val city: String, val nickname: String)
private fun printInfo(people: People) {
val (name, nickname) = people
println(name)
println(nickname)
}
數據類中的字段是可以隨時增減或者變換位置的,從而使得解構結果和我們一開始預想的不一致,因此我覺得解構聲明和數據類不太適合放在一起使用
慎用 toLowerCase 和 toUpperCase
當我們要以忽略大小寫的方式比較兩個字符串是否相等時,通常想到的是通過 toUpperCase
或 toLowerCase
方法將兩個字符串轉換為全大寫或者全小寫,然后再進行比較,這種方式完全可以滿足需求,但當中也包含著一個隱藏開銷
例如,以下的 Kotlin 代碼反編譯為 Java 代碼后,可以看到每次調用toUpperCase
方法都會創建一個新的臨時變量,然后再去調用臨時變量的 equals
方法進行比較
fun main() {
val name = "leavesC"
val nickname = "leavesc"
println(name.toUpperCase() == nickname.toUpperCase())
}
public static final void main() {
String name = "leavesC";
String nickname = "leavesc";
String var10000 = name.toUpperCase();
String var10001 = nickname.toUpperCase();
boolean var2 = Intrinsics.areEqual(var10000, var10001);
System.out.println(var2);
}
以上代碼就多創建了兩個臨時變量,這樣的代碼無疑會比較低效
有一個更好的解決方案,就是通過 Kotlin 提供的支持忽略大小寫的 equals
擴展方法來進行比較,此方法內部會調用 String 類原生的 equalsIgnoreCase
來進行比較,從而避免了創建臨時變量,相對來說會比較高效一些
fun main() {
val name = "leavesC"
val nickname = "leavesc"
println(name.equals(other = nickname, ignoreCase = true))
}
public static final void main() {
String name = "leavesC";
String nickname = "leavesc";
boolean var2 = StringsKt.equals(name, nickname, true);
boolean var3 = false;
System.out.println(var2);
}
慎用 arrayOf
Kotlin 中的數組類型可以分為兩類:
-
IntArray、LongArray、FloatArray
形式的基本數據類型數組,通過intArrayOf、longArrayOf、floatArrayOf
等方法來聲明 -
Array<T>
形式的對象類型數組,通過arrayOf、arrayOfNulls
等方法來聲明
例如,以下的 Kotlin 代碼都是用于聲明整數數組,但實際上存儲的數據類型并不一樣
val intArray: IntArray = intArrayOf(1, 2, 3)
val integerArray: Array<Int> = arrayOf(1, 2, 3)
將以上代碼反編譯為 Java 代碼后,就可以明確地看出一種是基本數據類型 int,一種是包裝類型 Integer,arrayOf
方法會自動對入參值進行裝箱
private final int[] intArray = new int[]{1, 2, 3};
private final Integer[] integerArray = new Integer[]{1, 2, 3};
為了表示基本數據類型的數組,Kotlin 為每一種基本數據類型都提供了若干相應的類并做了特殊的優化。例如,IntArray、ByteArray、BooleanArray
等類型都會被編譯成普通的 Java 基本數據類型數組:int[]、byte[]、boolean[]
,這些數組中的值在存儲時不會進行裝箱操作,而是使用了可能的最高效的方式
因此,如果沒有必要的話,我們在開發中要慎用 arrayOf
方法,避免不必要的裝箱消耗
慎用 vararg
和 Java 一樣,Kotlin 也支持可變參數,允許將任意多個參數打包到一個數組中再一并傳給函數,Kotlin 通過使用 varage 關鍵字來聲明可變參數
我們可以向 printValue
方法傳遞任意數量的入參參數,也可以直接傳入一個數組對象,但 Kotlin 要求顯式地解包數組,以便每個數組元素在函數中能夠作為單獨的參數來調用,這個功能被稱為展開運算符,使用方式就是在數組前加一個 *
fun printValue(vararg values: Int) {
values.forEach {
println(it)
}
}
fun main() {
printValue()
printValue(1)
printValue(2, 3)
val values = intArrayOf(4, 5, 6)
printValue(*values)
}
如果我們是以直接傳遞若干個入參參數的形式來調用 printValue
方法的話,Kotlin 會自動將這些參數打包為一個數組進行傳遞,這里面就包含著創建數組的開銷,這方面和 Java 保持一致。 如果我們傳入的參數就已經是數組的話,Kotlin 相比 Java 就存在著一個隱藏開銷,Kotlin 會復制現有數組作為參數拿來使用,相當于多分配了額外的數組空間,這可以從反編譯后的 Java 代碼看出來
public static final void printValue(@NotNull int... values) {
Intrinsics.checkNotNullParameter(values, "values");
int $i$f$forEach = false;
int[] var3 = values;
int var4 = values.length;
for(int var5 = 0; var5 < var4; ++var5) {
int element$iv = var3[var5];
int var8 = false;
boolean var9 = false;
System.out.println(element$iv);
}
}
public static final void main() {
printValue();
printValue(1);
printValue(2, 3);
int[] values = new int[]{4, 5, 6};
//復制后再進行調用
printValue(Arrays.copyOf(values, values.length));
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
可以看到 Kotlin 會通過 Arrays.copyOf
復制現有數組,將復制后的數組作為參數進行調用,這樣做的好處就是可以避免 printValue
方法影響到原有數組,壞處就是會額外消耗多一份的內存空間
慎用 lazy
我們經常會使用lazy()
函數來惰性加載只讀屬性,將加載操作延遲到需要使用的時候,適用于某些不適合立刻加載或者加載成本較高的情況
例如,以下的 lazyValue
只會等到我們調用到的時候才會被賦值
val lazyValue by lazy {
"it is lazy value"
}
而在使用lazy()
函數時很容易被忽略的地方就是其包含有一個可選的 model 參數:
- LazyThreadSafetyMode.SYNCHRONIZED。只允許由單個線程來完成初始化,且初始化操作包含有雙重鎖檢查,從而使得所有線程都得到相同的值
- LazyThreadSafetyMode.PUBLICATION。允許多個線程同時執行初始化操作,但只有第一個初始化成功的值會被當做最終值,最終所有線程也都會得到相同的值
- LazyThreadSafetyMode.NONE。允許多個線程同時執行初始化操作,不進行任何線程同步,導致不同線程可能會得到不同的初始化值,因此不應該用于多線程環境
lazy()
函數默認情況下使用的就是LazyThreadSafetyMode.SYNCHRONIZED
,從 SynchronizedLazyImpl 可以看到,其內部就使用到了synchronized
來實現多線程同步,以此避免多線程競爭
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
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
}
}
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
private fun writeReplace(): Any = InitializedLazyImpl(value)
}
對于 Android 開發者來說,大多數情況下我們都是在主線程中調用 lazy()
函數,此時使用 LazyThreadSafetyMode.SYNCHRONIZED
就會帶來不必要的線程同步開銷,因此可以根據實際情況考慮替換為LazyThreadSafetyMode.NONE
慎用 lateinit var
lateinit var 適用于某些不方便馬上就初始化變量的場景,用于將初始化操作延后,同時也存在一些使用上的限制:如果在未初始化的情況下就使用該變量的話會導致 NPE
例如,如果在 name 變量還未初始化時就調用了 print
方法的話,此時就會導致 NPE。且由于 lateinit var 變量不允許為 null,因此此時我們也無法通過判空來得知 name 是否已經被初始化了,而且判空操作本身也相當于在調用 name 變量,在未初始化的時候一樣會導致 NPE
lateinit var name: String
fun print() {
println(name)
}
我們可以通過另一種方式來判斷 lateinit 變量是否已初始化
lateinit 實際上是通過代理機制來實現的,關聯的是 KProperty0 接口,KProperty0 就提供了一個擴展屬性用于判斷其代理的值是否已經初始化了
@SinceKotlin("1.2")
@InlineOnly
inline val @receiver:AccessibleLateinitPropertyLiteral KProperty0<*>.isInitialized: Boolean
get() = throw NotImplementedError("Implementation is intrinsic")
因此我們可以通過以下方式來進行判斷,從而避免不安全的訪問操作
lateinit var name: String
fun print() {
if (this::name.isInitialized) {
println("isInitialized true")
println(name)
} else {
println("isInitialized false")
println(name) //會導致 NPE
}
}
lambda 表達式
lambda 表達式在語義上很簡潔,既避免了冗長的函數聲明,也解決了以前需要強類型聲明函數類型的情況
例如,以下代碼就通過 lambda 表達式聲明了一個回調函數 callback,我們無需創建一個具體的函數類型,而只需聲明需要的入參參數、入參類型、函數返回值就可以
fun requestHttp(callback: (code: Int, data: String) -> Unit) {
callback(200, "success")
}
fun main() {
requestHttp { code, data ->
println("code: $code")
println("data: $data")
}
}
lambda 表達式語法雖然方便,但也隱藏著兩個性能問題:
- 每次調用 lambda 表達式都相當于在創建一個對象
- lambda 表達式內部隱藏了自動裝箱和自動拆箱的操作
將以上代碼反編譯為 Java 代碼后,可以看到 callback 最終的實際類型就是 Function2,每次調用requestHttp
方法就相當于是在創建一個 Function2 變量
public static final void requestHttp(@NotNull Function2 callback) {
Intrinsics.checkNotNullParameter(callback, "callback");
callback.invoke(200, "success");
}
Function2 是 Kotlin 提供的一個的泛型接口,數字 2 即代表其包含兩個入參值
public interface Function2<in P1, in P2, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2): R
}
Kotlin 會在編譯階段將開發者聲明的 lambda 表達式轉換為相應的 FunctionX 對象,調用 lambda 表達式就相當于在調用其 invoke
方法,以此為低版本 JVM 平臺(例如 Java 6 / 7)也能提供 lambda 表達式功能。此外,我們也知道泛型類型不可能是基本數據類型,因此我們在 Kotlin 中聲明的 Int 最終會被自動裝箱為 Integer,lambda 表達式內部自動完成了裝箱和拆箱的操作
所以說,簡潔的 lambda 表達式背后就隱藏了自動創建 Function 對象進行中轉調用,自動裝箱和自動拆箱的過程,且最終創建的方法總數要多于表面上看到的
如果想要避免 lambda 表達式的以上開銷,可以通過使用 inline 內聯函數來實現
在使用 inline 關鍵字修飾 requestHttp
方法后,可以看到此時 requestHttp
的邏輯就相當于被直接復制到了 main
方法內部,不會創建任何多余的對象,且此時使用的也是 int 而非 Integer
inline fun requestHttp(callback: (code: Int, data: String) -> Unit) {
callback(200, "success")
}
fun main() {
requestHttp { code, data ->
println("code: $code")
println("data: $data")
}
}
public static final void main() {
String data = "success";
int code = 200;
String var4 = "code: " + code;
System.out.println(var4);
var4 = "data: " + data;
System.out.println(var4);
}
通過內聯函數,可以使得編譯器直接在調用方中使用內聯函數體中的代碼,相當于直接把內聯函數中的邏輯復制到了調用方中,完全避免了調用帶來的開銷。對于高階函數,作為參數傳遞的 lambda 表達式的主體也將被內聯,這使得:
- 聲明和調用 lambda 表達式時,不會實例化 Function 對象
- 沒有自動裝箱和拆箱的操作
- 不會導致方法數增多,但如果內聯函數方法體較大且被多處調用的話,可能導致最終代碼量顯著增加
init 的聲明順序很重要
看以下代碼,我們可以在 init 塊中調用 parameter1,卻無法調用 parameter2,從 IDE 的提示信息 Variable 'parameter2' must be initialized
也可以看出來,對于 init 塊來說 parameter2 此時還未賦值,自然就無法使用了
class KotlinMode {
private val parameter1 = "leavesC"
init {
println(parameter1)
//error: Variable 'parameter2' must be initialized
//println(parameter2)
}
private val parameter2 = "伯羽君"
}
從反編譯出的 Java 代碼也可以看出來,由于 parameter2 是聲明在 init 塊之后,所以 parameter2 的賦值操作其實是放在構造函數中的最后面,因此 IDE 的語法檢查器就會阻止我們在 init 塊中來調用 parameter2 了
public final class KotlinMode {
private final String parameter1 = "leavesC";
private final String parameter2;
public KotlinMode() {
String var1 = this.parameter1;
System.out.println(var1);
this.parameter2 = "伯羽君";
}
}
IDE 會阻止開發者去調用還未初始化的變量,防止我們寫出不安全的代碼,我們也可以用以下方式來繞過語法檢查,但同時也寫出了不安全的代碼
我們可以通過在 init 塊中調用 print()
方法的方式來間接訪問 parameter2,此時代碼是可以正常編譯的,但此時 parameter2 也只會為 null
class KotlinMode {
private val parameter1 = "leavesC"
init {
println(parameter1)
print()
}
private fun print() {
println(parameter2)
}
private val parameter2 = "伯羽君"
}
從反編譯出的 Java 代碼可以看出來,print()
方法依舊是會在 parameter2 初始化之前被調用,此時print()
方法訪問到的 parameter2 也只會為 null,從而引發意料之外的 NPE
public final class KotlinMode {
private final String parameter1 = "leavesC";
private final String parameter2;
private final void print() {
String var1 = this.parameter2;
System.out.println(var1);
}
public KotlinMode() {
String var1 = this.parameter1;
System.out.println(var1);
this.print();
this.parameter2 = "伯羽君";
}
}
所以說,init 塊和成員變量之間的聲明順序決定了在構造函數中的初始化順序,我們應該先聲明成員變量再聲明 init 塊,否則就有可能導致 NPE
Gson & data class
來看個小例子,猜猜其運行結果會是怎樣的
UserBean 是一個 dataClass,其 userName 字段被聲明為非 null 類型,而 json 字符串中 userName 對應的值明確就是 null,那用 Gson 到底能不能反序列化成功呢?程序能不能成功運行完以下三個步驟?
data class UserBean(val userName: String, val userAge: Int)
fun main() {
val json = """{"userName":null,"userAge":26}"""
val userBean = Gson().fromJson(json, UserBean::class.java) //第一步
println(userBean) //第二步
printMsg(userBean.userName) //第三步
}
fun printMsg(msg: String) {
}
實際上程序能夠正常運行到第二步,但在執行第三步的時候反而直接報 NPE 異常了
UserBean(userName=null, userAge=26)
Exception in thread "main" java.lang.NullPointerException: Parameter specified as non-null is null: method temp.TestKt.printMsg, parameter msg
at temp.TestKt.printMsg(Test.kt)
at temp.TestKt.main(Test.kt:16)
at temp.TestKt.main(Test.kt)
printMsg
方法接收了參數后實際上什么也沒做,為啥會拋出 NPE ?
將printMsg
反編譯為 Java 方法,可以發現方法內部會對入參進行空校驗,當發現為 null 時就會直接拋出 NPE。這個比較好理解,畢竟 Kotlin 的類型系統會嚴格區分 可 null 和 不可為 null 兩種類型,其區分手段之一就是會自動在我們的代碼里插入一些類型校驗邏輯,即自動加上了非空斷言,當發現不可為 null 的參數傳入了 null 的話就會馬上拋出 NPE,即使我們并沒有使用到該參數
public static final void printMsg(@NotNull String msg) {
Intrinsics.checkNotNullParameter(msg, "msg");
}
那既然 UserBean 中的 userName 字段已經被聲明為非 null 類型了,那么為什么還可以反序列化成功呢?按照我自己的第一直覺,應該在進行反序列的時候就直接拋出異常才對
將 UserBean 反編譯為 Java 代碼后,也可以看到其構造函數中是有對 userName 進行 null 檢查的,當發現為 null 的話會直接拋出 NPE
public final class UserBean {
@NotNull
private final String userName;
private final int userAge;
@NotNull
public final String getUserName() {
return this.userName;
}
public final int getUserAge() {
return this.userAge;
}
public UserBean(@NotNull String userName, int userAge) {
//進行 null 檢查
Intrinsics.checkNotNullParameter(userName, "userName");
super();
this.userName = userName;
this.userAge = userAge;
}
···
}
那 Gson 是怎么繞過 Kotlin 的 null 檢查的呢?
其實,通過查看 Gson 內部源碼,可以知道 Gson 是通過 Unsafe 包來實例化 UserBean 對象的,Unsafe 提供了一個非常規實例化對象的方法:allocateInstance
,該方法提供了通過 Class 對象就可以創建出相應實例的功能,而且不需要調用其構造函數、初始化代碼、JVM 安全檢查等,即使構造函數是 private 的也能通過此方法進行實例化。因此 Gson 實際上并不會調用到 UserBean 的構造函數,相當于繞過了 Kotlin 的 null 檢查,所以即使 userName 值為 null 最終也能夠反序列化成功
[圖片上傳失敗...(image-781bba-1637567285527)]
此問題的出現場景大多是在移動端解析服務端傳來的數據的時候,移動端將數據聲明為非空類型,但服務端給過來的數據卻為 null 值,此時用戶看到的可能就是應用崩潰了……
一方面,我覺得移動端應該對服務端傳來的數據保持不信任的態度,不能覺得對方傳來的數據就一定是符合約定的,為了保證安全需要將數據均聲明為可空類型。另一方面,這也無疑導致移動端需要加上很多多余的判空操作,簡直有點無解 =_=
ARouter & JvmField
在 Java 中,字段和其訪問器的組合被稱作屬性。在 Kotlin 中,屬性是頭等的語言特性,完全替代了字段和訪問器方法。在類中聲明一個屬性和聲明一個變量一樣是使用 val 和 var 關鍵字,兩者在使用上的差異就在于賦值后是否還允許修改,在字節碼上的差異性之一就在于是否會自動生成相應的 setValue
方法
例如,以下的 Kotlin 代碼在反編譯為 Java 代碼后,可以看到兩個屬性的可見性都變為了 private, name 變量會同時包含有getValue
和setValue
方法,而 nickname 變量只有 getValue
方法,這也是我們在 Java 代碼中只能以 kotlinMode.getName()
的方式來訪問 name 變量的原因
class KotlinMode {
var name = "伯羽君"
val nickname = "leavesC"
}
public final class KotlinMode {
@NotNull
private String name = "伯羽君";
@NotNull
private final String nickname = "leavesC";
@NotNull
public final String getName() {
return this.name;
}
public final void setName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.name = var1;
}
@NotNull
public final String getNickname() {
return this.nickname;
}
}
為了不讓 Kotlin 的 var / val 變量自動生成 getValue
或 setValue
方法,達到和在 Java 代碼中聲明公開變量一樣的效果,此時就需要為屬性添加 @JvmField
注解了,添加后就會變為 public 類型的成員變量,且不包含任何 getValue
和 setValue
方法
class KotlinMode {
@JvmField
var name = "伯羽君"
@JvmField
val nickname = "leavesC"
}
public final class KotlinMode {
@JvmField
@NotNull
public String name = "伯羽君";
@JvmField
@NotNull
public final String nickname = "leavesC";
}
@JvmField
的一個使用場景就是在配套使用 ARouter 的時候。我們在使用 ARouter 進行參數自動注入時,就需要為待注入的參數添加 @JvmField
注解,就像以下代碼一樣,不添加的話就會導致編譯失敗
@Route(path = RoutePath.USER_HOME)
class UserHomeActivity : AppCompatActivity() {
@Autowired(name = RoutePath.USER_HOME_PARAMETER_ID)
@JvmField
var userId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_home)
ARouter.getInstance().inject(this)
}
}
那為什么不添加該注解就會導致編譯失敗呢?
其實,ARouter 實現參數自動注入是需要依靠注解處理器生成的輔助文件來實現的,即會生成以下的輔助代碼,當中會以 substitute.userId
、substitute.userName
的形式來調用 Activity 中的兩個參數值,如果不添加 @JvmField
注解,輔助文件就沒法以直接調用變量名的方式來完成注入,自然就會導致編譯失敗了
public class UserHomeActivity$$ARouter$$Autowired implements ISyringe {
private SerializationService serializationService;
@Override
public void inject(Object target) {
serializationService = ARouter.getInstance().navigation(SerializationService.class);
UserHomeActivity substitute = (UserHomeActivity)target;
substitute.userId = substitute.getIntent().getLongExtra("userHomeId", substitute.userId);
}
}
Kotlin 這套為屬性自動生成 getValue
和setValue
方法的機制有一個缺點,就是可能會導致方法數極速膨脹,使得 Android App 的 dex 文件很快就達到最大方法數限制,不得不進行分包處理