I/O’19 Kotlin 了解幕後那一兩件事

Jast Lai
Jastzeonic
Published in
29 min readJun 1, 2019

本文章參照自這影片:

有興趣了解更多,可以直接看這影片。

這個 Session 可能是因為要講的內容有點生硬,可能是因為要讓生硬的演講活潑點,然後開場台上兩人各種相聲ww。

其實看到這影片一開頭就想寫這篇了,不過這邊我估計這篇會寫上一段時間了,因為這片的內容其實很硬,而且我會傾向自己去嘗試,而不是直接取影片的內容,這又更花時間了。不過如果有讀到這段話的話,代表我有把這篇寫出來惹。

Kotlin Byte code

首先先介紹工具

Kotlin 是可以跑在 JVM 上頭的靜態語言(同時也可以被編譯成 JavaScript ,非本篇主題且不提),意思就是 Kotlin 會在最後會被編譯成跑在 JVM 的 byte Code 。

那 byte code 是可以被反編譯回去成 Java code 的,雖然會跟原先寫的 Code 有點出入,那同理編譯成 JVM byte code 的 Kotlin 也可以反編譯成 Java Code (不過能不能反編譯回 Kotlin 我就不知道了),利用這點特性,可以理解 Kotlin 跑在 JVM 上的一些邏輯。

有用 Android Studio 寫 Kotlin 的人對這東西有可能陌生有可能不陌生,不過我畢竟是 Java 入門 Android 的,回頭過來看 Java 也寫了數年,經驗相對於只摸了兩年的 Kotlin ,Java 對我還是比較熟悉些,某一些 Kotlin 語法我仍然會好奇它會怎麼樣在 JVM 上頭跑,這時候就會用到這些工具了。

舉例來說,我覺得一個很有趣的部分。

int 是個原生型別(Primitive type),這大家都知道

在 Kotlin 中宣告

var arg1:Int = 1

Kotlin type 會是 Int ,Java type 則會是 int 。

那如果在 Kotlin 宣告是:

var arg2:Int? = 1

Kotlin type 會是 nullable 的 int ,而 Java type 則會變成 Integer,值得注意的是這樣便不再是原生型別了。

那我們要怎麼知道這件事呢?看 byte code。

Android Studio 提供了一個工具叫做 Kotlin Bytecode 。

他可以稍微看一下 Kotlin 會被編譯成甚麼樣的東西。

看這著些 Byte Code ,我回想起以前寫組語那些時候的美好日子

以上面的例子來說:

可以看到兩者在 assign 上的差異…。

嘛,畢竟 byte code 讀起來很花時間,而且看了就頭痛(翻譯:不要叫我解釋上面那兩張圖的差異,這要打很多字),但畢竟也不用這麼勉強自己看 byte code ,Koltin Byte cdoe 提供了一個 Decompile 的按鈕可以按:

按了就可以看到反編譯後的 Java Code :

雖然感覺很像在反編譯 apk ,但至少比原先 byte code 好看很多了。

上面的例子,直接看 decompiled 的 java code 很快就能明白:

Memory Profiler

此外還有一個東西,叫做 Memory Profiler ,這東西其實也出在 Android Studio 上幾年了,是拿來抓漏(Memory leak)的神器。

如果在下面沒有看到 Profiler ,可以在 View 的 Tool Windows 裡找到

打開之後可以看到這裡可以得到很多關於裝置的資訊:

包括 CPU 使用量,Memory 的使用量,網路的使用量,還有電池的使用量(因為我開模擬器的版本比較舊一點,所以不支援)

那在這邊舉個例子:

var arg2:Int? = null


fun setNullableInt(){
for(i in 0..10000){
arg2 = i
}
}

因為 nullable 的關係,所以使用的會是 Integer 而非原生型別 int,預計在 for loop 的瞬間應該會產生 10001 個 Integer 的 instance 。

這裡就可以使用 Profiler 來驗證了:

