我的 Android 重构之旅:Kotlin 的高阶特性入门

Kotlin 是一个用于现代多平台应用的静态编程语言,由 JetBrains 开发(也就是开发了号称Java界最智能的集成开发工具IntelliJ IDEA的公司)。Kotlin可以编译成Java字节码(就像Groovy和Scala一样),也可以编译成JavaScript,方便在没有JVM的设备上运行。

Kotlin 已经面世多年了,由于是 Google 亲儿子的缘故,Google 爸爸为他出了数量可观的框架以供支持开发者,恰逢最近他趣升级了 AndroidX 系列的框架,这给了我们使用"官方精品"Ktx系列框架的机会。与使用时感受到的丝般顺滑不同,在阅读源码时的❓❓❓成了日常,官方使用了大量 Kotlin 特性仿佛让我感觉我学了一个假的 Kotlin 。因此为了能够让我自己读得懂、抄的会、天天都开心,所以总结了该文章。


一些基础但又不那么基础的语法

Kotlin 的对象定义

class KotlinBean(var str: String = "1", var str2: String) {

    init {
        // 只存在 init 的作用域当中
        var info = "123"
        str = "init"
    }

    constructor() : this(str2 = "2") {

    }

    // 如果构造函数没有代码可以不写 {} 
    constructor(info: String) : this(str2 = info)

    fun methods1() {
        str = "methods1"
    }

    fun methods2() {
        str2 = "methods2"
        // 无法使用 init 中定义的变量
        // info
    }
}

...
用起来花样可多了
KotlinBean()
KotlinBean(str2 = "2")
KotlinBean("str","str2")
KotlinBean("info")

数据类与解构声明

平常我们业务开发接触的最多的就是从服务端获取 JSON,对于简单的数据 Kotin 提供了一种十分好用的方案,就是所谓的数据类。
数据类有一套自己的定义,必须由 data 修饰、主构造函数需要至少有一个参数、主构造函数当中的参数自动会生成 toString()、 equals()、 hashCode() 以及 copy() 方法。

data class AppConfigInfo(var avatarUrl: String = "www.baidu.com", var hostAudio: String) { 
    // 不包含在主构造函数的属性,都不包含在 toString()、 equals()、 hashCode() 以及 copy()
    var customerServiceInfo = "9:00-18:00"
    var customerServiceTel = "4000-975-976"
    var beanRechangeCall = "18150107014"
}

由于需要写在主构造函数当中,其实极大的限制了我们的使用场景我们面对的常常是复杂的 JSON,相比于正常的 class 有什么好处呢?这时候就需要引入一个新的概念“解构声明”

val appConfigInfo = AppConfigInfo("www.baidu2.com", "www.baidu3.com")
// 按照顺序获取数据
// 不在主构造函数当中的参数,无法获取
val (avatarUrl, hostAudio) = appConfigInfo
// 不需要获取第一个参数
val (_, hostAudio) = appConfigInfo

解构声明,能让我们按照主构造函数当中参数的顺序获取对应的参数,通过_跳过不需要的参数。
相对于使用较少的 data class ,我们开发中经常碰到 Map 的循环,同样可以使用解构声明

 var map = HashMap<String, String>()
 // 这样就是每次循环获取 key 因为 value 在前面
 for ((key) in map) {
 ...
 }
 // 这样每次只取 value 通过 _ 跳过参数
 for ((_, value) in map) {
 ...
 }
 // 获取到完整的 key value
 for ((key, value) in map) {
 ...
 }

Interface

对于 Kotlin 来说,接口与抽象类的概念已经无限相似了,接口可以既包含抽象方法的声明也包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现。

interface BasisInterface {

    var data1: String

    // 在接口中声明的属性要么是抽象的例如上面的 data1 它就是抽象的
    // 要么提供访问器(get 方法)的实现
    // 如果你改成 var 那么 Kotlin 就会默认实现 get set 方法,但是这样又违背了接口中属性的定义
    // 所以只能用 val
    val data2: String
        get() = "foo"

    /**
     * 有点像 java8 中接口被 default 修饰符修饰过的方法
     */
    fun defaultMethods(): String {
        return data2
    }
}

