Kotlin


协程 Coroutines

协程就是 Kotlin 提供的一套线程封装的 API,就是个线程框架。协程是一种非抢占式或者说协作式的计算机程序并发调度的实现,程序可以主动挂起或者恢复执行。在 Java 虚拟机上的线程大多数的实现是映射到内核线程的,线程当中的代码逻辑在线程抢到 CPU 的时间片的时候才可以执行,否则就得歇着。协程并不会映射成内核线程或者其他这么重的资源,它的调度在用户态就可以搞定,不需要从用户态切换到内核态,任务之间的调度并非抢占式,而是协作式的。可以认为是一种轻量级的线程。

协程设计的初衷是为了解决并发问题,让 「协作式多任务」 实现起来更加方便

特点:「非阻塞式挂起」「用同步的方式写异步的代码」

从 Android 开发者的角度去理解它们的关系:

• 我们所有的代码都是跑在线程中的,而线程是跑在进程中的。
• 协程没有直接和操作系统关联,但它不是空中楼阁,它也是跑在线程中的,可以是单线程,也可以是多线程。
• 单线程中的协程总的执行时间并不会比不用协程少。
• Android 系统上,如果在主线程进行网络请求,会抛出 NetworkOnMainThreadException ,对于在主线程上的协程也不例外,这种场景使用协程还是要切线程的。

AsyncTask 是 Android 对线程池 Executor 的封装,但它的缺点也很明显:

• 需要处理很多回调,如果业务多则容易陷入「回调地狱」。
• 硬是把业务拆分成了前台、中间更新、后台三个函数。

RxJava,准确来讲是 ReactiveX 在 Java 上的实现,是一种响应式程序框架,我们通过它提供的「Observable」的编程范式进行链式调用,可以很好地消除回调。

启动模式

DEFAULT:是饿汉式启动,launch 调用后,会立即进入待调度状态,一旦调度器 OK 就可以开始执行。

LAZY 是懒汉式启动,launch后并不会有任何调度行为,协程体也自然不会进入执行状态,直到我们需要它执行的时候。launch调用后会返回一个 Job实例,调用 Job.start,主动触发协程的调度执行,调用 Job.join,隐式的触发协程的调度执行

ATOMIC 只有涉及 cancel 的时候才有意义,cancel本身也是一个值得详细讨论的话题,在这里我们就简单认为 cancel 后协程会被取消掉,也就是不再执行了。那么调用cancel的时机不同,结果也是有差异的,例如协程调度之前、开始调度但尚未执行、已经开始执行、执行完毕等等。

UNDISPATCHED 就很容易理解了。协程在这种模式下会直接开始在当前线程下执行,直到第一个挂起点,这听起来有点儿像前面的 ATOMIC,不同之处在于 UNDISPATCHED 不经过任何调度器即开始执行协程体。当然遇到挂起点之后的执行就取决于挂起点本身的逻辑以及上下文当中的调度器了。

launch

下面三种方法来创建协程:

// 方法一,使用 runBlocking 顶层函数 (不推荐这种用法,线程阻塞)
runBlocking {
    getImage(imageId) 
} 

// 方法二,使用 GlobalScope 单例对象 (不推荐这种用法,因为它的生命周期会和 app一致,无法取消)
GlobalScope.launch(/*Dispatchers.IO*/) {
    getImage(imageId)
} 

// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象 (推荐)
// 通过 context 参数去管理和控制协程的生命周期
// 这里的context和Android里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用
val coroutineScope = CoroutineScope(context)
coroutineScope.launch(Dispatchers.Main) { // 开始协程:主线程
    val token = api.getToken()            // 网络请求:IO 线程 
    val user = api.getUser(token)         // 网络请求:IO 线程
    nameTv.text = user.name               // 更新 UI:主线程
}
async
//多个网络请求需要等待所有请求结束之后再对 UI 进行更新
coroutineScope.launch(Dispatchers.Main) {
    val avatar = async { api.getAvatar(user) }    // 获取用户头像 
    val logo = 
    { api.getCompanyLogo(user) } // 获取用户所在公司的logo 
    val merged = suspendingMerge(avatar, logo)    // 合并结果
    show(merged)                                  // 更新 UI
}

让复杂的并发代码,写起来变得简单且清晰,是协程的优势。

注意:async内部有 try catch机制,所以任何异常都会被内部 catch 住,而这个在我们开发当中很容易导致一些问题没有及时发现。

withContext

这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。

