风之旅人 Android Dev Engineer

Kotlin学习笔记


参考:码上开学

val 「只读变量」 var 「可变变量」

基本类型拆装箱

Kotlin 里的 Int 和 Java 里的 int 以及 Integer 不同,主要是在装箱方面不同。

1.Java 里的 int 是 unbox 的,而 Integer 是 box 的:

☕️
int a = 1;
Integer b = 2; // 👈会被自动装箱 autoboxing
Java
Kotlin 里,Int 是否装箱根据场合来定:

🏝️
var a: Int = 1 // unbox
var b: Int? = 2 // box
var list: List<Int> = listOf(1, 2) // box


2.Java 中的数组和 Kotlin 中的数组的写法也有区别:

☕️
int[] array = new int[] {1, 2};
Java
而在 Kotlin 里,上面的写法是这样的:

🏝️
var array: IntArray = intArrayOf(1, 2)
// 👆这种也是 unbox 的

Java 里的基本类型,类比到 Kotlin 里面,条件满足如下之一就不装箱:

  • 1.不可空类型。
  • 2.使用 IntArray、FloatArray 等。

Kotlin 处理强转错误的方式

🏝️
fun main() {
    var activity: Activity = NewActivity()
    // 👇'(activity as? NewActivity)' 之后是一个可空类型的对象,所以,需要使用 '?.' 来调用
    (activity as? NewActivity)?.action()
}

思考:

1.子类重写父类的 override 函数,能否修改它的可见性? 2.以下的写法有什么区别?

🏝️
activity as? NewActivity
activity as NewActivity?
activity as? NewActivity?

🍎

@Test
   fun test() {
       val str = "hello"
       //str as Context
       //str as? Context
       str as? Context?

       /*
       1.直接抛异常 : java.lang.ClassCastException: java.lang.String cannot be cast to android.content.Context
       2.通过
       3.通过

       此时 : val str="hello"  -> val str=null
       str as? Context? 和 str 是等效的
        */

   }

val自定义 getter

不过 val 和 final 还是有一点区别的,虽然 val 修饰的变量不能二次赋值,但可以通过自定义变量的 getter 函数,让变量每次被访问时,返回动态获取的值:

🏝️
👇
val size: Int
    get() { // 👈 每次获取 size 值时都会执行 items.size
        return items.size
    }

不过这个属于 val 的另外一种用法,大部分情况下 val 还是对应于 Java 中的 final 使用的。

Kotlin 中的 object

  • 创建一个类,并且创建一个这个类的对象。这个就是 object 的意思:对象。
  • 创建单例 , 只需要把 class 换成 object 就可以了。
  • object 实现的单例是一个饿汉式的单例,并且实现了线程安全。
  • Java 中的静态初始化
    static{ ... }
    🏝️
    Java 中的静态变量和方法,在 Kotlin 中都放在了 companion object 中。因此 Java 中的静态初始化在 Kotlin 中自然也是放在 companion object 中的,像类的初始化代码一样,由 init 和一对大括号表示:
    🏝️
    class Sample {
         👇
      companion object {
           👇
          init {
              ...
          }
      }
    }
    

    那在实际使用中,在 object、companion object 和 top-level 中该选择哪一个呢?简单来说按照下面这两个原则判断:

  • 如果想写工具类的功能,直接创建文件,写 top-level「顶层」函数。
  • 如果需要继承别的类或者实现接口,就用 object 或 companion object。

Kotlin常量和Java常量的区别

发现不同点有:

Kotlin 的常量必须声明在对象(包括伴生对象)或者「top-level 顶层」中,因为常量是静态的。
Kotlin 新增了修饰常量的 const 关键字。
除此之外还有一个区别:

Kotlin 中只有基本类型和 String 类型可以声明成常量。
原因是 Kotlin 中的常量指的是 「compile-time constant 编译时常量」,它的意思是「编译器在编译的时候就知道这个东西在每个调用处的实际值」,因此可以在编译时直接把这个值硬编码到代码里使用的地方。


Java 中的常量可以认为是「伪常量」,因为可以通过上面这种方式改变它内部的值。而 Kotlin 的常量因为限制类型必须是基本类型,所以不存在这种问题,更符合常量的定义。

Kotlin mutable 前缀注意问题

listOf() 创建不可变的 List,mutableListOf() 创建可变的 List。可通过 toMutableList 由不可变转成可变集合:
🏝️
val strList = listOf("a", "b", "c")
            👇
strList.toMutableList()

👉 toMutable*() 返回的是一个新建的集合,原有的集合还是不可变的,所以只能对函数返回的集合修改。

可见性修饰符

  • public :公开,可见性最大,哪里都可以引用。
  • private:私有,可见性最小,根据声明位置不同可分为类中可见和文件中可见。
  • protected:保护,相当于 private + 子类可见。
  • internal:内部,仅对 module 内可见。

Kotlin 中如果不写可见性修饰符,就表示公开,和 Java 中 public 修饰符具有相同效果。在 Kotlin 中 public 修饰符「可以加,但没必要」。