利用 Profiler 抓明顯 memoy 變化的區段來看看

奇怪的是,預期應該會在 for loop 的瞬間產生 10001 個 instance,但是實際上卻只有產生 9873 個 instance,這是為什麼呢?

這時可以按一下該 integer 的型別可以看到它的 instance ,這時再按一下該 instance 可以看到它的 call stack,可以知道它具體是怎麼產生的。

在這裡我們可以確定它最後是跟 Integer 的 valueOf 有關的,直接點擊這個 valueOf 這個 method,可以知道它是 Integer 的一個 static method,追下去最後可以得到這段 Code

public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

在這裡我們知道了 IntegerCache 這個東西,嘛,因為 Integer 這個 class 知道大部分的 Application 都會需要 Integer,而每次都要產生一個 instance 實在有點浪費,所以他設定了一個範圍,範圍是 -128 到 127 之間,這個範圍的 integer 就存放在 IntegerCatch 裏頭,有使用到的話就直接從 catch 拿出來。

這也是為什麼 instance 只有 9873 的理由,因為其中有 128 (0-128) 個是從 catch 拿出來的不會產生新的 instance,10001–128 = 9873 ,所以只有產生 9873 個 instance。

Enum

來了,來了,大家都愛的偶像 Enums 。我寫 Kotlin 的時候愛死他了,最主要的原因是在定義一些 status 時,會使用到 when ,而 when 使用 enum 去做判斷時,會強迫開發者一定要把所有的 enum case 都列入(或者是用 else),否則會 compiler error,這樣最具體的好處是,當你新增或移除 enum 的 status 時,你一定會知道,哪邊被影響到哪邊要做修改(除非你用 else),可以避免新增東西在某處忘記加的情況(除非你用 else)。

不過如果跟我一樣是從 Android 2.0 時代開發至今的 Android 開發者,應該都會依稀有一些印象是,Android 當中不推薦使用 enum ,而是最好使用 Integer 常數來取代,那時候在 Android Developer 的 Manage your app’s memory 這個條目可以看到一行說法關於 enum 的

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

意思是,Enum 相對於靜態常數而言,通常會使用兩倍以上的記憶體,所以在 Android 上應該嚴格避免使用 Enum。

(然而這個條目現在應該已經找不到了)

兩倍記憶體,這具體是啥概念呢?這邊簡單解釋一下:

enum class Statement {
Create,
Start,
Resume,
Processing,
Pause,
Stop,
Destroy
}

這邊設定了一個 enum ,代表各式各樣的 Statement。

fun method1() {

val statement: Statement = Statement.Create

when (statument) {
Statement.Create -> TODO()
Statement.Start -> TODO()
Statement.Resume -> TODO()
Statement.Processing -> TODO()
Statement.Pause -> TODO()
Statement.Stop -> TODO()
Statement.Destroy -> TODO()
}
}

而這邊使用 When ,可以針對各種 Statement 做處理,那在 Java 中與之對應的當然就是 Switch 了,那看起來就是這樣:

可以看到這邊 Java Code 利用 $EnumSwitchMapping$0 這個 Array,並利用該 enum 的 instance 的 ordinal(順序數)去取對應的常數。

咦?這 Array 哪來的?

Compiler 產的阿,實際那看起來就是這樣:

可以看到 enum 的順序對應到其儲存的數字,Switch case 便可以利用該數字做對應的判斷,偷偷幫我產一個 Array 就算了,這損失也不大…。

你以為就只有這樣嗎?太甜了!!

這裡再加一個 method2

fun method2() {

val statument: Statument = Statument.Create

when (statument) {

Statument.Start -> TODO()
Statument.Resume -> TODO()
Statument.Processing -> TODO()
Statument.Pause -> TODO()
Statument.Stop -> TODO()

Statument.Create -> TODO()
Statument.Destroy -> TODO()
}
}

再看看那個 Mapping 表。

