1. 協程
github位址:kotlinx.coroutines(https://github.com/kotlin/kotlinx.coroutines)
fun main(args: Array<String>) {
launch(CommonPool) {
delay(L)
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 =
while(true) {
println("$i little sheep")
++i
delay(L) // 每半秒數一隻, 一秒可以輸兩隻
}
}
Thread.sleep(L) // 在主線程睡眠期間, 協程裡已經數了兩隻羊
job.cancel() // 協程才數了兩隻羊, 就被取消了
Thread.sleep(L)
println("main process finished.")
}
運作結果是,如果不調用cancel, 可以數到4隻羊:
little sheep
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() // 這個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() // 請不要關注這個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(L) // 過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 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(L) // 繼續無視這個sleep
}
suspend fun requestDataAsync1(): String {
delay(L)
return "data1"
}
suspend fun requestDataAsync2(): String {
delay(L)
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 = , len: Int = b.size) { …… }
覆寫方法總是使用與基類型方法相同的預設參數值。 當覆寫一個帶有預設參數值的方法時,必須從簽名中省略預設參數值:
open class A {
open fun foo(i: Int = ) { …… }
}
class B : A() {
override fun foo(i: Int) { …… } // 不能有預設值
}
如果一個預設參數在一個無預設值的參數之前,那麼該預設值隻能通過使用命名參數調用該函數來使用:
fun foo(bar: Int = , baz: Int) { /* …… */ }
foo(baz = ) // 使用預設值 bar = 0
不過如果最後一個 lambda 表達式參數從括号外傳給函數函數調用,那麼允許預設參數不傳值:
fun foo(bar: Int = , baz: Int = , qux: () -> Unit) { /* …… */ }
foo() { 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, 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(), f, "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 ,
"height" to ,
"dp" to ,
"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 [email protected]
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())
}