github地址:kotlinx.coroutines(https://github.com/kotlin/kotlinx.coroutines)
fun main(args: Array) {
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() // 持续等待,直到子协程执行完成
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) {
// 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) {
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 suspendCoroutine(block: (Continuation) -> 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.
我们前面多次使用了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(也就是协程体)才可能引起协程挂起.
函数参数可以有默认值,当省略相应的参数时使用默认值。与其他语言相比,这可以减少重载数量。
fun read(b: Array, 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
可以在调用函数时使用命名的函数参数。当一个函数有大量的参数或默认参数时这会非常方便。
给定以下函数
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") // 对于单个值不需要星号
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")
}
内联函数与普通的函数有点不同。一个内联函数会在编译的时候被替换掉,而不是真正的方法调用。这在译写情况下可以减少内存分配和运行时开销。例如,有一函数只接收一个函数作为它的参数。如果是普通函数,内部会创建一个含有那个函数的对象。而内联函数会把我们调用这个函数的地方替换掉,所以它不需要为此生成一个内部的对象。
// 例一、创建代码块只提供 Lollipop 或更高版本来执行
inline fun supportsLollipop(code: () -> Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
code()
}
}
// usage
supportsLollipop {
window.setStatusBarColor(Color.BLACK)
}
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> : ReadWritePropertyT> {
fun getValue(thisRef: Any?, property: KProperty<*>): T {
return ...
}
fun setValue(thisRef: Any?,property: KProperty<*>, value: T) {...}
// 如果该属性是不可修改(val), 就会只有一个 getValue 函数
}
场景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
}
}
另一种委托方式,属性的值会从一个map中获取 value,属性的名字对应这个map 中的 key。
import kotlin.properties.getValue
class Configuration(map: Map) {
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"
))
自定义委托需要实现 ReadOonlyProperty / ReadWriteProperty 两个类,具体取决于被委托的对象是 val 还是 var。
// step1
private class NotNullSingleValueVar() : ReadWriteProperty {
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(): ReadWriteProperty = NotNullSingleValueVar()
}
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()
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 = 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 = bg {
// Runs in background
getData()
}
// This code is executed on the UI thread
showData(data.await())
}