可以看到又多了一個 Array ,是針對 Method 2 產生的,那假設有 Method 3 Method 4 呢?

誠如預期,有多少 method 使用到這個 enum ,就會需要產生多少 Array 來做相對應的判斷,顯然針對個個 method 都有可能不同的特性,這麼做是需要的,否則在修改一個 method 後,無法確保其他使用到這個 Array 的 method 不會被影響到。

這個 hidden cost 顯得有點恐怖,但是,不過就只是幾個 integer array 而已嘛?大多時候有記憶體問題時,省掉這幾個可能不到 1 kilobyte 的 array 顯然治標不治本,有記憶問題多半會是其他 instance ,那應該關注的焦點不再 enum 上,而在真正占用記憶體的東西上。

那為了省這幾個 byte 的記憶體,結果要花更多的時間去寫更多的 Code 或花更多的時間防範更動的錯誤,這究竟值不值得,這得交由開發者來考慮了。

不值得,就用吧,使用 enum。

Lazy Properties

val arg1: Int = 543

我這有個常數,叫 arg1 ,內容是 543 ,不重要,知道有他就好,但我希望當有人使用到他的時後才被 init。

作法如下:

private var _arg1: Int? = null
val arg1: Int
get() {
if (_arg1 == null) {
_arg1 = 543
}
return _arg1!!
}

那 java code 則會長這樣:

這個做過很多次了,類似的情況還有使用 singleton 的時候,不過在 Kotlin 還有更省事的作法:

val arg1: Int by lazy { 543 }

這樣 arg1 在有人用到他的時候,才會被 init 了,那這怎麼辦到的呢?可以看一下 java code:

這一段其實有北爛到,我看到那個 $$delegatedProperties 時一整個問號:幹,這啥?咒文?

況且根本沒有用到為啥要建立他呢?

事實上,除了直接 access instance 的 field 去取得需要的資料以外,還有一招我和我周遭的人都喜歡稱他為邪淫巧計的東西 Reflection,也就是我們常說的反射。

作法大概類似下邊這樣:

val demo: Any = DemoClass()

val fields = demo::class.memberProperties

for (field in fields) {
if (field.name == "arg1") {
field as KProperty1<Any, *>
field.isAccessible = true
println(field.get(demo))
}
}

這個用法多下流暫且不提,要知道是 lazy 是 Kotlin 才有的語法糖,Java 是沒有這種概念,這樣就產生了一個問題,這個 arg1 會在呼叫他的時候被 init,恩顯然,要怎麼反射出 byte code 裡沒有的功能,自然就是 Compiler 會把需要的資訊加進去。

所以 Kotlin 建了一個 class 叫 K Property,那如果你用了反射,就可以藉此取得所需的額外資訊。

總之,在使用 by lazy 的時候,他會在該 class 建立一個 KProperty 的 Array,在這個 K Property 裏頭,有對應當該變數被呼叫時所需要的名稱、建立的資訊、類別等。

然後在建構子則會有一個叫 arg1$delegete 被 assign LazyKt.lazy(…)。

那當這個 arg1 被呼叫時,就會很自然地呼叫 lazy ,所以我們要看 lazy 裏頭具體長怎樣:

總之大概就是檢查有沒有被 init,沒有就 init 這樣。

unsigned number

Kotlin 在 1.3 版時加入了無號數。

這邊簡單講一下有號數、無號數的定義,簡而言之,有號數就是可以有負數,無號數就是沒有負數,差別在於無號數會比有號數的範圍多一個次方,比如說 int 有號數是 2³¹,那無號數的範圍是 2³²

當吃香蕉的程序猿也好段時間了,其實沒怎麼用到無號數這東西,畢竟沒有甚麼問題是 int 不能解決的,如果有,那就 long(被拖走)。

Kotlin 用無號數很簡單,直接在數字後面加 u 就好了。

val a = 1u
val b = 43

println(a)
println(b)

可以看到 Java 這邊用了 Uint 去裝無號數,這樣應該可以讓這個 int 超越 2³¹領域。