internal 表示修饰的类、函数仅对 module 内可见,这里的 module 具体指的是一组共同编译的 kotlin 文件,常见的形式有:

  • Android Studio 里的 module
  • Maven project

private Java 中的 private 表示类中可见,作为内部类时对外部类「可见」。 Kotlin 中的 private 表示类中或所在文件内可见,作为内部类时对外部类「不可见」

🌰

在 Java 中,外部类可以访问内部类的 private 变量:

☕️
public class Outter {
    public void method() {
        Inner inner = new Inner();
                            👇
        int result = inner.number * 2; // success
    }

    private class Inner {
        private int number = 0;
    }
}
Java
在 Kotlin 中,外部类不可以访问内部类的 private 变量:

🏝️
class Outter {
    fun method() {
        val inner = Inner()
                            👇
        val result = inner.number * 2 // compile-error: Cannot access 'number': it is private in 'Inner'
    }

    class Inner {
        private val number = 1
    }
}

高阶函数和Lambda表达式

课件 👉 https://kaixue.io/kotlin-lambda/ Bilibili 👉 https://www.bilibili.com/video/av96768809

概念

「参数或者返回值为函数类型的函数」,在 Kotlin 中就被称为「高阶函数」—— Higher-Order Functions。 ⭐ Kotlin中双冒号函数,匿名函数和Lambda表达式本质上都是函数类型的对象。

高阶函数

双冒号 :: 把声明好的函数变成函数类型的对象 :

fun b(param: Int): String {
    return ""
}
val d = ::b
val e = d

对象是不能加个括号来调用的,对吧?但是函数类型的对象可以。为什么?因为这其实是个假的调用,它是 Kotlin 的语法糖,实际上你对一个函数类型的对象加括号、加参数,它真正调用的是这个对象的 invoke() 函数:

d(1) // 实际上会调用 d.invoke(1)
(::b)(1) // 实际上会调用 (::b).invoke(1)

包括双冒号加上函数名的这个写法,它是一个指向对象的引用,但并不是指向函数本身,而是指向一个我们在代码里看不见的对象。这个对象复制了原函数的功能,但它并不是原函数。

协程

一种线程框架。

创建协程

🏝️
// 方法一,使用 runBlocking 顶层函数
runBlocking {
    getImage(imageId)
}

// 方法二,使用 GlobalScope 单例对象
//            👇 可以直接调用 launch 开启协程
GlobalScope.launch {
    getImage(imageId)
}

// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
//                                    👇 需要一个类型为 CoroutineContext 的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    getImage(imageId)
}
  • 方法一通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。

  • 方法二和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会和 app 一致,且不能取消(什么是协程的取消后面的文章会讲)。

  • 方法三是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。

withContext

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

🏝️
launch(Dispatchers.Main) {              // 👈 在 UI 线程开始
    val image = getImage(imageId)
    avatarIv.setImageBitmap(image)     // 👈 执行结束后,自动切换回 UI 线程
}
//                               👇
suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
    ...
}

合并网络请求并更新UI

🏝️ 「协作式任务」
coroutineScope.launch(Dispatchers.Main) {
    //            👇  async 函数之后再讲
    val avatar = async { api.getAvatar(user) }    // 获取用户头像
    val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
    val merged = suspendingMerge(avatar, logo)    // 合并结果
    //                  👆
    show(merged) // 更新 UI
}

项目中配置对 Kotlin 协程的支持

  • 项目根目录下的 build.gradle :
buildscript {
    ...
    // 👇
    ext.kotlin_coroutines = '1.3.1'
    ...
}
  • Module 下的 build.gradle :
dependencies {
    ...
    //                                       👇 依赖协程核心库
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
    //                                       👇 依赖当前平台所对应的平台库
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
    ...
}

launch 与 async

接下来我们主要来对比 launch 与 async 这两个函数。

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

  • 不同点:async 返回的 Coroutine 多实现了 Deferred 接口。

关于 Deferred 更深入的知识就不在这里过多阐述,它的意思就是延迟,也就是结果稍后才能拿到。

我们调用 Deferred.await() 就可以得到结果了。

接下来我们继续看看 async 是如何使用的,先回忆一下上期中获取头像的场景:

🏝️
coroutineScope.launch(Dispatchers.Main) {
    //                      👇  async 函数启动新的协程
    val avatar: Deferred = async { api.getAvatar(user) }    // 获取用户头像
    val logo: Deferred = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
    //            👇          👇 获取返回值
    show(avatar.await(), logo.await())                     // 更新 UI
}

可以看到 avatar 和 logo 的类型可以声明为 Deferred ,通过 await 获取结果并且更新到 UI 上显示。

await 函数签名如下:

🏝️
public suspend fun await(): T

「挂起」的本质

一个稍后会被自动切回来的线程调度操作。

挂起的对象是协程。

从当前线程挂起。换句话说,就是这个协程从正在执行它的线程上脱离。注意,不是这个协程停下来了!是脱离,当前线程不再管这个协程要去做什么了。

