2019年6月27日 星期四

理解 Kotlin 的 Coroutines 的運作行為

參考文章: https://kotlinlang.org/docs/reference/coroutines/basics.html
簡單來說, Kotlin 的 Coroutines 是一個資源共享的程序區塊簡稱窟程序(一段程序走著走著跑到窟窿去了, 後續可以命令它再鑽出來), 它可以將一條執行緒(Thread)分裂(一個 launch 或 async 區塊只會分裂一次)成不同的程序路徑, 經多次分裂(launchs 或是 asyncs)後就像是(但不是)多條執行緒同時運行, 先了解一下 runBlocking {   } 的運作, 顧名思義,它就是一個阻礙式(blocking)函數 λ (lambda), 區塊內如同一般的函式, 等到區塊內的程序走完才能繼續運行下一步, 但在 λ 區塊 { } 作用(scope)下可以建立出同時進行的程序區塊(Coroutine), 也就是利用像是 async {  } 或是 launch { } 去分裂出 Coroutine,  讓原本循序的程式碼一分為二又不阻塞(block)程式繼續運行, launch 建立出程序區塊後會傳回一個在背景運作的工作代號(job), 後續透過方法 join( ) 可以同步確認程序的完成. 它適用於不需回傳結果的程序,或者稱之為射後不理程序區塊, 至於 async 建立的程序區塊則是從發動的 suspend fun 中獲取一個延遲物件 Deferred <T> , 其中的 <T> 視函式(function)的傳回值而定, 後續再透過延遲物件的方法 await( ) 去等待(suspend)回傳輸出結果, 這是 async 與 launch 區塊應用時最大的區別, 每一個 launch 或 async 區塊都是獨立並行運作的, 一經發動就在背景依序運行, Coroutine 區塊內每條程序也就循序的(sequential)運作起來. 我們可以把 runBlocking 看成是循序程式碼與程序區塊間的中介運作橋樑,如果想要在主程序發動 Coroutine, 另外可以用  GlobalScope.launch { } 去建立與主程序同時進行的程序區塊讓它在背景同時運行, 與 runBlocking 不同的是 GlobalScope.launch 直接回傳一個背景代碼就繼續運行而不會阻塞. 一旦產生 GlobalScope.launch 的 job 後, 就有責任需要呼叫其方法 join( ), 等待該 job 同步完成, 否則有可能程序未完成前就被終結掉, 但 join( ) 也只能放在 Coroutine 區塊內(scope)呼叫. 至於 runBlocking 本身就會等待內部 Coroutine 完成, 正如同程式名所言.因此不用呼叫對應的 join( ).以下範例程式可以貼到 https://play.kotlinlang.org  執行  Playground 看看:

import kotlinx.coroutines.*
fun main( ) {
     println("main start")
     val     job = GlobalScope.launch {
             println("Global Job 1 start")
             delay(2000L)   // suspend for 2 seconds
             println("Global Job 1 stop")
     }
    println("main running")
    runBlocking {
            launch {
                   println("Job 2 start")
                   delay(500L)   // suspend for  500ms = 0.5 sec
                   println("Job 2 stop")
            }        
            launch {
                   println("Job 3 start")
                   delay(1000L) // suspend for 1 sec
                   println("Job 3 stop")
            }  
            job.join()  // wait job in GlobalScope.launch to finish
      }
     println("main stop")
}
看輸出結果可以想像: 上述程序建構了(launch) 了 3 個並行的工作, 就好像從主程序分裂出 3 條執行緒同時運行, 主程序走到 runBlocking 會一直等待直到它結束, 最後主程序執行最後一行列印指令終結任務:
main start
main running

Global Job 1 start
Job 2 start
Job 3 start

Job 2 stop
Job 3 stop

Global Job 1 stop
main stop

