1. 程式人生 > >【用 Kotlin 寫 Android】用 Kotlin 寫 Hello World

【用 Kotlin 寫 Android】用 Kotlin 寫 Hello World

寫在前面

這篇文章題目叫“【用 Kotlin 寫 Android】用 Kotlin 寫 Android Hello World”,主要介紹一用 Kotlin 寫出來的 Hello World 究竟與用 Java 寫有什麼區別,並會介紹一些概念和 Kotlin 的具體實現。

技術點分析

一個控制元件定義後,在程式碼中不需要通過 findViewById 來講程式物件和 xml 中佈局繫結起來,而是可以直接使用使用 xml 中控制元件 id 直接操縱控制元件,這個時候會提示引入 import kotlinx.android.synthetic.main.activity_main.* 的依賴,這樣可以說相當方便,去除了那些沒什麼大意義的模板程式碼,減少程式碼量,使結構更清晰。相信你已經看過很多地方介紹 Kotlin 都會說這是它很大的一個優點,但我想說的還包括:如果你在 setContentView(R.layout.activity_main)

中載入了一個佈局檔案,但是你在下面使用佈局檔案中 id 直接去操縱控制元件時,id 寫錯了,寫成了一個在 activity_main 中根本不存在的一個 ID,會不會有問題?答案是會掛掉,可以理解,操作了不屬於自己介面的元素,會報 Crash:Attempt to invoke virtual method * on a null object reference;那如果兩個介面 id 一致,但是在 import 時寫錯包了,會有問題嗎?經試驗,是沒有問題的,這是不是很奇怪?其實這是因為 Android 中,在 R 檔案中將資原始檔都對映成一個 int 整數,儲存在不同 R 檔案的靜態內部類裡面,因此兩個 id 其實在 R 檔案中用一個值表示了。同理,如果其他的一些資原始檔被同樣對映,即使寫錯了,也可能正常執行。PS:不過一般沒有理由把這個寫錯。

我們通過 id 可以找到對應的 View,為什麼在 Java 中就不可以呢?這個 include 就是怎麼完成這個的呢?答案是這個 include 不僅僅是簡單的 include,而是因為在 build.gradle 加入了擴充套件 plugin: kotlin-android-extensions,而這個擴充套件 plugin 其實是會編譯生成一些額外程式碼的,那我們就把編譯出來位元組碼檔案進行反編譯,看看反編譯出來了點什麼:

反編譯 Kotlin 生成的位元組碼

再對比一下原始檔:

原始檔

我們可以看到:setContentView(2131296283),將 2131296283 轉換成十六進位制是 0x7f09001b,在 R 檔案中:

public static final class layout {
  public static final int activity_main=0x7f09001b;
}

同時也不存在直接用 id 直接操作 View,而是也通過 findViewById() 來獲取 View,並且這裡還有一個 HashMap 進行優化,並且 id.my_app_text 也是 R 檔案中的,因此在原始檔中 import 錯檔案也不會報錯:

public View _$_findCachedViewById(int var1) {
   if (this._$_findViewCache == null) {
      this._$_findViewCache = new HashMap();
   }

   View var2 = (View)this._$_findViewCache.get(var1);
   if (var2 == null) {
      var2 = this.findViewById(var1);
      this._$_findViewCache.put(var1, var2);
   }

   return var2;
}

我們還看到 println(testNull?.length) 最後被反編譯出來是:System.out.println(var3);,在 Kotlin 標準庫中 println 的定義是:

/** Prints the given message and newline to the standard output stream. */
@kotlin.internal.InlineOnly
public inline fun println(message: Any?) {
    System.out.println(message)
}

因此也就是一些邊準庫的簡單寫法,在編譯後恢復成了正常 Java 程式碼的寫法。我們接著看反編譯出來的下半部分:

反編譯下班半分截圖

這裡有靜態類 MainActivity.Companion,對應的是 Kotlin 中的伴隨物件,伴隨物件內的變數是所有類共用的屬性,類似於 static 的含義,在反編譯後的程式碼看,也確實是用 static final 物件實現的:public static final MainActivity.Companion Companion = new MainActivity.Companion((DefaultConstructorMarker)null);

我們還注意到:val TAG: String? = MainActivity::class.simpleName 最後被反編譯後的程式碼是:private static final String TAG = Reflection.getOrCreateKotlinClass(MainActivity.class).getSimpleName();,Reflection 是反射的意思,由 Kotlin 類對映到 Java 物件。在寫前一段程式碼後會報警告說需要引入 org.jetbrains.kotlin:kotlin-reflect:1.2.31,這裡我們知道其實了其實它最後用到了 Kotlin 類 Reflection 和方法,不引入包就會報 kotlin.jvm.KotlinReflectionNotSupportedError: Kotlin reflection implementation is not found at runtime. Make sure you have kotlin-reflect.jar in the classpath 的錯誤也就可以理解了。

最後我們看一下:

var testNull: String? = null
println(testNull?.length)

var testNull: String? = "213"
println(testNull?.length)

var testNull: String? = null
if ((System.currentTimeMillis() % 2) == 0L) {
    testNull = "123"
} else {
    testNull = null
}
println(testNull?.length)

最後被編譯成:

String testNull = (String)null;
Object var3 = null;
System.out.println(var3);

String testNull = "213";
Integer var3 = testNull.length();
System.out.println(var3);

String testNull = (String)null;
if (System.currentTimeMillis() % (long)2 == 0L) {
   testNull = "123";
} else {
   testNull = (String)null;
}
Integer var3 = testNull != null ? testNull.length() : null;
System.out.println(var3);

有一定的優化,如果在編譯時就能確定值,直接簡化,否則就是正常轉換,其中 ?. 最後其實就是轉換成了 if != null 的判斷。

還有一個知識點是 Lambdas 表示式其實依舊是還原成最基本的 Java 程式碼,也包含 new OnClickListener

總結一下

到這裡,我們展示了一個最簡單的 Hello World 程式,可以看出因為 Kotlin 也是執行在 JVM 上的,因此也需要符合位元組碼規範,因此反編譯後生成的程式碼就會和直接用 Java 寫有很多相似的部分,只不過直接寫 Kotlin 的時候,Kotlin 外掛或者是庫幫我們完成了一些工作,我們接下來一些東西還會類似去分析。

如果有一天你覺得過的舒服了,你就要小心了!歡迎關注我的公眾號:我是任玉琢

qrcode_for_gh_45402a07f7d9_258