suspend 是有暂停的意思,但我们在协程中应该理解为:当线程执行到协程的 suspend 函数的时候,暂时不继续执行协程代码了。

我们先让时间静止,然后兵分两路,分别看看这两个互相脱离的线程和协程接下来将会发生什么事情:

1.线程

前面我们提到,挂起会让协程从正在执行它的线程上脱离,具体到代码其实是:

协程的代码块中,线程执行到了 suspend 函数这里的时候,就暂时不再执行剩余的协程代码,跳出协程的代码块。

那线程接下来会做什么呢?

如果它是一个后台线程:

  • 要么无事可做,被系统回收
  • 要么继续执行别的后台任务

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

如果这个线程它是 Android 的主线程,那它接下来就会继续回去工作:也就是一秒钟 60 次的界面刷新任务。

一个常见的场景是,获取一个图片,然后显示出来:

🏝️
// 主线程中
GlobalScope.launch(Dispatchers.Main) {
  val image = suspendingGetImage(imageId)  // 获取图片
  avatarIv.setImageBitmap(image)           // 显示出来
}

suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
  ...
}

这段执行在主线程的协程,它实质上会往你的主线程 post 一个 Runnable,这个 Runnable 就是你的协程代码:

🏝️
handler.post {
  val image = suspendingGetImage(imageId)
  avatarIv.setImageBitmap(image)
}

当这个协程被挂起的时候,就是主线程 post 的这个 Runnable 提前结束,然后继续执行它界面刷新的任务。

2.协程

它从 suspend 函数开始脱离启动它的线程,继续执行在 Dispatchers 所指定的 IO 线程。

紧接着在 suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来。

这个「切回来」是什么意思?

我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers 切换到了 IO 线程;

当这个函数执行完毕后,线程又切了回来,「切回来」也就是协程会帮我再 post 一个 Runnable,让我剩下的代码继续回到主线程去执行。

「非阻塞式」挂起指的是用看起来阻塞的代码写出非阻塞式操作的。

协程练习

使用协程下载一张图,并行进行两次切割

  • 一次切成大小相同的 4 份,取其中的第一份
  • 一次切成大小相同的 9 份,取其中的最后一份

得到结果后,将它们展示在两个 ImageView 上。

GlobalScope.launch(Dispatchers.Main) {
    Log.w("coroutines","launch ${Thread.currentThread().name}")
    val bitmap = getSuspendImage()
    Log.w("coroutines","launch getImage Ok")
    iv1.setImageBitmap(bitmap)
    val bitmapHalf2 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width / 2, bitmap.height / 2)
    iv2.setImageBitmap(bitmapHalf2)
    val bitmapHalf3 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width / 3, bitmap.height / 3)
    iv3.setImageBitmap(bitmapHalf3)
}

private suspend fun getSuspendImage(): Bitmap = withContext(Dispatchers.IO) {
    Log.w("coroutines","suspend ${Thread.currentThread().name}")
    OkHttpClient()
            .newCall(Request.Builder()
                    .url("http://oss.tuyuing.com/TUYU/trend/20190930/trend257401569854904487.jpeg")
                    .get()
                    .build())
            .execute().body?.byteStream().use {
                Log.w("coroutines","suspend getImage Ok")
                BitmapFactory.decodeStream(it)
            }
}

Log :
W/coroutines: launch main
W/coroutines: suspend DefaultDispatcher-worker-1
W/coroutines: suspend getImage Ok
W/coroutines: launch getImage Ok

效果图 :

2020-04-22-Kotlin手册-2e6fe29e.png

扩展函数和扩展属性

扩展函数和扩展属性(Extension Functions / Properties), 在不改变既有代码的情况下扩展函数或扩展对象。

(Int::toFloat)(1)
Int::toFloat.invoke(1)
1.toFloat()
1.toFloat().invoke()

🌰 Global.kt

//扩展函数
fun String.method1(i: Int) {}

//扩展属性
val Float.dp
    get() = TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP,
        this,
        Resources.getSystem().displayMetrics
    )

MainActivity.kt

val str: String.(Int) -> Unit = String::method1
val str2: (String, Int) -> Unit = String::method1
fun strTest() {
    str("hello", 1)
    str.invoke("hello", 1)
    "".method1(1)
}

//also
fun strTest2() {
    fun String.method1(i: Int) {
    }
    val str: String.(Int) -> Unit = String::method1
    val str2: (String, Int) -> Unit = String::method1
    "hello".method1(100)
}

其中val str2: (String, Int) -> Unit = String::method1(String, Int) 第一个参数是调用者,往后是参数

注: Kotlin不允许使用既是成员函数又是扩展函数的函数。

此外 , 普通的无Receiver的函数也可以赋值给有Receiver的变量 :

Global.kt
fun method2(s:String,i: Int) {}  比 method1 多了一个 String 参数

MainActivity.kt
val str3: String.(Int) -> Unit = ::method2
val str4: (String, Int) -> Unit = ::method2

.


评论