Coroutine 運作的行為方式很像是(但不是)執行緒(Thread), 每一個 coroutine 都會攜帶 Context (文本), 在此架構下去運作一個輕量級執行緒, 透過執行緒派任工 Dispatchers 將任務(Task  or job)派發至專屬文本的執行緒去運行,換句話說, 他並不一定要產生執行緒, 而是將任務分配給執行緒,定義可供 coroutine 呼叫的函式, 之前要加上 suspend 關鍵字讓 Kotlin compiler 把它轉變成可以暫停或持續運作的函式.  Kotlin 幾個重要的派任工:
1. Dispatchers.Main: 通常是派任至前景執行緒
2. Dispatchers.IO: 通常是派任至背景,需長時間運作的執行緒
3. Dispatchers.Default: 通常是派任至背景, 短時間加速完成的執行緒
4. Dispatchers.Unconfined: 派任至目前呼叫的 Thread, 開始執行一直到第一個暫停點結束.
透過內建 withContext 及派任工, 可以將任務交給專屬執行緒在其所屬文本底下運行, 或是創建一群共同生命期(coutineScope) coroutines, 他既不組塞目前 Thread 運作, 又可以非同步操作函式, 等到裏面全數 coroutine結束才會終結生命:
suspend fun mySuspend( ) =   withContext(Dispatcher.Main) { // dispatch to Thread   
      //  dispatch to main UI thread
}
suspend fun myScope( ) = coroutineScope { // create a group of coroutines to live together
      async {
         // ...
      } 
      launch {

         // ...
      }
} // live until finishing all of coroutines (async or launch)
一個 Coroutine 是一個有生命周期(scope)的任務(Job),透過 coroutineContext[Job]  就可以獲取目前的任務. CoroutineScope 介面只含唯一屬性 coroutineContext,  至於 async 與 launch 是 CoroutineScope 介面的從屬函數, coroutineScope 創建工則是另一個 suspend 的函式, 它根據 coroutineContext 去建立 coroutine 上下父子關係(承上啟下). 參考文章:
         https://medium.com/@elizarov/coroutine-context-and-scope-c8b255d59055:
或許可以這樣理解: coutineScope { } 是 Kotlin 的一群 coroutines 區塊的生命期創建工(builder), 而 async { } 或 launch { } 僅是單一 coroutine 區塊生命期創建工.
Task, Context, Thread 都是作業系統的專有名詞, Task 是一個獨立作業的前景或背景任務簡稱任務, Context 是執行任務時的特定參數資料庫(database)簡稱文本, Thread 則是運行任務時可以走的程序路徑(program path)簡稱執行緒, 當 CPU 支援硬體的 multiThread (多工緒)或是透過軟體摹擬出 Thread pool(工緒池塘, 一整池的執行緒)時就可以將程式碼分配到不同路徑去執行,主要目地是增加運算效率並改善使用者體驗. 過多的 Threads 對記憶體需求將是一大考驗, 但對 Coroutines 卻不是問題, 這是他神奇的地方. 查了網路理解了一下 Coroutine 的運作原理: 首先透過 suspend 函式宣告後, 編譯器(compiler) 將 suspend 函式轉變成狀態機(state machine), 呼叫時再傳一個叫作 Continuation 的物件進入該函式, 之後透過 Continuation 物件去掌控函式暫停(suspend)或重啟(resume)的狀態, 有點類似Javascript yield 的運行方式, 暫停時傳回值並保留運作狀態, 後續回頭重啟時再從暫停的地方繼續執行,其壓根就不是 Thread, 因此可以透過派任的方式讓 Coroutines 在其他 Thread 上運行,最後來看一個例子:

    import kotlin.coroutines.*
    import kotlinx.coroutines.*
    lateinit var continuation1: Continuation < Unit >
    lateinit var continuation2: Continuation < Unit >
    fun main(args: Array <String> ) {
        println("Stat of main")
        GlobalScope.launch(Dispatchers.Unconfined) {
             println("Coroutine Start")   
             suspendCoroutine<Unit> { // it is a Continuation object
                println("Coroutine seq#1 running")
                continuation1 = it   // save the continuation object to be resume later
                println("Coroutine seq#1 suspend") // will suspend here
             }                
             println("Coroutine running") 
             suspendCoroutine<Unit> { // it is a Continuation object
                println("Coroutine seq#2 running")
                continuation2 = it   // save the continuation object to be resume later
                println("Coroutine seq#2 suspend") // will suspend here
             }   
             println("Coroutine stop")
         }
         println("Resume 1 in main")
         continuation1.resume(Unit) // resume through continuation
         println("Resume 2 in main")
         continuation2.resume(Unit) // resume through continuation
         println("End of main")
    }
輸出結果:
Stat of main
Coroutine Start
Coroutine seq#1 running
Coroutine seq#1 suspend
Resume 1 in main
Coroutine running
Coroutine seq#2 running
Coroutine seq#2 suspend
Resume 2 in main
Coroutine stop
End of main