class BasisImpl(override var data1: String) : BasisInterface {

    // 对于接口中声明但是未实现的属性,我们俩种方式去实现
    // 一种是写在主构造函数当中
    // 一种是覆写方法
    // override var data1: String
    // get() = TODO("Not yet implemented")
    // set(value) {}

    companion object {
        var aa:String = ""
    }
}

对于我们平常开发中常常遇见需要实现匿名内部类,Kotlin 也出了新定义

window.addMouseListener(object : MouseAdapter() {

    override fun mouseClicked(e: MouseEvent) { …… }

    override fun mouseEntered(e: MouseEvent) { …… }
})

这里又引申出一个新的修饰符 object-对象表达式,它的作用可不止于实现匿名类,在之前的开发中,如果我们想获取一个实现了多个接口的对象,那么只能定义一个新的类去分别继承需要的接口现在我们只需利用 object 修饰符

// 获取一个实现多个接口的对象
val impl4 = object : BasisInterface, BasisInterfaceB {
    override var data1: String
        get() = TODO("Not yet implemented")
        set(value) {}
}

甚至于我们想临时定义一个对象,来限定参数范围我们也能利用它

...
// 直接获取一个对象
val impl3 = getObject(9)
impl3.age
impl3.sex
...
/**
 * 利用 object 关键字直接生成一个对象
 * object = 对象声明
 * 注意如果你先引用内部的 age 那么一定只能是 private
 * 暂时没想明白为什么要这样限制
 */
private fun getObject(tempAge: Int) = object {
    val age: Int = tempAge
    val sex: Int = 0
}

或是

...
val adHoc = object {
    var x: Int = 0
    var y: Int = 0
}
print(adHoc.x + adHoc.y)
...

这些看上去已经很酷了对吧,但是它的能力不止于此,他还能够生成单例代码、伴生对象(类 Static 关键字)

object KotlinSingleton {

    fun getInfo(): String {
        return ""
    }
}
...
class AccountInfo {
    // 类似于 static 关键字
    companion object  {
        var mAccountInfo :String? = null

    }
}
...

// 注意 companion 本质上也是一个对象也可以继承接口与被赋值
AccountInfo.Companion.mAccountInfo
AccountInfo.mAccountInfo

object 关键词赋予了被修饰的代码"成为对象"的能力,极大的省略了我们平常开发中编写的“Temp”类。


默认参数

开发中,我们经常碰到一个函数要对不同的参数写不同的方法,这时默认参数就能解决我们的问题

....
overloading("a", "b")
overloading("a")
overloading(str2 = "b")
...

/**
 * 有默认参数的方法能够调用重载
 */
fun overloading(str1: String = "str1", str2: String = "str2") {

}

以上就是对 Kotlin 基础的一些回顾。
在我们开始介绍 Kotlin 一些高级特性之前,我们先要理解一个概念,任何 Ktolin 代码最终一定是转化成字节码交由 jvm 运行,所以无论 Kotlin 有什么样的语法糖都一定能转化成我们熟悉的 Java 代码,利用 Android Studio 的 Kotlin ShowBytecode 功能我们就能将 Kotlin 代码转为 Java 代码,这对我们理解 Kotlin 一些高级特性十分有效。


可空属性中存在的问题

从上面我们学习到了,在 Kotlin 中,不需要自己动手去写一个 JavaBean,可以直接使用 DataClass。

internal class UserInfo(var name: String, var age: Int) {
...
}

// 这个Bean是用于接收服务端 JSON 数据,通过Gson转化为对象的。
val gson = Gson()
val person = gson.fromJson<Person>("{\"age\":\"12\"}", Person::class.java)

我们传递了一个json字符串,但是没有包含key为name的值,并且注意:
在Person中name的类型是String并没有带 ?,也就是说是不允许name=null的
那么上面的代码,我运行起来结果是什么呢?

1.报错,毕竟没有传name的值;
2.不报错,name 默认值为"";
3.不报错,name=null;

感觉1最合理,也符合Kotlin的空安全检查。
那么我们,修改一下代码,看一下输出:

val gson = Gson()
val person = gson.fromJson<Person>("{\"age\":\"12\"}", Person::class.java)
println(person.name )

输出结果:

null

就算我们规定了 name 非空,但是依然绕过了 Kotlin 的空类型检查,所以我们平常在使用 Kotlin 非空类型时候一定要定义默认值,在第三方框架中,难免会碰见用奇怪的方法构建出来一个类,从而绕过 Kotlin 的空类型检查,就比如说 gson 内部用了 "sun.misc.Unsafe" 这个类来生成对象,导致出现上面的问题。


扩展函数

接下来说的就是 Kotlin 另一个常用功能扩展,它可以给已有的类添加额外方法(函数)和属性,而且既不用改源码也不需要写子类。例如,你可以为一个不能修改的第三方 SDK 中的类编写一个新的方法,这个新增的方法就和该类中的原有方法一样,可以用普通的方式调用,新增属性也是如此。

这里我借鉴网上比较热门的一个方法,为 Float 增加一个 dp 值转成像素值:

val Float.dp
  get() = TypedValue.applyDimension(
    TypedValue.COMPLEX_UNIT_DIP,
    this,
    Resources.getSystem().displayMetrics
  )

...

val RADIUS = 200f.dp

看到这里可能会有些疑问,上面的 get() 方法是什么呢?这就要回到 Kotlin 对属性的定义了,在 Kotlin 中每个属性都会有幕后字段(backing field)也就是所谓的 gei set 方法,一个完整的属性定义应该是这样的:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

其初始器(initializer)、getter 和 setter 都是可选的。属性类型如果可以从初始器 (或者从其 getter 返回值,如下文所示)中推断出来,也可以省略。

回到扩展这边,扩展函数存在一个作用域的概念,我们一般都将扩展函数写至 Top Level 也就是类的顶部,这样它就不属于任何类,这样你就能在任何类里使用它和 JAVA 中的 static 一致,那么 Kotlin 是怎么知道它可以被谁调用呢?在 Kotlin 里,当你给声明的函数名左边加上一个类名的时候,表示你要给这个函数限定一个 Receiver——直译的话叫接收者,其实也就是哪个类的对象可以调用这个函数。如果我们想限定扩展在某个类中使用,这时候我们就需要将拓展写至类当中,如下:

class AppConfigInfo {

  fun String.toString(i: Int) {
    ...
  }
}

扩展属性和扩展函数基本一致,但是需要注意定义扩展属性,必须定义getter函数,它没有默认getter的实现。

接下来是高级函数时间


lambda

lambda 自从 java 1.8 之后大家或多或少都有稍微接触过,Kotlin 中的 lambda 和 java 中概念差别还是蛮大的:

Java:
view.setOnClickListener(v -> {
    v.setVisibility(View.GONE);
});

Kotlin:
btnRequestNetwork.setOnClickListener {
    it.setVisibility(View.VISIBLE)
}

Kotlin 中默认不需要声明参数的名称,译器自动生成一个名为it的参数,乍一看大家是不是觉得还挺像的?但是 Kotlin 的 lambda 语法不只于此。

...
// 如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外
lambdaBFun("a") {
    onClick()
}
...
fun lambdaBFun(str: String = "view", onClickListener: () -> View.OnClickListener) {
...
}

// lambda最后一条语句的执行结果表示这个lambda的返回值
view.setOnDragListener { it, event ->
    false
}

// 如果不需要用到参数可以用 _ 代替
view.setOnDragListener { _, event ->
    false
}

除此之外还很多为了便利而生的语法糖,这里就不讨论了,着重说一下高阶函数
“高阶函数是将函数用作参数或返回值的函数。” 这是 Kotlin 对于高阶函数的定义,初次听上去非常难以理解。为此我们不妨构建一个场景:
在 Java 里,如果你有一个 a 方法需要调用另一个 b 方法

int a() {
  return b(1);
}
a();

而如果你想在 a 调用时动态设置 b 方法的参数,你就得把参数传给 a,再从 a 的内部把参数传给 b:

