1. 協程
github地址:kotlinx.coroutines(https://github.com/kotlin/kotlinx.coroutines)
fun main(args: Array<String>) {
launch(CommonPool) {
delay(1000L)
println("World!")
}
println("Hello,")
Thread.sleep(2000L)
}
/*
運行結果: ("Hello,"會立即被打印, 1000毫秒之後, "World!"會被打印)
Hello,
World!
*/
解釋一下delay方法:
在協程裏delay方法作用等同於線程裏的sleep, 都是休息一段時間, 但不同的是delay不會阻塞當前線程, 而像是設置了一個鬧鐘, 在鬧鐘未響之前, 運行該協程的線程可以被安排做了別的事情, 當鬧鐘響起時, 協程就會恢復運行.
協程啓動後還可以取消:
launch方法有一個返回值, 類型是Job, Job有一個cancel方法, 調用cancel方法可以取消協程, 看一個數羊的例子:
fun main(args: Array<String>) {
val job = launch(CommonPool) {
var i = 1
while(true) {
println("$i little sheep")
++i
delay(500L) // 每半秒數一隻, 一秒可以輸兩隻
}
}
Thread.sleep(1000L) // 在主線程睡眠期間, 協程裏已經數了兩隻羊
job.cancel() // 協程才數了兩隻羊, 就被取消了
Thread.sleep(1000L)
println("main process finished.")
}
運行結果是,如果不調用cancel, 可以數到4只羊:
1 little sheep
2 little sheep
main process finished.
注意還有一個方法:job.join() // 持續等待,直到子協程執行完成
1.1 理解suspend方法
suspend方法的語法很簡單, 只是比普通方法只是多了個suspend關鍵字:
suspend fun foo(): ReturnType {
// ...
}
suspend方法只能在協程裏面調用, 不能在協程外面調用.
suspend方法本質上, 與普通方法有較大的區別, suspend方法的本質是異步返回(注意: 不是異步回調).
現在, 我們先來看一個異步回調的例子:
fun main(...) {
requestDataAsync {
println("data is $it")
}
Thead.sleep(10000L) // 這個sleep只是爲了保活進程
}
fun requestDataAsync(callback: (String)->Unit) {
Thread() {
// do something need lots of times.
callback(data)
}.start()
}
邏輯很簡單, 就是通過異步的方法拉一個數據, 然後使用這個數據, 按照以往的編程方式, 若要接受異步回來的數據, 唯有使用callback.
但是假如使用協程, 可以不使用callback, 而是直接把這個數據”return”回來, 調用者不使用callback接受數據, 而是像調用同步方法一樣接受返回值. 如果上述功能改用協程, 將會是:
fun main(...) {
launch(Unconfined) { // 請重點關注協程裏是如何獲取異步數據的
val data = requestDataAsync() // 異步回來的數據, 像同步一樣return了
println("data is $it")
}
Thead.sleep(10000L) // 請不要關注這個sleep
}
suspend fun requestDataAsync() { // 請注意方法前多了一個suspend關鍵字
return async(CommonPool) { // 先不要管這個async方法, 後面解釋
// do something need lots of times.
// ...
data // return data, lambda裏的return要省略
}.await()
}
這裏, 我們首先將requestDataAsync轉成了一個suspend方法, 其原型的變化是:
1.在前加了個suspend關鍵字.
2.去除了原來的callback參數.
這是怎麼做到的呢?
當程序執行到requestDataAsync內部時, 通過async啓動了另外一個新的子協程去拉取數據, 啓動這個新的子協程後, 當前的父協程就掛起了, 此時requestDataAsync還沒有返回.子協程一直在後臺跑, 過了一段時間, 子協程把數據拉回來之後, 會恢復它的父協程, 父協程繼續執行, requestDataAsync就把數據返回了.
爲了加深理解, 我們來對比一下另一個例子: 不使用協程, 將異步方法也可以轉成同步的方法(在單元測試裏, 我們經常這麼做):
fun main(...) {
val data = async2Sync() // 數據是同步返回了, 但是線程也阻塞了
println("data is $it")
// Thead.sleep(10000L) // 這一句在這裏毫無意義了, 註釋掉
}
private var data = ""
private fun async2Sync(): String {
val obj = Object() // 隨便創建一個對象當成鎖使用
requestDataAsync { data ->
this.data = data // 暫存data
synchronized(locker) {
obj.notifyAll() // 通知所有的等待者
}
}
obj.wait() // 阻塞等待
return this.data
}
fun requestDataAsync(callback: (String)->Unit) {
// ...普通的異步方法
}
注意對比上一個協程的例子, 這樣做表面上跟它是一樣的, 但是這裏main方法會阻塞的等待async2Sync()方法完成. 同樣是等待, 協程就不會阻塞當前線程, 而是自己主動放棄執行權, 相當於遣散當前線程, 讓它去幹別的事情去.
爲了更好的理解這個”遣散”的含義, 我們再來看一個例子:
fun main(args: Array<String>) {
// 1. 程序開始
println("${Thread.currentThread().name}: 1");
// 2. 啓動一個協程, 並立即啓動
launch(Unconfined) { // Unconfined意思是在當前線程(主線程)運行協程
// 3. 本協程在主線程上直接開始執行了第一步
println("${Thread.currentThread().name}: 2");
/* 4. 本協程的第二步調用了一個suspend方法, 調用之後,
* 本協程就放棄執行權, 遣散運行我的線程(主線程)請幹別的去.
*
* delay被調用的時候, 在內部創建了一個計時器, 並設了個callback.
* 1秒後計時器到期, 就會調用剛設置的callback.
* 在callback裏面, 會調用系統的接口來恢復協程.
* 協程在計時器線程上恢復執行了. (不是主線程, 跟Unconfined有關)
*/
delay(1000L) // 過1秒後, 計時器線程會resume協程
// 7. 計時器線程恢復了協程,
println("${Thread.currentThread().name}: 4")
}
// 5. 剛那個的協程不要我(主線程)幹活了, 所以我繼續之前的執行
println("${Thread.currentThread().name}: 3");
// 6. 我(主線程)睡2秒鐘
Thread.sleep(2000L)
// 8. 我(主線程)睡完後繼續執行
println("${Thread.currentThread().name}: 5");
}
運行結果:
main: 1
main: 2
main: 3
kotlinx.coroutines.ScheduledExecutor: 4
main: 5
上述代碼的註釋詳細的列出了程序運行流程, 看完之後, 應該就能明白 “遣散” 和 “放棄執行權” 的含義了.
Unconfined的含義是不給協程指定運行的線程, 逮到誰就使用誰, 啓動它的線程直接執行它, 但被掛起後, 會由恢復它的線程繼續執行, 如果一個協程會被掛起多次, 那麼每次被恢復後, 都可能被不同線程繼續執行.
現在再來回顧剛剛那句: suspend方法的本質就是異步返回.含義就是將其拆成 “異步” + “返回”:
首先, 數據不是同步回來的(同步指的是立即返回), 而是異步回來的.
其次, 接受數據不需要通過callback, 而是直接接收返回值.
調用suspend方法的詳細流程是:
在協程裏, 如果調用了一個suspend方法, 協程就會掛起, 釋放自己的執行權, 但在協程掛起之前, suspend方法內部一般會啓動了另一個線程或協程, 我們暫且稱之爲”分支執行流”吧, 它的目的是運算得到一個數據.當suspend方法裏的*分支執行流”完成後, 就會調用系統API重新恢復協程的執行, 同時會數據返回給協程(如果有的話).
爲什麼不能再協程外面調用suspend方法?
suspend方法只能在協程裏面調用, 原因是只有在協程裏, 才能遣散當前線程, 在協程外面, 不允許遣散, 反過來思考, 假如在協程外面也能遣散線程, 會怎麼樣, 寫一個反例:
fun main(args: Array<String>) {
requestDataSuspend();
doSomethingNormal();
}
suspend fun requestDataSuspend() {
// ...
}
fun doSomethingNormal() {
// ...
}
requestDataSuspend是suspend方法, doSomethingNormal是正常方法, doSomethingNormal必須等到requestDataSuspend執行完纔會開始, 如果main方法失去了並行的能力, 所有地方都失去了並行的能力, 這肯定不是我們要的, 所以需要約定只能在協程裏纔可以遣散線程, 放棄執行權, 於是suspend方法只能在協程裏面調用.
協程創建後, 並不總是立即執行, 要分是怎麼創建的協程, 通過launch方法的第二個參數是一個枚舉類型CoroutineStart, 如果不填, 默認值是DEFAULT, 那麼協程創建後立即啓動, 如果傳入LAZY, 創建後就不會立即啓動, 直到調用Job的start方法纔會啓動.
在協程裏, 所有接受callback的方法, 都可以轉成不需要callback的suspend方法,上面的requestDataSuspend方法就是一個這樣的例子, 我們回過頭來再看一眼:
suspend fun requestDataSuspend() {
return async(CommonPool) {
// do something need lots of times.
// ...
data // return data
}.await()
}
其內部通過調用了async和await方法來實現(關於async和await我們後面再介紹), 這樣雖然實現功能沒問題, 但並不最合適的方式, 上面那樣做只是爲了追求最簡短的實現, 合理的實現應該是調用suspendCoroutine方法, 大概是這樣:
suspend fun requestDataSuspend() {
suspendCoroutine { cont ->
// ... 細節暫時省略
}
}
// 可簡寫成:
suspend fun requestDataSuspend() = suspendCoroutine { cont ->
// ...
}
在完整實現之前, 需要先理解suspendCoroutine方法, 它是Kotlin標準庫裏的一個方法, 原型如下:
suspend fun <T> suspendCoroutine(block: (Continuation<T>) -> Unit): T
現在來完善一下剛剛的例子:
suspend fun requestDataSuspend() = suspendCoroutine { cont ->
requestDataFromServer { data -> // 普通方法還是通過callback接受數據
if (data != null) {
cont.resume(data)
} else {
cont.resumeWithException(MyException())
}
}
}
/** 普通的異步回調方法 */
fun requestDataFromServer(callback: (String)->Unit) {
// ... get data from server, it will call back when finished.
}
suspendCoroutine有個特點:
suspendCoroutine { cont ->
// 如果本lambda裏返回前, cont的resume和resumeWithException都沒有調用
// 那麼當前執行流就會掛起, 並且掛起的時機是在suspendCoroutine之前
// 就是在suspendCoroutine內部return之前就掛起了
// 如果本lambda裏返回前, 調用了cont的resume或resumeWithException
// 那麼當前執行流不會掛起, suspendCoroutine直接返回了,
// 若調用的是resume, suspendCoroutine就會像普通方法一樣返回一個值
// 若調用的是resumeWithException, suspendCoroutine會拋出一個異常
// 外面可以通過try-catch來捕獲這個異常
}
回過頭來看一下, 剛剛的實現有調用resume方法嗎, 我們把它摺疊一下:
suspend fun requestDataSuspend() = suspendCoroutine { cont ->
requestDataFromServer { ... }
}
清晰了吧, 沒有調用, 所以suspendCoroutine還沒有返回之前就掛起了, 但是掛起之前lambda執行完了, lambda裏調用了requestDataFromServer, requestDataFromServer裏啓動了真正做事的流程(異步執行的), 而suspendCoroutine則在掛起等待.
等到requestDataFromServer完成工作, 就會調用傳入的callback, 而這個callback裏調用了cont.resume(data), 外層的協程就恢復了, 隨後suspendCoroutine就會返回, 返回值就是data.
1.2 async/await模式:
我們前面多次使用了launch方法, 它的作用是創建協程並立即啓動, 但是有一個問題, 就是通過launch方法創建的協程都沒辦法攜帶返回值. async之前也出現過, 但一直沒有詳細介紹.
async方法作用和launch方法基本一樣, 創建一個協程並立即啓動, 但是async創建的協程可以攜帶返回值.
launch方法的返回值類型是Job, async方法的返回值類型是Deferred, 是Job的子類, Deferred裏有個await方法, 調用它可得到協程的返回值.
async/await是一種常用的模式, async的含義是啓動一個異步操作, await的含義是等待這個異步操作結果.
是誰要等它啊, 在傳統的不使用協程的代碼裏, 是線程在等(線程不幹別的事, 就在那裏傻等). 在協程裏不是線程在等, 而且是執行流在等, 當前的流程掛起(底下的線程會被遣散去幹別的事), 等到有了運算結果, 流程才繼續運行.
所以我們又可以順便得出一個結論: 在協程裏執行流是線性的, 其中的步驟無論是同步的還是異步的, 後面的步驟都會等前面的步驟完成.
我們可以通過async起多個任務, 他們會同時運行, 我們之前使用的async姿勢不是很正常, 下面看一下使用async正常的姿勢:
fun main(...) {
launch(Unconfined) {
// 任務1會立即啓動, 並且會在別的線程上並行執行
val deferred1 = async { requestDataAsync1() }
// 上一個步驟只是啓動了任務1, 並不會掛起當前協程
// 所以任務2也會立即啓動, 也會在別的線程上並行執行
val deferred2 = async { requestDataAsync2() }
// 先等待任務1結束(等了約1000ms),
// 然後等待任務2, 由於它和任務1幾乎同時啓動的, 所以也很快完成了
println("data1=$deferred2.await(), data2=$deferred2.await()")
}
Thead.sleep(10000L) // 繼續無視這個sleep
}
suspend fun requestDataAsync1(): String {
delay(1000L)
return "data1"
}
suspend fun requestDataAsync2(): String {
delay(1000L)
return "data2"
}
運行結果很簡單, 不用說了, 但是協程總耗時是多少呢, 約1000ms, 不是2000ms, 因爲兩個任務是並行運行的.
有一個問題: 假如任務2先於任務1完成, 結果是怎樣的呢?
答案是: 任務2的結果會先保存在deferred2裏, 當調用deferred2.await()時, 會立即返回, 不會引起協程掛起, 因爲deferred2已經準備好了.
所以, suspend方法並不總是引起協程掛起, 只有其內部的數據未準備好時纔會.
需要注意的是: await是suspend方法, 但async不是, 所以它纔可以在協程外面調用, async只是啓動了協程, async本身不會引起協程掛起, 傳給async的lambda(也就是協程體)纔可能引起協程掛起.
2.函數
2.1默認參數
函數參數可以有默認值,當省略相應的參數時使用默認值。與其他語言相比,這可以減少重載數量。
fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size) { …… }
覆蓋方法總是使用與基類型方法相同的默認參數值。 當覆蓋一個帶有默認參數值的方法時,必須從簽名中省略默認參數值:
open class A {
open fun foo(i: Int = 10) { …… }
}
class B : A() {
override fun foo(i: Int) { …… } // 不能有默認值
}
如果一個默認參數在一個無默認值的參數之前,那麼該默認值只能通過使用命名參數調用該函數來使用:
fun foo(bar: Int = 0, baz: Int) { /* …… */ }
foo(baz = 1) // 使用默認值 bar = 0
不過如果最後一個 lambda 表達式參數從括號外傳給函數函數調用,那麼允許默認參數不傳值:
fun foo(bar: Int = 0, baz: Int = 1, qux: () -> Unit) { /* …… */ }
foo(1) { println("hello") } // 使用默認值 baz = 1
foo { println("hello") } // 使用兩個默認值 bar = 0 與 baz = 1
2.2 命名參數
可以在調用函數時使用命名的函數參數。當一個函數有大量的參數或默認參數時這會非常方便。
給定以下函數
fun reformat(str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ') {
……
}
我們可以使用默認參數來調用它:
reformat(str)
然而,當使用非默認參數調用它時,該調用看起來就像:
reformat(str, true, true, false, '_')
使用命名參數我們可以使代碼更具有可讀性:
reformat(str,
normalizeCase = true,
upperCaseFirstLetter = true,
divideByCamelHumps = false,
wordSeparator = '_'
)
並且如果我們不需要所有的參數:
reformat(str, wordSeparator = '_')
當一個函數調用混用位置參數與命名參數時,所有位置參數都要放在第一個命名參數之前。例如,允許調用 f(1, y = 2) 但不允許 f(x = 1, 2)。
可以通過使用星號操作符將可變數量參數(vararg) 以命名形式傳入:
fun foo(vararg strings: String) { /* …… */ }
foo(strings = *arrayOf("a", "b", "c"))
foo(strings = "a") // 對於單個值不需要星號
3.其他知識點
3.1 componentX (多聲明)
val f1 = Forecast(Date(), 27.5f, "Shinny")
val (date, temperature, details) = f1
//=======================
// 上面的多聲明會被編譯成下面的代碼
val date = f1.component1()
val temperature = f1.component2()
val details = f1.copmponent3()
// 映射對象的每一個屬性到一個變量中,這就是 多聲明。
// object class 默認具有該屬性。但普通 class 想要具有這種屬性,需要這樣做:
class person(val name: String, val age: Int) {
operator fun component1(): String {
return name
}
operator fun component2(): Int {
return age
}
}
val 必須有: 用來保存在 component1 和 component2 中返回構造函數傳進來的參數的。
operator 暫時還不明真相,IDE 提示的。 操作符重載,函數名爲操作符名(即系統默認的關鍵詞,此處爲 component1,component2).當使用該操作時,自己重寫的操作會覆蓋系統默認的操作。
// 常見用法:該特性功能強大,可以極大的簡化代碼量。 如 map 中的擴展函數實現,允許在迭代時使用 key value
for ((key, value) in map) {
Log.d("map","key:$key, value:$value")
}
3.2 inline (內聯函數)
內聯函數與普通的函數有點不同。一個內聯函數會在編譯的時候被替換掉,而不是真正的方法調用。這在譯寫情況下可以減少內存分配和運行時開銷。例如,有一函數只接收一個函數作爲它的參數。如果是普通函數,內部會創建一個含有那個函數的對象。而內聯函數會把我們調用這個函數的地方替換掉,所以它不需要爲此生成一個內部的對象。
// 例一、創建代碼塊只提供 Lollipop 或更高版本來執行
inline fun supportsLollipop(code: () -> Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
code()
}
}
// usage
supportsLollipop {
window.setStatusBarColor(Color.BLACK)
}
3.3 Application 單例化和屬性的 Delegated (by)
class App : Application() {
companion object {
private var instance: Application? = null
fun instance() = instance!!
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
我們可能需要一個屬性具有一些相同的行爲,使用 lazy 或 observable 可以被很有趣的實現重用,而不是一次又一次的去聲明那些相同的代碼。kotlin 提供了一個委託屬性到一個類的方法。這就是委託屬性。
class Delegate<T> : ReadWriteProperty<Any?, T> {
fun getValue(thisRef: Any?, property: KProperty<*>): T {
return ...
}
fun setValue(thisRef: Any?,property: KProperty<*>, value: T) {...}
// 如果該屬性是不可修改(val), 就會只有一個 getValue 函數
}
3.4 Not Null
場景1:需要在某些地方初始化該屬性,但不能在構造函數中確定,或不能在構造函數中做任何事。
場景2:在 Activity fragment service receivers…中,一個非抽象的屬性在構造函數執行之前需要被賦值。
解決方案1:使用可 null 類型並且賦值爲 null,直到真正去賦值。但是,在使用時就需要不停的進行 not null 判斷。
解決方案2:使用 notnull 委託。含有一個可 null 的變量並會在設置該屬性時分配一個真實的值。如果該值在被獲取之前沒有被分配,它就會拋出一個異常。
class App : Application() {
companion object {
var instance: App by Delegates.notnull()
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
3.5 從 Map 中映射值
另一種委託方式,屬性的值會從一個map中獲取 value,屬性的名字對應這個map 中的 key。
import kotlin.properties.getValue
class Configuration(map: Map<String,Any?>) {
val width: Int by map
val height: Int by map
val dp: Int by map
val deviceName: String by map
}
// usage
conf = Configuration(mapof(
"width" to 1080,
"height" to 720,
"dp" to 240,
"deviceName" to "myDecive"
))
3.6 custom delegate
自定義委託需要實現 ReadOonlyProperty / ReadWriteProperty 兩個類,具體取決於被委託的對象是 val 還是 var。
// step1
private class NotNullSingleValueVar<T>() : ReadWriteProperty<Any?, T> {
private var value: T? = null
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: throw IllegalStateException("${desc.name not initialized}")
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = if (this.value == null) value else throw IllegalStateException("${desc.name} already initialized")
}
}
// step2: usage
object DelegatesExt {
fun notNullSingleValue<T>(): ReadWriteProperty<Any?, T> = NotNullSingleValueVar()
}
3.7 重新實現 Application 單例
class App : Application() {
companion object {
var instance: App by Delegates.notNull()
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
// 此時可以在 app 的任何地方修改這個值,因爲**如果使用 Delegates.notNull(),
//屬性必須是 var 的。可以使用剛剛創建的委託,只能修改該值一次
companion object {
var instance: App by DeleagesExt.notNullSingleValue()
4.使用協程別導錯包
import org.jetbrains.anko.coroutines.experimental.Ref
import org.jetbrains.anko.coroutines.experimental.asReference
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
fun loadAndShowData() {
// Ref<T> uses the WeakReference under the hood
val ref: Ref<MainActivity> = this.asReference()
async(CommonPool) {
val data = getData()
// Use ref() instead of this@MyActivity
launch(UI) {
ref().asyncOverlay()
}
}
}
fun getData(): Data { ... }
fun showData(data: Data) { ... }
async(UI) {
val data: Deferred<Data> = bg {
// Runs in background
getData()
}
// This code is executed on the UI thread
showData(data.await())
}