可以看到兩個 suspend 函式放在一個 launch 區塊內時, 他們是接續(sequential)運行, 並非想像中同時(parallel)運作, 若真要同時(運作兩個程序區塊, 那就要把他放在不同的 launch 或是 async 區塊去運行, 像是這樣就可以了:
    import kotlin.coroutines.*
    import kotlinx.coroutines.*
    lateinit var continuation1: Continuation < Unit >
    lateinit var continuation2: Continuation < Unit >
    fun main(args: Array < String > ) {
        println("Stat of main")
        GlobalScope.launch(Dispatchers.Unconfined) {
             println("Coroutine Start") 
             val seq1 = async {
                 suspendCancellableCoroutine < Unit > { // it is a Continuation object
                     println("Coroutine seq#1 running")
                     continuation1 = it   // save the continuation object to be resume later
                     println("Coroutine seq#1 suspend") // will suspend here
                 }
                 "Coroutine seq#1 stop" // return a string
             }
             "Coroutine running"
             val seq2 = async {
                 suspendCancellableCoroutine < Unit > { // it is a Continuation object
                     println("Coroutine seq#2 running")
                     continuation2 = it   // save the continuation object to be resume later
                     println("Coroutine seq#2 suspend") // will suspend here
                 }
                 "Coroutine seq#2 stop" //  return a string
             }             
             println(seq1.await()) // will suspend until seq1 return
             println(seq2.await()) // will suspend until seq2 return
             println("Coroutine stop")
         }
         println("Resume 1 in main")
         continuation1.resume(Unit) // resume through continuation
         println("Resume 2 in main")
         continuation2.resume(Unit) // resume through continuation
         println("End of main")
    }
執行結果:
Stat of main
Coroutine Start
Coroutine running
Coroutine seq#1 running
Coroutine seq#1 suspend
Coroutine seq#2 running
Coroutine seq#2 suspend
Resume 1 in main
Coroutine seq#1 stop
Resume 2 in main
Coroutine seq#2 stop
Coroutine stop
End of main

2019年6月23日 星期日

Android 的多工機制 HandlerThread , 理解 Looper , Handler 協調與分工

參考資料:
1. https://blog.mindorks.com/android-core-looper-handler-and-handlerthread-bd54d69fe91a
2. http://www.mallaudin.com/looper-handler-api-part-4
單純使用 Thread 可以讓程式放在背景去執行, 但 Thread 執行完就消失了. 為了要讓 Thread 重複利用, 分派複雜的工作又不會阻塞程式運行, 首先要了解 Looper 與 Handler 如何分工:  Looper 稱為迴圈訊息搬運工, 他主要建構一個工作訊息串,把要工作的訊息駐列成隊, 當 Thread 裏面無任何工作訊息時, 就暫停進入休眠(剛開始仍需要發動 start( ) 啟動 Thread 開始運作), 一旦駐列有工作訊息就交給 Handler 在背景處理, Handler 可以想像是工頭(迴圈訊息處理工),負責處理程式運作之邏輯, 包含 message 的解釋(sendMessage),  或是處理像是 post 過來的片段程式碼, 若要自行定義含 Looper 的 Thread, 可以利用 Thread 裏面 run( ) 區塊, 去標示一個類似多工環境的程式迴圈, 介於 Looper.prepare( ) 與 Looper.loop( ) 之間, 從而讓 Thread 週而復始不停的運作又不阻塞(nonblock), 除非呼叫 Looper 的方法 quit( ) 打破迴圈, 才能結束 thread loop 讓系統回收資源. Android 內建的 HandlerThread 其實是一個內建了 Looper 的 Thread, 可以搭配後續的 Handler 去執行所要交代的任務:
        val background = HandlerThread("Hi").run {
            start() // HandlerThread.start( ) to run
            object : Handler(looper) { // return Handler for this looper
                override fun handleMessage(msg: Message) { //message handler
                    when(msg.what){ // get mesage
                        0 -> looper.quit() // quit the looper
                        //...
                    }
                }
            }
        }
        background.sendEmptyMessage(0) // send message 0  to HandlerThread

2019年6月18日 星期二

Android 使用 kotlin 語言讀取觸控螢幕訊息

 Android 手機產生觸控動作時,在Activity裏面會觸發 onTouchEvent(event: MotionEvent) 這隻函式, 只要覆寫該程式, 並利用 event 的一些成員就可操作整個觸控螢幕, 為了減少累綴物件程式碼, 可以利用 with(event}{when(actionMasked){ }} 將操控成員放在區域塊(scope)內操作.以下列出 MotionEvent 重要成員:
    pointerCount :螢幕觸控點累積的數量
    actionIndex :觸發動作的索引鍵(index), 0是第 1 個 UP 點, pointerCount - 1 是最後一點
    actionMasked :觸控動作遮罩用來判斷觸控點的動作, 共有以下 6 個工作狀態:
    MotionEvent.ACTION_CANCEL : 觸控動作被取消了
    MotionEvent.ACTION_DOWN :手指按壓觸控螢幕後, 當下 pointerCount==1 時觸發
    MotionEvent.ACTION_POINTER_DOWN :當手指按壓螢幕後, pointerCount>1 時觸發
    MotionEvent.ACTION_UP :手指離開觸控螢幕時, 當下 pointerCount==1 時觸發
    MotionEvent.ACTION_POINTER_UP :當手指離開螢幕時, pointerCount>1 觸發
    MotionEvent.ACTION_MOVE:只要有手指在螢幕上移動就觸發,此時actionIndex固定是 0,似乎是固定追蹤第一個 UP 的觸控點, 當pointerCount > 1 時, 額外觸控點座標就要限縮索引值後再去讀取:  for(p in 1 until pointerCount) {  getPointerId(p); getX(p) ; getY(p); ... } 而每個觸控點 id 透過 getPointerId(索引值) 加以區分, 其他常用的成員函數還有:
    getX(index):  取得索引觸控點的X(橫)軸座標值
    getY(index):  取得索引觸發點的Y(縱)軸座標值
    getPointerId(index): 取得索引觸發點的唯一序號(id)
    findPointerIndex(id): 取得觸發點唯一序號所對應的索引鍵
// Kotlin 程式碼: activity.kt    ...
override fun onTouchEvent(event: MotionEvent): Boolean {
    return with(event){
         val index =  actionIndex
         val id = getPointerId(index)
         var motion  = "(" + getX(index).toInt() + "," + getY(index).toInt() + ")#" + id +"#\n"
         motion += "[index=" + index + "]/"+ pointerCount+ "\n"
         when(actionMasked) {
                MotionEvent.ACTION_CANCEL -> motion +=":CANCEL"
                MotionEvent.ACTION_DOWN -> motion +=":DOWN"
                 MotionEvent.ACTION_POINTER_DOWN -> motion +=":POINT DOWN"
                MotionEvent.ACTION_UP -> motion +=":UP"
                MotionEvent.ACTION_POINTER_UP -> motion +=":POINT UP"
                MotionEvent.ACTION_MOVE -> {
                    motion +=":MOVE\n"
                    for(x in 1 until pointerCount)  motion += "(" + getX(x).toInt() + "," + getY(x).toInt() + ")" +"#" + getPointerId(x) +"#\n"                   
                }
         }
         Toast.makeText(applicationContext, motion, LENGTH_SHORT).show()
          return true
    }
}

2019年6月17日 星期一

理解 Android app 的 activity 與 context

參考文章
https://android-developers.googleblog.com/2009/01/avoiding-memory-leaks.html
一個 Android app 通常有兩種 context (用來承先啟後的上下文本): application context 及activity context, 只要 app 有任何(幕前或幕後的)Activity(活動)正在運作 applicationConext 都會存在, 但 activityContext 只存在當下(幕前)的活動, 一旦 app 被切換至幕後時, 例如翻轉螢幕時, 幕前的活動(activity)就會暫停(onPause), 接著 activityContext 就消失了, 系統要再重新喚醒(onResume)復出另一個幕前的 Activity, 因此活動暫停前, 一些在 Activity 的重要參數必須先保存, 當然 app 可以在 Activity 程式裏面動態分配空間來儲存資源並指定給變數參照, 一旦動態分配了資源, 後續資源回收機制(GC)若沒有適當釋放, 一旦活動結束, 參照的變數跟著消失, 就會造成記憶體洩漏危機(memory leak: 無法回收的資源), 萬一動態分配的空間夠大, 只要將手機多翻轉幾次, 很快的系統記憶體就不夠用了

2019年6月14日 星期五

Kotlin 的 try catch, if else, when( ) { else } 等區塊敘述與函數

例外處理一直是很煩瑣的任務, 要讓程式碼看起來簡潔易懂, 可以利用 kotlin 的 try-catch陳述函數, 也可以搭配 if else, when 等條件陳述函數去處理, 只要把結果擺在區塊內最後一行就可以回傳, 而且回傳的資料型態必須與描述宣告的(statement)一致, 例如 try expression(函數):
  val a: Boolean = try { TaskMayThrowException() ;     true } catch (e: typeName) { false }
意思是說: 嘗試執行任務, 正常執行最後要回傳 true, 否則當產生不可預期事件時回傳 false, 詳情參考: https://kotlinlang.org/docs/reference/exceptions.html
要注意的是:
1. 必需用雙括號將程式碼分別圈出 try 與 catch 的程式區塊
2. 要接收的變數或常數, 要宣告資料型態(包含 catch 的接收參數), 主要是讓回傳資料型態一致
3. 無論有無發生例外, 都會執行 finally { } 程式區塊, 但 finally 區塊並不會回傳最後敘述值
4. try 區塊必須跟隨至少一個 catch 區塊, 或是接續 finally 區塊(可以省略 catch 區塊)處理
5. 描述宣告(statement)不會取值, 但陳述函數(expression)會取最後一行結果當函數值
把 if { }  else { }, when(condition ) {  else { }   } 等當成陳述函, 詳情參考:
https://kotlinlang.org/docs/tutorials/kotlin-for-py/conditionals.html

Kotlin 的 run, apply, let, also, with

參考文章: https://kotlinlang.org/docs/reference/scope-functions.html
要分辨 run, apply, let, also 等擴展函式的用法, 首先要明瞭 λ(lambda) 的區塊表達式:
      { parameters  ->  retval }
或是參考之前的文章:
      http://amitmason.blogspot.com/2018/08/kotlin-function.html         
其中箭頭前面是: 分別用逗號分開的輸入參數,箭頭之後的最後一行是函式的傳回值(簡稱函數),當數入參數只有一個時就可以使用 it 代表唯一參數, 並且可以省略箭頭符號與參數的宣告,就只剩下:
           {   ... ; retval }
物件產生後, 內建的 run { } 及 let  {  } 就是屬於這種型態的 λ, 兩者差別是 let 會將物件本身當唯一輸入參數也就是 it, 但 run 完全無輸入參數, 因此不能在 run 區塊裏面使用 it, 兩者相同的是: 將最後一行結果當作函數值傳回. 至於 also 同樣會將物件本身當唯一參數 it, 但不管最後一行的結果如何, also 的函數傳回值還是物件本身, apply 與 also 類似 , 函數傳回值也是物件本身,無輸入參數 it,  這些 run, let, apply, also 函式共通點是: 所有在區域(scope)內的從屬函式(member function)不需要引用物件句號, kotlin 自動會引用物件, 因此可以減少不少程式代碼, 下表就是根據 λ 的輸入與輸出列出函式差異, X = no,  V = Yes
                            it=物件輸入     函數輸出=物件本身
           run           X                       X                                   .run { member( );  retval   }
           let            V                       X                                   .let   { member( );  it.  ; ... ;  retval  }
           apply       X                       V                                  .apply{  member( );       }.apply{ ... }
           also          V                       V                                  .also { member( );  it. ; ...  }.also{ ...   }
由於 apply 與 also 函數傳回值就是物件本身, 因此可以不斷的串下去, 至於 run 與 let 傳回值是函式最後一行來指定, 因此適用於轉換不同物件時(transform)使用
說到 run 與 let 實際上的應用,以從屬函式(member function)的角度來看, let 有了 it 的加持後,就可從物件本身為起點全面展開, 但 run 只能從所屬函式為起點來展開:
val ret1 = objectTo.run { member1( ) ; member2( ); ... ;  ret_val }
val ret2 = objectTo.let   { 
           val some_ret = it.let { member1( );  member2( ); ... ; retval } 
           ...  Transform(it)
           ret_val
    } 
同樣觀點也適用於分辨 apply 與 also (帶有 it 物件本身)運用上的差異. 他們與 run 和 let 差異只在傳回物件的不同,  run 與 let 可以自訂傳回值, apply 與 aslo 傳回物已經內定是物件本身, 因此程式碼裏面沒有傳回值:
   objectTo.apply { member1( ) ; member2( ); ... }
   objectTo.also {
           it.also { member1( );  member2( ); ... }
           ...  Transform(it)           
    }
而且 run, let, aslo, apply 可以互相穿插與搭配, 從單純需求(輸入參數 it , 輸出本身物件 this)的角度來看:
1. 不用再處理物件本身(it), 單純用 run 或 apply 就夠了, 之後若要傳回計算值(函數), 就用 run,  否則用 apply
2. 不用轉換物件, 最常用到的是 apply 或 also, 若又要處理物件本身(it)時就用 also
3. 若還需要處理物件本身並轉換成不同物件就只能用帶輸入參數(it)的 let 
記憶口訣: let it be and run return, also it apply this. 意思是區分 let, run 和 also, apply 為兩組擴展函數, 只有 let run 可以自訂 return, 而 let 與 also 可以接 it, also apply 最後傳自我. 最後一個 with 其實跟 run 差不多, 差異在語法的使用, with 是 kotlin 的語法, 但 run 是擴展函數 λ( ), 操作  with(obj) { ... } 的結果就如同  obj.run { ... }  同樣都會把區塊最後一行的結果當作函數