int a(int param) {
  return b(param);
}
a(1); // 内部调用 b(1)
a(2); // 内部调用 b(2)

这时候又出现一个新的需求,我在 a 的内部有一处对别的方法的调用,这个方法可能是 b,可能是 c,不一定是谁,我只知道,我在这里有一个调用,它的参数类型是 int ,返回值类型也是 int ,而具体在 a 执行的时候内部调用哪个方法,我希望可以动态设置,那么聪明的你一定很快就想到用接口来实现:

public interface Wrapper {
  int method(int param);
}

int a(Wrapper wrapper) {
  return wrapper.method(1);
}

a(wrapper1);
a(wrapper2);

写完之后你是不是有点迷糊,这在哪里有用到?那我们这时换个说法,拿最常见的 OnClickListener 事件举例:

public class View {
  OnClickListener mOnClickListener;
  ...
  public void onTouchEvent(MotionEvent e) {
    ...
    mOnClickListener.onClick(this);
    ...
  }
}

所谓的点击事件,最核心的内容就是调用内部的一个 OnClickListener 的 onClick() 方法:

public interface OnClickListener {
  void onClick(View v);
}

而所谓的这个 OnClickListener 其实只是一个壳,它的核心全在内部那个 onClick() 方法。换句话说,我们传过来一个 OnClickListener:

OnClickListener listener1 = new OnClickListener() {
  @Override
  void onClick(View v) {
    doSomething();
  }
};
view.setOnClickListener(listener1);

本质上其实是传过来一个可以在稍后被调用的方法(onClick())。只不过因为 Java 不允许传递方法,所以我们才把它包进了一个对象里来进行传递。
但是在 Kotlin 当中,函数的参数也可以是函数类型的参数:

// 这种写法多此一举请不要学
addOnclick(view) {
    it.visibility = View.VISIBLE
}

fun addOnclick(view: View, notClickView: (View) -> Unit) {
        view.setOnClickListener {
            // 这俩个等价
            notClickView(view)
            // invoke 后面再解释
            notClickView.invoke(view)
        }
}

函数类型是有严格的语法规定,例如 (Int) -> String 代表了一个传入参数为 Int 类型,返回值为 String 的函数类型,参数类型列表可以为空,如 () ->String 但是返回值为空时必须用 Unit 表示。
同时,函数类型不只可以作为函数的参数类型,还可以作为函数的返回值类型:

fun lambdaAFun(param: Int): (Int) -> String {
  ...
}

我们不光能在函数中定义函数类型,还可以将已经声明好的函数转化为一个函数类型:

var overloading = ::overloading
/**
 * 有默认参数的方法能够调用重载
 */
fun overloading(str1: String = "str1", str2: String = "str2") {
...
}

可能有的同学不太理解 :: 的写法,这边我们就用最直白的说法来解释:任何函数类型最终会转化成一个对象,:: 作用就是将这个函数转化为一个函数类型,也就是转化为一个对象,我们这边可以看下 Kotlin 生成的源码:

Function2 var10000 = new Function2((BasisGrammar)this) {
   // $FF: synthetic method
   // $FF: bridge method
   public Object invoke(Object var1, Object var2) {
      this.invoke((String)var1, (String)var2);
      return Unit.INSTANCE;
   }
   public final void invoke(@NotNull String p1, @NotNull String p2) {
      Intrinsics.checkParameterIsNotNull(p1, "p1");
      Intrinsics.checkParameterIsNotNull(p2, "p2");
      ((BasisGrammar)this.receiver).overloading(p1, p2);
   }
   public final KDeclarationContainer getOwner() {
      return Reflection.getOrCreateKotlinClass(BasisGrammar.class);
   }
   public final String getName() {
      return "overloading";
   }
   public final String getSignature() {
      return "overloading(Ljava/lang/String;Ljava/lang/String;)V";
   }
};

实际上 Kotlin 是生成了一个 Function2 对象,然后再其中调用真正的实现 overloading 方法,看到源码之后我们也就能很好的解释之前的invoke方法了,本质就是一个编译器帮我们生成的一个代理对象。