Ranges

從 Java 跳過來 Kotlin 除了一些保留字用法的改變以外,最不能適應的大概是迴圈的用法了,不過其實用法差異不大

//Kotlin
for (i in 0..10) { }
// to Java
int var1 = 0;
byte var2;
for(var2 = 10; var1 <= var2; ++var1) {}
//Kotlin
for (index in 0 until 10) { }
// to Java
var1 = 0;
byte var2;
for(var2 = 10; var1 < var2; ++var1) {}
//Kotlin
repeat(10) {}
// to Java
byte var1 = 10;
int var7 = 0;
for(byte var4 = var1; var7 < var4; ++var7) {}

差異比較大的是使用 foreach():

//Kotlin
(0..10).forEach { _ ->}
//to Java
byte var1 = 0;
Iterable $this$forEach$iv = (Iterable)(new IntRange(var1, 10));
int $i$f$forEach = false;

boolean var6;
for(Iterator var3 = $this$forEach$iv.iterator(); var3.hasNext(); var6 = false) {
int element$iv = ((IntIterator)var3).nextInt();
}

可以看到 java 的 code 使用了迭代,因為 forEach 裡頭可能會需要迭代的不只是 int 而已。

比較有意思的是,過往我們迴圈使用三步驟,第三個加疊部分可能不只加一,有時可能 +2,有時可能 x2 :

以 +2 來說,Kotlin 的寫法是這樣:

for(i in 0..10 step 2){
println(i)
}

本來想說應該會跟上頭類似應該短短幾行,但事實上 Byte code 轉回來會變成這樣:

其實可以看作是把 for loop 拆開當成 while loop 的樣子,前面先判斷範圍,後面確定後才開始真的運算這樣。

Inline classes

這個屬性我覺得翻成中文反而難懂,排隊集合,這四個字集在其他地方我看得懂,但用在這我看不懂啊。

Inline classes 的目的其實可以這樣去想。舉例來說,我這有一個時間:

val value = 1

這是甚麼呢?它或許是一秒鐘,或許是一分鐘,或許是一小時,總之得給它的定義,那我這邊定義它為一分鐘好了。

val minute = 1

但這時候我們可能在某處需要它換算為秒、換算為毫秒,甚至有地方要我把它換算為小時..。

val minute = 1
val second = minute * 60
val millisecond = second * 1000
val hour = minute/60
val day = hour/24

這看起來一副就很容易出錯的樣子,而且顯然每個地方都要這樣乘 60 ,除 60 的實在有點麻煩,那現代工程師很有可能最直覺想到的解決方法就是 — 封裝:

class Time(val minute:Int = 1){

fun getSecond():Int{
return minute * 60

}
fun getMillisecond():Int{
return getSecond() * 1000
}

//....
//ignore

}

然而在每次一有新的時間,在不同的地方都得建立一個實體才能得到自己想要的資訊,雖然現代電腦設備記憶體之大,這些實體真的對 memory 的 heap 來說實在微不足道,但是心理總有種不踏實的感覺 — 有沒有方法可以讓減少這些實體、減少 heap 的使用呢?

Kotlin 提供了 inline classes 來解決這個問題,用法很簡單,前面加上 inline classes 即可。

inline class Time(val minute:Int = 1){


fun getSecond():Int{
return minute * 60

}
fun getMillionSecond():Int{
return getSecond() * 1000
}

//....
//ignore

}

那 inline classes 是甚麼秘密,可以嫖了不付錢(?),這時候看看 Byte code 就可以知道

舉例來說:

val time1 = Time(1)
val timeSecond = time1.getSecond()
println(time1.getSecond())

換作 ByteCode 會寫成這樣:

其實就是把這個 inline classes 裡封裝的方法給弄成 static ,所以嚴格說起來也不是嫖了不付錢,應該是一開始講好付多少無限次數嫖這樣。