coroutineScope.launch(Dispatchers.Main) { 
    val image = withContext(Dispatchers.IO) { 
        getImage(imageId) 
    } 
    avatarIv.setImageBitmap(image) 
}
suspend

中文意思是「暂停」或者「可挂起」

代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程。「非阻塞式挂起」,其实就是在讲协程在挂起的同时切线程这件事情。

这个 suspend 关键字,既然它并不是真正实现挂起,那它的作用是什么? 它其实是一个提醒。

对比 launch 与 async 这两个函数。

• 相同点:它们都可以用来启动一个协程,返回的都是 Coroutine ,我们这里不需要纠结具体是返回哪个类。
• 不同点: async 返回的 Coroutine 多实现了 Deferred

挂起的本质就是线程切出去再切回来

挂起的对象是协程。从当前线程挂起,就是这个协程从正在执行它的线程上脱离。

具体到代码其实是: 协程的代码块中,线程执行到了 suspend 函数这里的时候,就暂时不再执行剩余的协程代码,跳出协程的代码块。如果它是一个后台线程:1. 要么无事可做,被系统回收, 2. 要么继续执行别的后台任务

跟 Java 线程池里的线程在工作结束之后是完全一样的:回收或者再利用。

线程的代码在到达 suspend 函数的时候被掐断,接下来协程会从这个 suspend 函数开始继续往下执行,不过是在指定的线程。是 suspend 函数指定的,函数内部的 withContext 传入的 Dispatchers.IO 所指定的 IO 线程。

紧接着在 suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来。我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers 切换到了 IO 线程; 当这个函数执行完毕后,线程又切了回来,「切回来」也就是协程会帮我再 post 一个 Runnable,让我剩下的代码继续回到主线程去执行。

实质上会往你的主线程 post 一个 Runnable,这个 Runnable 就是你的协程代码。

Dispatchers 调度器

可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行

Dispatchers.Main: Android 中的主线程
Dispatchers.IO :针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务。
比如:读写文件,操作数据库以及网络请求
Dispatchers.Default :适合 CPU 密集型的任务,比如计算

写法

对于 Retrofit,改造成协程的写法,有两种,分别是通过 CallAdapter 和 suspend 函数:

1.CallAdapter方式

Deferred await

interface GitHubServiceApi {
    @GET("users/{login}") 
    fun getUser(@Path("login") login: String): Deferred<User> 
}

val gitHubServiceApi by lazy {
    val retrofit = retrofit2.Retrofit.Builder() 
                    .baseUrl("https://api.github.com")
                    .addConverterFactory(GsonConverterFactory.create()) 
                    //添加对 Deferred 的支持 
                    .addCallAdapterFactory(CoroutineCallAdapterFactory())
                    .build() 
    retrofit.create(GitHubServiceApi::class.java) 
}

GlobalScope.launch(Dispatchers.Main) { 
    try {
        //showUser 在 await 的 Continuation 的回调函数调用后执行
        showUser(gitHubServiceApi.getUser("bennyhuo").await()) 
    } catch (e: Exception) {
        showError(e) 
    }
}