注意,这边可能会有点小坑,现在我的代码中有这么几行:

fun b(param: Int): String {
 return param.toString()
}
val d = ::b

那我如果想把 d 赋值给一个新的变量 e:

val e = d

这时候是否需要加::呢?

匿名函数感兴趣可以自行去学习,和上面同理只是不带有名称。


Kotlin 永远滴神

学会了高阶函数后,我们来看看 Kotlin 帮我们提前定义好的高阶函数,下面我就拿一个很常见的场景从服务端获取 JSON 数据举例:

// 我们有这样一个数据
class User {
    String name;
    String nickName;
    int age;
    int sex;
    Address address;
 }

如果我们需要进行判空

java
if (user != null && user.address != null) {
    print(user.address.address);
}

Kotlin 
user?.run {
    print(it.address?.address)
}

这时候可能有些同学有疑问了,我用❓不也可以吗,为什么还需要这个函数呢。别急我们接着往下看,这时候我们想偷懒不写 it 那就可以这样写:

user?.run {
    print(address?.address)
}

在 run 的作用域当中我们可以不用显式的去写 it 并且还将包含一个返回值也就是所谓闭包形式返回,这样让我们在写业务代码的时候能够更简洁,例如我们常常会碰到需要更改一个 bean 的数据就可以这样写:

user?.run{
    name = "xxx"
    nickName = "xxx"
    age = 10
    sex = 1
    // 闭包形式返回
    getAvatar()
}?.run{
    // 这里取到的值是 getAvatar()的值
}

既然有“闭包形式返回”那一定有返回原始对象的函数:

user?.also{
    name = "xxx"
    nickName = "xxx"
    age = 10
    sex = 1
    getAvatar()
}?.run{
    // 这里取到的是 user 对象
}

既然我们已经会用了,我们就根据前面所学的知识来看下 Kotlin 是如何实现这几个方法的

public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

上面的代码,首先针对了<T>这个类型的做了拓展函数,利用了 inline 内联函数修饰符将函数内部的调用“拆为同一级”我们简单的理解成提升性能的一种方式,并定义了函数类型的“对象”作为参数,最后根据不同的方法返回不同的值,这里将我们之前所有的知识点全部整合到了一起。

fun initInfo(){
    var name = getName()
}

// 在有内联函数的情况下可以等价于
fun initInfo(){
    var name = XAcountInfo.getName()
}

public inline fun getName():String{
    XAcountInfo.getName()
}

委托

在 Kotlin 中委托一般特指俩种场景,委托实现与委托属性。

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print()
}

委托模式一般都用于代替继承,我们只需要知道语法规则即可。

委托属性
委托属性有固定的语法规范val/var <属性名>: <类型> by <表达式>。
在 by 后面的表达式是该 委托, 因为属性对应的 get()(与 set())会被委托给它的 getValue() 与 setValue() 方法

class Example {
    var p: String by Delegate()
}

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }
 
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

当我们从委托到一个 Delegate 实例的 p 读取时,将调用 Delegate 中的 getValue() 函数, 所以它第一个参数是读出 p 的对象、第二个参数保存了对 p 自身的描述 (例如你可以取它的名字)。

val e = Example()
println(e.p)

输出结果:
Example@33a17727, thank you for delegating ‘p’ to me!

除此之外 Kotlin 为我们提供了三种标准委托:

  • 延迟属性(lazy properties): 其值只在首次访问时计算;
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知;
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。

lazy 算是我们比较常接触到的,它可以让属性在我们首次调用时才进行初始化

val lazyValue: String by lazy {
    // 第一次调用时会触发此作用域
    println("computed!")
    // 为 lazyValue 赋值
    "Hello"
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}

输出结果:
  computed!
  Hello
  Hello

默认情况下 lazy 是线程安全的底层依靠同步锁保存安全,如果我们不需要同步锁的话,可以将LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy() 函数。 而如果你确定初始化将总是发生在与属性使用位于相同的线程, 那么可以使用 LazyThreadSafetyMode.NONE 模式:它不会有任何线程安全的保证以及相关的开销。
这种特性很适合我们替换之前静态内部类的单例

