Kotlin中一些知识点学习

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()) 
}

猜你喜欢

转载自blog.csdn.net/jielundewode/article/details/77941597