#゚Å゚)⊂彡☆))゚Д゚)・∵

好啦這樣講可能會有誤會,但如果細看會發現一件事情,儘管在 code 有實作了一個 inline class 的 instance ,但是在 Byte code 裏頭卻沒有這個 instance ,只有得到結果的 integer ,compiler 直接重編了這段 code ,省去了 instance 的動作而是直接算出結果了,少了這個 instance 也少用了 heap , 也就是說 Compiler 使用了緋紅之王的能力,把過程消去最後只留下結果。

(。益。)⊂彡☆))д`)

然後我在想,這樣直接用 static 不就好了嗎?

Emmm….只是直接用 static method 就沒有封裝的感覺了阿(咦?)

概念上還是有點出入,因為 static 只要傳入參數就能用,意味著它就是個公車 method ,在使用上不用經歷過甚麼代價直接 import 就能用,但是用 inline classes 卻還是經過封裝的動作,這樣在職責區分上就會清楚許多,那語法上的呈現到維護階段就會有很多不同的發展了。

但這邊有個有趣的地方是,既然它都是 static ,那它是不是 singleton?如果我這樣寫:

val time1 = Time(1)
val time2 = Time(2)

print(time1 == time2)

那是不是會回傳 true?

答案是 false,這邊用 decompiler 會比較好理解:

可以看到他從 Time.box-impl 裡頭取出了是 time1 的數值,接著再從 Time.box-impl 取出 time2 的數值,在用 areEqual 比對,這兩個取出來的東西應該就是原生型別的 1 和 2 ,兩者顯然是不同的所以會回傳 false。

Arrays

var intArray = intArrayOf(1, 2, 3)

var arrayFromArrayOf = arrayOf(null, 1, 2, 3)

var arrayFromIntArray = IntArray(3) {
it
}

這三個究竟有什麼差別呢?

intArrayOf 和 arrayOf 的差異其實還蠻好區別的,就是原生型別和 object 的差異:

所以第二項可以寫成 nullable,第三個 IntArray(size:Int) 呢?

顯然為了去算出 Size 會複雜得多。

Lambdas

這裡介紹了一個 Kotlin 使用 Java interface 的情況:

public class Widget {

private ArrayList<Listener> list = new ArrayList<>();

public interface Listener {
void onEvnet(@NotNull Widget widget);
}

public int getListenerCount() {
return list.size();
}

public void addListener(@NotNull Listener listener) {
list.add(listener);
}

public void removeListener(@NotNull Listener listener) {
list.remove(listener);
}
}

這裡有一個 Java Widget ,他有一個類似 observable 的 Listener,那在 Kotlin 會寫作這樣:

val w = Widget()

val listener = { widget: Widget -> println("Listened to $widget") }

w.addListener(listener)

w.removeListener(listener)

println(w.listenerCount)

那這裡我預期最後會印出 0 ,但答非所望,結果會印出 1。

怪怪der,removeListener 沒有作用。

原因其實可以從 decompiler 的 code 看出來:

理由是,這裡用 lambda 建立出了一個 Function1 ,但是對 compiler 來說 , Function1 並不等同於 Listener ,為了解決這個問題,所以 compiler 會用建立一個新的 instance 叫 Widget_Listener (前面的字根據使用的地方和 Package name 而不同)(而該 Object 直接 implement Listener) 的 Object 去包裝,最後 add 進去。

那 remove 原則上做了相同的事情,顯然 compiler 為此又建立了一個新的 Listener,但此 instance 非彼 instance ,自然 remove 不掉。

依據狀況的不同,這可能會造成 memory leak。

那要避免這個問題,其實很簡單,標示清楚該 lambda 為何物就好,不要使用 naked lambda form (突然覺得這個稱呼好刺激)就好:

val w = Widget()

val listener = Widget.Listener{ widget: Widget -> println("Listened to $widget") }

w.addListener(listener)

w.removeListener(listener)

println(w.listenerCount)

只是這樣寫好醜…

Extension Functions and Subclasses

這裡做一個有趣的實驗

open class Superclass

class Subclass: Superclass()

fun Superclass.getIdentifier() = "super!"
fun Subclass.getIdentifier() = "sub!"

這裡有一個 Superclass 然後有一個繼承 Superclass 的 Subclass。

val superInstance = Superclass()
val subInstance = Subclass()
val subAsSuperInstance : Superclass = Subclass()

那這裡做了三個實體,一個 Superclass 的實體,一個 Subclass 的實體,另一個是 Subclass 被 cast 成 Superclass 的實體。

val superVal = superInstance.getIdentifier()

superVal 會是什麼?”super!”

val subVal = subInstance .getIdentifier()

subVal 會是什麼?”sub!”

val subAsSuperInstanceVal = subAsSuperInstance.getIdentifier()

subAsSuperInstanceVal 會是什麼?

你以為會是 “sub!”嗎?其實是我 “super!” 噠!

val subAsSuperVal = (subInstance as Superclass).getIdentifier()

那其實寫作這樣同上,也會是 “super!”

理由是什麼?其實可以看 decompiler 的 code。

Extension 其實就是 static method 的語法糖, static method 用起來很方便,但容易變成公車 method ,一個懷孕…更正,一個修改就各種影響。

根據這點其實可以知道,這邊是根據他定義的型別去使用它的 extension ,而並非其實際建立的實體為何,所以會有誤以為是 JOJO ,其實是我 Dio 噠的狀態。

Default Parameters

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

這樣 decompiler 的 code 會是這樣:

這沒問題,那如果加上 default parameter 呢?

fun adder(a: Int = 0, b: Int = 1): Int {
return a + b
}

會變成這樣:

會發現多了一個 method 叫 adder$default ,那其實可以發現 compiler 用一個 int 去針對該 int 的位元用 AND 去做位元運算,只要 AND 出來不等於 0 就是跑 default value 。

舉例說,這個 method 如果什麼都沒給 var3 就會給 3 ,3 的二進位是 11 ,1 的二進位則為 01 ,2 的二進位則為 10 ,那兩個 AND 出來分別為 1 和 2 都不等於零,意思就兩個位數都要給預設值。

假設給了 a=27 ,那 var3 就會給 2 ,2 的二進位是 10 ,其中第一位等於 01 & 10 出來會是 0 ,10 與 01 AND 出來等於 1 ,所以第二個 var 需要給予 default 。

那 int 有 32 位元,顯然第一個 parameter 是第一個位元,第二個 parameter 是第二個位元,那看起來最多可以塞上 32 個 parameter ,有趣的是,這樣 default parameter 的限制是 32 個嗎?如果我塞到 33 個會發生什麼事?

fun adder(
a: Int = 1, b: Int = 1, c: Int = 1, d: Int = 1,
e: Int = 1, f: Int = 1, g: Int = 1, h: Int = 1,
i: Int = 1, j: Int = 1, k: Int = 1, l: Int = 1,
m: Int = 1, n: Int = 1, o: Int = 1, p: Int = 1,
q: Int = 1, r: Int = 1, s: Int = 1, t: Int = 1,
u: Int = 1, v: Int = 1, w: Int = 1, x: Int = 1,
y: Int = 1, z: Int = 1, aa: Int = 1, ab: Int = 1,
ac: Int = 1, ad: Int = 1, ae: Int = 1, af: Int = 1,
ag: Int = 1
): Int {
return a + b
}

(影片裡有特別屬咐沒事不要這麼做ww)這裡塞了 33 的 parameter。然後我們看看 decompiler 的 code 唄。

太長了取片段,可以看到除了原本應該有地 34 個 int 以外,又有了第 35 個 int 。

顯然超過 32 個 parameter 則會用兩個 int 去算。

沒有一個 int 不能解決的事情,如果有,那就兩個。

Coroutines

終於來到了最後一個段落了。寫到這裡距離我開文章寫好標題至今已經過快三個星期了

影片中因為有針對 Coroutine 開另外一個 Session ,所以在這個影片裡面只是簡單帶過,那我這篇文章也是ww。

其實我總覺得 Kotlin 的 Coroutine 呈現的很跳痛,有機會的話,我再拿另一個 Session 來細聊 Coroutine 的概念唄。

那這裡是一個簡單的 Sample Code:

suspend fun compute() {
println("Compute (suspend)")
delay(1_000)
}

一個叫 compute 的 Coroutine method

然後我在 unit test 這樣跑它

println("Launching coroutine")

GlobalScope.launch{
compute()
compute()
println("Exiting coroutine")

}

Thread.sleep(3_000)

結果會是:

Launching coroutine
Compute (suspend)
Compute (suspend)
Exiting coroutine

到這裡我發現如果我 decompiler 我 coroutine 的 code 會讓 Android Studio OOM (What’s happening?!!),後來重開後我再試著 decompiler 一次這些 code ,最後發現 build 出了一個長達一萬四千行的 code ,然後 IDE 說他只能印出極限值 3 MB 的 Code ,而這個 code 約 12 MB 大,總覺得這裡應該有 Bug 。

原則上 Kotlin 的 Coroutine 全會實作成一個 state machine。state machine 其實就是一個有限狀態的模型,那本質上就是在這些有限的狀態來回移動。

所以如果看看 decompiler 的 code 會看起來像這樣..

雖然有一部分因為檔案太大被碼掉,但是仍然可以看出一二,。

invokeSuspend 這個 method 原則上會在 suspend method 被呼叫到時被調用,代表狀態的轉換,那可以看到開頭用一個 static method 見了一個 var5 (其實就是代表 suspend),然後裡面主要是一個 Switch case 在跑,switch 判斷的是 label ,此時 label 尚未被 assign ,所以固然是 0 因此進了 switch case 的 0 ,然後將 label assign 為 1 代表下一個要跑的地方,然後他跑了 compute ,得到了 susppend 離開了這個 state machine,等待下一次被調用(以這個例子來說會是馬上)。

再次被調用時,則會走到 case 1 ,算是通知第一次執行的結果(因為這裡沒回 result,所以會丟 throwOnFailure ,但並不影響什麼),之後會跑到 switch 外,把 label 設為 2 ,並執行第二次 compute

用印出來的結果來表達,執行的步驟是這樣:

Launching coroutine
//Entering coroutine state machine
//調用 invokeSuspend
//label = 0
Compute (suspend)
//Entering coroutine state machine
//調用 invokeSuspend
//label = 1
Compute (suspend)
//Entering coroutine state machine
//調用 invokeSuspend
//label = 2
Exiting coroutine

結語

除了 corotine 的 decompiler 我懷疑有 bug 以外,其他的東西都順利解析出來了,這篇我也寫了兩個星期以上,一天一點一點的去寫。

其實研究 Kotlin 背後發生什麼事情還蠻有趣的,很多時候會有意想不到的事情,總是會有那一些看起來很厲害很像魔術的玩意,說穿了就沒啥了不起的東西,但反過來想想,設計的人怎麼會想到這些呢?

那寫這篇文章的初衷除了當作自己閱讀學習的筆記外,另一個初衷跟 Google 的那個 Session 一樣,希望能夠幫助讀的人了解一下自己使用 Kotlin 背後到底是在做什麼,平常使用 Kotlin 其實也不用煩惱太多,大約知道他怎麼去動的、多少了解一下他背後怎麼運作的,肯定是會對自己 code 有幫助的。

如果有任何問題,或是看到寫錯字,請不要吝嗇對我發問或對我糾正,您的回覆和迴響會是我邊緣人我寫文章最大的動力。

(回過頭來看這篇我寫了近兩千字…)

--

--

Jast Lai
Jastzeonic

A senior who happened to be an Android engineer.