class KotlinSingleton2 {

    fun getInfo(): String {
        return ""
    }

    companion object  {
        val INSTANCE: KotlinSingleton2 by lazy { KotlinSingleton2() }
    }
}

对于 Delegates.observable 以及 map 在 Android 使用场景较少,我们只需要了解语法即可

class User {
    var name: String by Delegates.observable("默认值") {
        // 被赋值的属性、旧值与新值:
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    val user = User()
    user.name = "first"
}

输出结果:
  默认值 -> first

map
一个常见的用例是在一个映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))

println(user.name) 
println(user.age) 

输出结果:
  John Doe
  25

KTX

前面说了这么多我们趁热打铁看看 google 是如何利用 Kotlin 这些特性的吧

Frgament:

// 获取以 fragment 孵化出来的 viewModel
val model by viewModels<TestViewModel>()

// 由于是 Fragment 的扩展函数,自然就可以调用 requireActivity()
// ?= null 等价于不传入参数就是 null
inline fun <reified VM : ViewModel> Fragment.activityViewModels(
    noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { requireActivity().viewModelStore },
    factoryProducer ?: { requireActivity().defaultViewModelProviderFactory })

...

fun <VM : ViewModel> Fragment.createViewModelLazy(
    viewModelClass: KClass<VM>,
    storeProducer: () -> ViewModelStore,
    factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }
    return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}

...

// 利用 lazy 延迟加载属性
class ViewModelLazy<VM : ViewModel> (
    private val viewModelClass: KClass<VM>,
    private val storeProducer: () -> ViewModelStore,
    private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {
    private var cached: VM? = null

    override val value: VM
        get() {
            val viewModel = cached
            return if (viewModel == null) {
                val factory = factoryProducer()
                val store = storeProducer()
                ViewModelProvider(store, factory).get(viewModelClass.java).also {
                    cached = it
                }
            } else {
                viewModel
            }
        }

    override fun isInitialized() = cached != null
}

// 获取以 activity 孵化出来的 viewModel
val model2 by activityViewModels<TestViewModel>()

协程

可能很多同学之前有了解过协程,网上也一直在吹鼓协程的性能,由于我脑内脑补了过多关于协程的林林总总导致学起来异常吃力,所以这次再开始学习之前,我先给出个人对协程的定义:

  • 协程也拥有自己的生命周期,会内存泄露
  • 协程内部基于线程池
  • 协程的性能并不一定高
  • 协程是为了以同步的方式写异步代码
  • 协程可以直接代替 Callback

除此之外我们还要了解一个概念,协程需要一个协程构建器来启动它,协程的生命周期基于它的构建器
基于这些我们接着看下去,为了避免内存泄露下面的代码都基于 ktx 框架提供给我们的函数

lifecycleScope.launch{
    delay(2000)
    mTvContent.text = "过了2秒"
}

试想一下上面的代码会发生什么事情。

  1. 闪退
  2. 主线程卡2秒后更新 UI
  3. 不会卡顿主线程过俩秒后 UI 被刷新

运行代码后,我们会发现主线程没有出现卡顿,并且俩秒后 Ui 被刷新了,这是为什么呢?为此,我们去 ktx 的代码中寻找答案

val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                // 请注意这一句
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

我们在源码中可以看到 newScope 定义成 Dispatchers.Main.immediate 的,我们不用清楚那么多的细节,只需要知道这样让 lifecycleScope 这个协程的作用域定义成主程序的即可,那这样我们又有新的疑问了,既然是主线程的,我们为什么 delay(2000) 不会导致卡顿呢?

这就是我们协程的第二个知识点被 suspend 修饰过的函数被称为挂起函数,这时候想想我们最开始给协程的定义“协程内部基于线程池”这句话把它拆的再细一些,就可以这样说“基于线程池的轻量线程”简单一些理解就是,Kotlin 在 java 线程池的基础上有搭建了一套线程就是所谓的协程,那么这样不管我们怎么阻塞协程都不会影响到我们的线程了。

那这时候肯定又有疑问了,如果像网络请求这种不能运行在主线程之上的耗时操作要怎么处理呢?这时候就轮到 withContext 出场了

lifecycleScope.launch{
    delay(2000)
    getSoSolder()
}

private suspend fun getSoSolder(): SoSolder = withContext(Dispatchers.IO) {
    delay(3000L)
    SoSolder()
}

withContext 本质上是一个线程调度器,Kotlin 默认帮我们声明了一些调度器,这就是我前面说的“协程的性能并不一定高”,协程本身并不能在不同线程调度中带来多少优势,更多的是能让我们从人的角度来提升性能,而不是从语言的角度。

与基于回调的等效实现相比,withContext()不会增加额外的开销。此外,在某些情况下,还可以优化 withContext() 调用,使其超越基于回调的等效实现。例如,如果某个函数对一个网络进行十次调用,您可以使用外部 withContext() 让 Kotlin 只切换一次线程。这样,即使网络库多次使用 withContext(),它也会留在同一调度程序上,并避免切换线程。此外,Kotlin 还优化了 Dispatchers.DefaultDispatchers.IO 之间的切换,以尽可能避免线程切换。--- google

所以对我来说,协程更多的是为了让我能够“以同步的方式编写异步代码”

lifecycleScope.launch {
    // 开始协程:主线程
    var userInfo = getNetworkUserInfo()                 // 网络请求:IO 线程
    var forumInfo = getNetworkUserForumInfo()           // 网络请求:IO 线程
    var appcinfigInfo = getNetworkAppconfigInfo()       // 网络请求:IO 线程
    mNickName.text = userInfo?.name ?: ""
    // 更新 UI:主线程
    mTitle.text = forumInfo?.title
}

我们还可以利用 Kotlin 的扩展函数对网络请求进行“结构化并发机制”

 lifecycleScope.launch {
     val deferreds = listOf(    
             async { getNetworkUserInfo() }, 
             async { getNetworkUserForumInfo() }  
     )
    deferreds.awaitAll() 
 }

在我们使用协程的过程中往往会有队列的需求,Kotlin 也为我们提供了一个协程的互斥锁

class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) {
   val singleRunner = SingleRunner()

   suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> {
      // 开始新的任务之前,等待之前的排序任务完成
       return singleRunner.afterPrevious {
           if (ascending) {
               productsDao.loadProductsByDateStockedAscending()
           } else {
               productsDao.loadProductsByDateStockedDescending()
           }
       }
   }
}


class SingleRunner {

    private val mutex = Mutex()
    
    suspend fun <T> afterPrevious(block: suspend () -> T): T {
        mutex.withLock {
            return block()
        }
    }
}

经过简单的封装,我们就能得到一个协程队列。

KTX 中的协程

在学完了协程之后,我们可以看看 Ktx 中帮我们封装了那些协程,首当其冲的就是LiveData
在以下示例中,requestNetwork() 是在其他地方声明的 suspend 函数。 可以使用 liveData 构建器函数异步调用 requestNetwork(),然后使用 emit() 来发出结果:

var user = liveData {
    emit(requestNetwork())
}

fun <T> liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    @BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

在 LiveData 被监听时会触 onActive() 函数而后触发 maybeRun() 函数,最终触发我们定义的方法块

fun maybeRun() {
    cancellationJob?.cancel()
    cancellationJob = null
    if (runningJob != null) {
        return
    }
    runningJob = scope.launch {
        val liveDataScope = LiveDataScopeImpl(liveData, coroutineContext)
        block(liveDataScope)
        onDone()
    }
}

当然我们不只希望 LiveData 只获取一次参数

private val page = MutableLiveData<Int>()
val networkData = page.switchMap {
    liveData { emit(requestNetwork(it)) }
}

....

fun refresh() {
    page.value = 1
}

fun loadMore() {
    page.value += 1
}

....

这样每当我们调用loadMore之后会触发switchMap回调然后从服务端获取相应 page 的数据,进而让networkData触发 UI 界面的刷新。

以上就是本次所有的所有内容了,感谢阅读。

参考资料

[1]Kotlin 协程真的比 Java 线程更高效吗?
[2]Coroutines 协程

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容