///注意以下并不是真实的实现,仅供大家理解协程使用
fun await(continuation: Continuation<User>): Any {
    ... // 切到非 UI 线程中执行,等待结果返回 
    try { 
        val user = ...
        handler.post{ continuation.resume(user)
    } catch(e: Exception) {
        handler.post{ continuation.resumeWithException(e) }
    } 
}
2.suspend 函数的方式

Retrofit修改接口方法,最新的 Retrofit 源码的支持

@GET("users/{login}") 
suspend fun getUser(@Path("login") login: String): User

总结

• 协程就是切线程
• 挂起就是可以自动切回来的切线程
• 挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作

高阶函数

• 高阶函数:以另一个函数作为参数或者返回值的函数
• 函数类型
(Int, String) –> Unit
参数类型 –>返回类型 Unit不能省略

函数没有Receiver, 方法有Receiver

函数作为参数,即高阶函数中,函数的参数可以是一个函数类型

函数作为返回值也非常实用,例如我们的需求是根据不同的快递类型返回不同计价公式,普通快递和高级快递的计价规则不一样,这时候我们可以将计价规则函数作为返回值

From Java To Kotlin

常量与变量

var name = "Amit Shekhar"
val name = "Amit Shekhar"

null声明

var otherName : String? 
otherName = null

空判断

text?.let { val length = text.length }// or simplyval length = text?.length

字符串拼接

val firstName = "Amit"
val lastName = "Shekhar"
val message = "My name is: $firstName $lastName"

换行

val text = """ |First Line |Second Line |Third Line """.trimMargin()

三元表达式

val text = if (x > 5) "x > 5" else "x <= 5"

操作符

val andResult = a and b
val orResult = a or b
val xorResult = a xor b
val rightShift = a shr 2
val leftShift = a shl 2
val unsignedRightShift = a ushr 2

data关键词

/** Kotlin会为类的参数自动实现get set方法* */
class User(val name: String, val age: Int, val gender: Int, var address: String) 
/** 用data关键词来声明一个数据类,除了会自动实现get set,还会自动生成equals hashcode toString* */
data class User2(val name: String, val age: Int, val gender: Int, var address: String)

类型判断和转换 (声明式)

if (object is Car) {
    var car = object as Car
}

类型判断和转换 (隐式)

if (object is Car) {
    var car = object // 聪明的转换
}

多重条件

if (score in 0..300) { }

更灵活的case语句

var score = // some score
var grade = when (score) { 
    9, 10 -> "Excellent"
    in 6..8 -> "Good"
    4, 5 -> "OK"
    in 1..3 -> "Fail"
    else -> "Fail"
}

for循环

for (i in 1..10) { } 
for (i in 1 until 10) { } 
for (i in 10 downTo 0) { } 
for (i in 1..10 step 2) { } 
for (i in 10 downTo 0 step 2) { }
for (item in collection) { }
for ((key, value) in map) { }

更方便的集合操作

val listOfNumber = listOf(1, 2, 3, 4)
val keyValue = mapOf(1 to "Amit", 2 to "Ali", 3 to "Mindorks")

遍历

cars.forEach { 
    println(it.speed)
}

cars.filter { it.speed > 100 }
    .forEach { println(it.speed)}

方法定义

fun doSomething() {
	// logic here
}
fun doSomething(vararg numbers: Int) {
    // logic here 
}

带返回值的方法

fun getScore(): Int { 
    // logic here return score 
} 
// as a single-expression function 
fun getScore(): Int = score

无结束符号

fun getScore(value: Int): Int { 
    // logic here 
    return 2 * value
} 
// as a single-expression function 
fun getScore(value: Int): Int = 2 * value

constructor 构造器

class Utils private constructor() {
    companion object { 
        fun getScore(value: Int): Int { return 2 * value } 
    } 
}
// another way
object Utils {
	fun getScore(value: Int): Int { return 2 * value }
}

Get Set 构造器

data class Developer(val name: String, val age: Int)

原型扩展

fun Int.triple(): Int {
    return this * 3
} 
var result = 3.triple()

闭包原则

在 Kotlin 中有这样一个语法糖:当函数的最后一个参数是 lambda 表达式时,可以将 lambda 写在括号外。

关键字

Kotlin的硬关键字

as一一 用于做类型转换或为 import 语句指定别名

as?一一类型安全 的类型转换运算符。

break一一中断循环

class一一声明类。

continue 一忽略本次循环剩下的语句,重新开始下一次循环。

do一一用于 do while 循环

else一一在 if 分支中使用

false一一在 Boolean 类型中表示假 的直接量。

for一一用于 for 循环

fun 一一声 明函数

if-在 if 分支中使用

in 一一在 for 循环中使用; in 还可作为双目运算符,检查 一个值是否处于区间或集合 内;

in 也可 在when 表达式中使用; in 还可用于修饰泛型参数,表明该泛型参数支持逆变

!in 一一可作为双目运算符 的反义词:!in 也可在 when 表达式中使用

is 一一用于做类型检查(类 Java instanceof) 或在 when 表达式中使用

!is一一 用于做类型检查( is 的反义词〉或在 when 表达式中使用

null 一一 代表空的直接量。

object ——用于声明对象表达式或定义命名对象

package一一用于为当 前文件指定包

return 一一声明函数的返回

super一-用于引用父类实现的方法或属性,或者在子类构造器中调用父类构造器

this 一一 代表当前类的对象或在构造器中调用当前类的其他构造器

throw一一用于抛出异常

true 一一在 Boolean 类型中表示真的直接量。

try一一开始异常处理

typealias一一用于定义类型别名。

val 一声明只读属性或变量。

var一一声明可变属性或变量。

when一一用于 when 表达式。while一一-用于 while 循环或 do while 循环

Kotlin 的软关键字

by一一用于将接口或祖先类的实现代理给其他对象。

catch 一一在异常处理中用于捕捉异常

constructor一一用于声明构造器。

delegate 一用于指定该注解修饰委托属性存储其委托实例的字段

dynamic一一主要用于在 Kotlin/JavaScript 中引用 一个动态类型

field 一一用于指定该注解修饰属性的幕后字段。

file 一一用于指定该注解修饰该源文件本身

finally一一异常处理中的 finally

get一一用于声明属性的 getter 方法,或者用于指定该注解修饰属性的 getter 方法

import一一用于导包。

init一一用于声明初始化块

param 一一用于指定该注解修饰构造器参数

property一一用于指定该注解修饰整个属性(这种目标的注解对 Java 不可见,因为 Java并没有真正的属性)。

receiveris一一用于指定该注解修饰扩展方法或扩展属性的接收者

set一一用于声明属性的 setter 方法,或者用于指定该注解修饰属性的 setter 方法

setparam 一一用于指定该注解修饰 setter 方法的参数

where一一用于为泛型参数增加限制。

Kotlin 的修饰符关键字

abstract一一用于修饰抽象类或抽象成员

annotation 一一用于修饰一个注解类。

companion 一一用于声明一个伴生对象

const一一用于声明编译时常量

crossinline一一用于禁止在传给内联函数的 Lambd 表达式中执行非局部返回

data一一用于声明数据类。

enum一一用于声明枚举

external一一用于声明某个方法不由 Kotlin 实现(与 Java 的 native 相似〉。

final 一一用于禁止被重写

infix一一声明该函数能以双目运算符的格式执行

inline 一一用于声明内联函数, Lambda 表达式可在内联函数中执行局部返回。

inner一一用于声明内部类,内部类可以访问外部类的实例

internal 一一用于表示被修饰的声明只能在当前模块内可见

lateinit——-用于修饰 non-null 属性,用于指定该属性可在构造器以外的地方初始化

noinline一一用于禁止内联函数中个别 Lambda 表达式被内联化

open 一一用于修饰类,表示该类可派生子类;或者用于修饰成员,表示该成员可以被重写。

out一一用于修饰泛型参数,表明该泛型参数支持协变。

override一一用于声明重写父类的成员

private ——private 访问权限

protected ——–protected 访问权限

public——-public 访问权限。

reified 一一用于修饰内联函数中的泛型形参,接下来在该函数中就可像使用普通类型一样使用该类型参数。

sealed一一用于声明一个密封类。

suspend 一一用于标识一个函数后 Lambda 表达式可作为暂停。

tailrec一一用于修饰一个函数可作为尾随递归函数使用。

vararg 一一用于修饰形参,表明该参数是个数可变的形参。

延迟初始化

懒初始化 by lazy

by lazy本身是一种属性委托。属性委托的关键字是by

懒初始化是指推迟一个变量的初始化时机,变量在使用的时候才去实例化,这样会更加的高效。

/** 懒初始化by lazy**/
val user1: User by lazy {
    User("jack", 15) 
}

延迟初始化 lateinit

lateinit var 只能用来修饰类属性,不能用来修饰局部变量,并且只能用来修饰对象,不能用来修饰基本类型(因为基本类型的属性在类加载后的准备阶段都会被初始化为默认值)。

lateinit var的作用也比较简单,就是让编译期在检查时不要因为属性变量未被初始化而报错。

 /** 延迟初始化lateinit* */
private lateinit var name: String

lateinit var user2: User fun testLateInit() { 
    user2 = User("Lily", 14) 
} 

by lazy 和 lateinit 的区别

• by lazy 修饰val的变量

• lateinit 修饰var的变量,且变量是非空的类型

类委托 by

想要修改HashSet的某些行为函数add和addAll,需要实现MutableCollection接口的所有方法,将这些方法转发给innerSet去具体的实现。虽然只需要修改其中的两个方法,其他代码都是模版代码。 只要是重复的模版代码,Kotlin这种全新的语法糖就会想办法将它放在编译阶段再去生成。 这时候可以用到类委托by关键字,如下所示:

/** 通过by关键字将接口的实现委托给innerSet成员变量,需要修改的函数再去override就可以了* */
class CountingSet2<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> by innerSet {
    var objectAdded = 0
    override fun add(element: T): Boolean {
        objectAdded++ 
        return innerSet.add(element)
    }
    override fun addAll(elements: Collection<T>): Boolean {
        objectAdded += elements.size 
        return innerSet.addAll(elements)
    }
}

通过by关键字将接口的实现委托给innerSet成员变量,需要修改的函数再去override就可以了,通过类委托将10行代码就可以实现上面接近100行的功能,简洁明了,去掉了模版代码。

with

• 函数原型:

inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
/** 通过with语句,可以直接将对象传入,省掉对象的声明* */
fun alphabet4(): String {
    return with(StringBuilder()) { 
    	append("START\n") 
        for (letter in 'A'..'Z') { 
            append(letter) 
        } 
        append("\nEND") 
        toString()
    } 
}

apply

• 函数原型:

inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
/** 用apply语句简化代码,在apply的大括号里可以访问类的公有属性和方法* */
fun alphabet5() = StringBuilder().apply {
    append("START\n")
    for (letter in 'A'..'Z') {
        append(letter) 
    }
    append("\nEND") 
}.toString()

let

• 函数原型:

inline fun <T, R> T.let(block: (T) -> R): R = block(this)

let 函数默认当前这个对象作为闭包的 it 参数,返回值是函数里面最后一行,或者指定 return

/* * 通过let语句,在?.let之后,如果为空不会有任何操作,只有在非空的时候才会执行let之后的操作 * */ 
user?.let { 
    it.name
    it.age 
    it.toString() 
}

通过let语句,在?.let之后,如果为空不会有任何操作,只有在非空的时候才会执行let之后的操作

Elvis 操作符 ?:

?:符号会在符号左边为空的情况才会进行下面的处理,不为空则不会有任何操作。跟?.let正好相反

/** * Elvis操作符 ?: 简化对空值的处理 */
fun testElvis2(input: String?, user: User?) {
    val b = input?.length ?: -1;
    user?.save() ?: User().save()
}

扩展函数 Extension Functions

比如给String增加一个hello函数,可以这样子写:

fun String.hello(world : String) : String {
    return "hello " + world + this.length; 
} 
fun main(args: Array<String>) {
    System.out.println("abc".hello("world"));
}

反编绎生成的字节码,结果是:

@NotNull 
public static final String hello(@NotNull String $receiver, @NotNull String world) {
    return "hello " + world + $receiver.length();
} 
public static final void main(@NotNull String[] args) {
    System.out.println(hello("abc", "world"));
}
可以看到,实际上是增加了一个static public final函数。

并且新增加的函数是在自己的类里的,并不是在String类里。即不同的库新增加的扩展函数都是自己类里的,不会冲突。

@ExtensionMethod实际上也是一个语法糖。

class Extensions { 
    public static String hello(String receiver, String world) {
        return "hello " + world + receiver.length(); 
    } 
} 
public class Test {
    public static void main(String[] args) {
        System.out.println(Extensions.hello("abc", "world")); 
    }
}
总结

1.kotlin 的 Extension Functions 和 lombok 的 @ExtensionMethod 实际上都是增加 public static final 函数
2.不同的库增加的同样的 Extension Functions 不会冲突
3.设计的动机是减少各种 utils 类。

顶层函数和属性

顶层函数

见名知意,原来在Java中,类处于顶层,类包含属性和方法,在 Kotlin中,函数站在了类的位置,我们可以直接把函数放在代 码文件的顶层,让它不从属于任何类。就像下面这样,我们在一个 Str.kt文件中写入如下的 Kotlin代码。

可以通过 import 包名 .函数名 来导入我们将要使用的函数,然后就可以直接使用了看看在

顶层属性

了解了顶层函数,下面再看看顶层属性。顶层属性也就是把属性直接放在文件顶层,不依附
于类。我们可以在顶层定义的属性包括 var变量和 val常量,就像下面这样。

Kotlin Reified

使用Kotlin Reified (具体化)让泛型更简单安全

我们在编程中,出于复用和高效的目的,我们使用到了泛型。但是泛型在JVM底层采取了类型擦除的实现机制,Kotlin也是这样。然后这也带来了一些问题和对应的解决方案。这里我们介绍一个reified用法,来实现更好的处理泛型。

在编译成class文件后,就采用了类型擦除

类型擦除带来的问题

安全问题:未检查的异常

显式传递Class

可能导致更多方法的产生

使用reified很简单,主要分为两步

• 在泛型类型前面增加 reified
• 在方法前面增加 inline (必需的

inline fun <reified T> Bundle.puls(key: String, value: T) {
    when(value) {
        is Long -> putLog(key, value)
        is String -> putString(key, value)
        is Char -> putChar(key, value)
        is Int -> putInt(key, value)
    }
}

• KotlinKotlin 编译器会将 reified 方法 asType 内联 (inline) 到调用的地方(call site)

• 方法被内联到调用的地方后,泛型 T 会被替换成具体的类型


文章作者: GPC
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 GPC !
评论
  目录