Android常见面试题集锦(六)

380 阅读34分钟

kotlin篇

1. Kotlin 与 Java 对比

Kotlin 和 Java 都是针对 JVM 的编程语言。它们有一些相似之处,比如都支持面向对象编程、静态类型和垃圾回收等。但是 Kotlin 和 Java 也有很多不同之处。以下是一些 Kotlin 和 Java 的比较:

  1. 代码量:Kotlin 比 Java 代码量少很多。Kotlin 通过使用更简洁的语法和函数式编程的概念来简化 Java 代码,以减少代码的复杂性。
  2. 空指针安全:Kotlin 通过引入空指针安全机制来避免空指针异常,而 Java 中需要手动检查 null 值。
  3. 扩展函数:Kotlin 中有一个强大的功能叫做扩展函数,它允许用户将一个已存在的类进行扩展。
  4. 函数式编程概念:Kotlin 支持更多的函数式编程概念,比如 lambda 表达式、高阶函数和尾递归等。
  5. 数据类:Kotlin 中引入了数据类,它允许程序员快速创建简单的数据类。相比之下,Java 需要编写大量的样板代码。

总的来说,Kotlin 相对于 Java 拥有更简洁的语法,更少的瑕疵,更多的功能和更高的生产效率,但是 Java 相对于 Kotlin 拥有更成熟的生态体系,更广泛的支持和更好的跨平台支持。

2. Kotlin 常见关键字

  • companion:伴生对象,可以在类内部定义一个对象,用于实现静态方法和属性。

  • data:数据类,用于快速创建一个用于存储数据的类。

  • by:委托,可以在一个对象中使用另一个对象的属性或方法。

  • reified:具体化,用于解决 Java 泛型擦除问题。

  • inline:内联,用于在编译时将函数代码插入到调用处,提高性能。

  • non-local return:非局部返回,可以在嵌套函数中使用 return 关键字返回到外部函数。

  • tailrec:尾递归,用于将递归函数改为尾递归函数,提高性能。

  • suspend 和 coroutine:协程,Kotlin 支持协程编程,可以使用 suspend 关键字定义挂起函数,使用 coroutine 构建异步和并发程序。

3. Kotlin 常见修饰符

image.png

4. Kotlin 常见内置函数

  1. let 函数,必须让某个对象调用,接收一个 Lambda 表达式参数,Lambda 表达式中的参数为当前调用者,且最后一行代码作为返回值

  2. also 函数,必须让某个对象调用,接收一个 Lambda 表达式参数,Lambda 表达式中的参数为当前调用者,无法指定返回值,这个函数返回的是当前调用对象本身

  3. with 函数,接收两个参数,第一个为任意类型参数,第二个为 Lambda 表达式参数,Lambda 表达式中拥有第一个参数的上下文 this ,且最后一行代码作为返回值

  4. run 函数,必须让某个对象调用,接收一个 Lambda 表达式参数,Lambda 表达式中拥有当前调用对象的上下文 this ,且最后一行代码作为返回值

  5. apply 函数,必须让某个对象调用,接收一个 Lambda 表达式参数,Lambda 表达式中拥有当前调用对象的上下文 this ,无法指定返回值,这个函数返回的是当前调用对象本身

image.png

image.png image.png

image.png

image.png

其实上面 5 个标准函数有很多相似的地方,我们需搞清楚它们差异之处,下面我们用一个图表来总结一下:

image.png

5. Kotlin 与 RxJava

Kotlin是一种现代的编程语言,它对函数式编程和响应式编程提供了很好的支持。RxJava也是一种非常流行的响应式编程库。虽然Kotlin本身没有RxJava那么强大,但它提供了一些工具和语言功能来简化异步编程和响应式编程。下面是一些使用Kotlin替代RxJava的技术:

  1. 协程:Kotlin提供了一种名为协程的轻量级线程,可以简化异步编程。协程使用类似于JavaScript的async/await语法,允许您轻松地编写异步代码而无需编写回调或使用RxJava。
  2. Flow:Kotlin的流是一种响应式编程的替代方案。它提供了与RxJava的Observable类似的流式API,但它是基于协程的,并且更容易与Kotlin集成。
  3. LiveData:LiveData是一种Kotlin Android架构组件,它提供了类似于RxJava的观察者模式。LiveData可以让您轻松地观察数据变化,同时避免RxJava的一些复杂性和性能问题。

总之,Kotlin提供了许多替代RxJava的工具和功能,从而使异步编程和响应式编程更加简单和直观。

6. Kotlin 协程

以下是一些与Kotlin协程相关的面试题和答案:

  1. 什么是Kotlin协程?

答:Kotlin协程是一种轻量级的线程,它使用协作式调度来实现并发。与传统的线程不同,协程可以自由地挂起和恢复。它们使并发代码更加轻松和直观,并且可以避免一些常见的并发问题。

  1. Kotlin协程的优点是什么?

答:Kotlin协程的优点包括:

  • 简单易用:协程使异步代码更加轻松和直观,而无需编写复杂的回调或使用RxJava。
  • 轻量级:协程使用协作式调度,因此它们比传统线程更加轻量级。
  • 避免共享状态问题:协程通过将计算任务拆分为许多小的、非共享的组件来避免共享状态问题。
  • 更好的性能:因为协程是轻量级的,它们的创建和销毁所需的开销更小,因此具有更好的性能。
  1. Kotlin协程中的“挂起”意味着什么?

答:在Kotlin协程中,挂起是指暂停协程的执行,直到某些条件满足。在挂起期间,协程不会占用线程,并且可以由另一个协程或线程执行。协程通常在遇到I/O操作或长时间运行的计算时挂起。

  1. 如何在Kotlin中创建协程?

答:在Kotlin中,可以使用launch、async和runBlocking等函数来创建协程。例如:

image.png

  • launch是非阻塞的,它启动一个协程并返回一个Job对象。
  • async也是非阻塞的,但返回一个包含结果的Deferred对象。
  • runBlocking是阻塞的,它启动一个协程并阻塞当前线程直到协程完成。
  1. Kotlin中的“协程作用域”是什么?

答:Kotlin中的“协程作用域”是一个用于管理协程的上下文环境的接口,它提供了协程的启动和取消操作的上下文。协程作用域定义了协程的生命周期,并决定了协程在何时启动、在何时取消。

在实际开发中,我们可以使用CoroutineScope接口来创建一个协程作用域,然后在其中创建和启动协程。同时,我们也可以通过协程作用域来管理这些协程的生命周期,并对它们进行统一的异常处理。

协程作用域的主要作用包括:

  1. 管理协程的生命周期:通过协程作用域,我们可以创建和启动一组协程,同时也可以在需要的时候取消它们的执行。
  2. 定义协程的上下文:协程作用域可以定义协程的上下文,包括运行所在的线程、协程的异常处理方式、协程调度器等。
  3. 管理协程的执行方式:协程作用域可以控制一组协程的执行方式,比如并发执行或顺序执行等。

总之,Kotlin中的“协程作用域”是一个用于管理协程的上下文环境的接口,它提供了协程的启动和取消操作的上下文,并定义了协程的生命周期和执行方式。

  1. Kotlin协程中的“挂起函数”是什么?

答:挂起函数是指可以在协程中使用的特殊函数,它们可以在执行过程中暂停协程的执行,直到某些条件满足。通常,挂起函数通过使用“挂起标记”(suspend)来定义。

  1. 如何处理Kotlin协程中的异常?

答:在Kotlin协程中,可以使用try/catch语句来处理异常。如果协程中的异常未被捕获,它将传播到协程的上层。可以使用CoroutineExceptionHandler在协程中设置一个全局异常处理程序。例如:

image.png

7. Kotlin 泛型-逆变/协变

Kotlin中的泛型支持协变和逆变。接下来分别对它们进行介绍:

  1. 协变(Covariant)

协变意味着可以使用子类型作为父类型的替代。在Kotlin中,为了支持协变,我们可以将out修饰符添加到泛型参数上。例如,让我们看一个用于生产者的接口:

image.png

这个接口可以使用out修饰符,表示这是一个生产者,它只会产生类型T的值,而不会对其进行任何更改。因此,我们可以将子类型作为父类型的替代:

image.png

这里DogAnimal的子类型,所以我们可以使用DogProducer作为类型为Producer<Animal>的变量的值。因为我们知道我们总是可以期望DogProducer生产类型为Animal的值。

  1. 逆变(Contravariant)

逆变意味着可以使用父类型作为子类型的替代。在Kotlin中,为了支持逆变,我们可以将in修饰符添加到泛型参数上。例如,让我们看一个用于消费者的接口:

image.png

这个接口可以使用in修饰符,表示这是一个消费者,它只接受类型T的值,而不会返回任何值。因此,我们可以将父类型作为子类型的替代:

image.png

这里AnimalDog的父类型,所以我们可以使用AnimalConsumer作为类型为Consumer<Dog>的变量的值。因为我们知道我们总是可以期望AnimalConsumer会接受类型为Dog的值。

总之,Kotlin中的协变和逆变提供了更好的类型安全性和代码灵活性。使用它们可以确保类型转换是正确的,并且可以使程序更加健壮和易于维护。

8. 在 Kotlin 中,何为解构?该如何使用?

给一个包含N个组件函数(component)的对象分解为替换等于N个变量的功能,而实现这样功能只需要一个表达式就可以了。 例如 有时把一个对象 解构 成很多变量会很方便,例如: val (name, age) = person 这种语法称为 解构声明 。一个解构声明同时创建多个变量。 我们已经声明了两个新变量: name 和 age,并且可以独立使用它们: println(name) println(age) 一个解构声明会被编译成以下代码: val name = person.component1() val age = person.component2()

9. 在 Kotlin 中,什么是内联函数?有什么作用?

对比java 函数反复调用时,会有压栈出栈的性能消耗

kotlin优化 内联函数 用来解决 频繁调用某个函数导致的性能消耗

使用 inline标记 内联函数,调用非内联函数会报错,,需要加上noinline标记

noinline,让原本的内联函数形参函数不是内联的,保留原有数据特征

crossinline 非局部返回标记 为了不让lamba表达式直接返回内联函数,所做的标记 相关知识点:我们都知道,kotlin中,如果一个函数中,存在一个lambda表达式,在该lambda中不支持直接通过return退出该函数的,只能通过return@XXXinterface这种方式

reified 具体化泛型 java中,不能直接使用泛型的类型 kotlin可以直接使用泛型的类型

使用内联标记的函数,这个函数的泛型,可以具体化展示,所有 能解决方法重复的问题

10. 谈谈Kotlin中的构造方法?有哪些注意事项?

参考答案:

一、概要简述

  1. kotlin中构造函数分为主构造次级构造两类
  2. 使用关键词constructor标记构造函数,部分情况可省略
  3. init关键词用于初始化代码块,注意与构造函数的执行顺序,类成员的初始化顺序
  4. 继承,扩展时候的构造函数调用逻辑
  5. 特殊的类如data classobject/componain objectsealed class等构造函数情况与继承问题
  6. 构造函数中的形参声明情况

二、详细说明

  • 主/次 构造函数

    1. kotlin中任何class(包括object/data class/sealed class)都有一个默认的无参构造函数
    2. 如果显式的声明了构造函数,默认的无参构造函数就失效了。
    3. 主构造函数写在class声明处,可以有访问权限修饰符private,public等,且可以省略constructor关键字。
    4. 若显式的在class内声明了次级构造函数,就需要委托调用主构造函数。
    5. 若在class内显式的声明处所有构造函数(也就是没有了所谓的默认主构造),这时候可以不用依次调用主构造函数。例如继承View实现自定义控件时,三四个构造函数同时显示声明。
  • init初始化代码块

    kotlin中若存在主构造函数,其不能有代码块执行,init起到类似作用,在类初始化时侯执行相关的代码块。

    1. init代码块优先于次级构造函数中的代码块执行。
    2. 即使在类的继承体系中,各自的init也是优先于构造函数执行。
    3. 在主构造函数中,形参加有var/val,那么就变成了成员属性的声明。这些属性声明是早于init代码块的。
  • 特殊类

    1. object/companion object是对象示例,作为单例类或者伴生对象,没有构造函数。
    2. data class要求必须有一个含有至少一个成员属性的主构造函数,其余方面和普通类相同。
    3. sealed class只是声明类似抽象类一般,可以有主构造函数,含参无参以及次级构造等。

11. 请谈谈 Kotlin 中的 Coroutines,它与线程有什么区别?有哪些优点?

参考答案:

先列出协程几个特点: 1,在单个进程内,多个协程串行执行,只挂起不阻塞 2,协程最终的执行还是在各个线程之中

优点: 1,由于不阻塞线程,异步任务是编译器主动交到线程池中执行。因此,在异步任务执行上,切换和消耗的资源都较少。 2,由于协程是跨多个线程,并且能够保持串行执行;因此,在处理多并发的情况上,能够比锁更轻量级。通过状态量实现

12. 说说 Kotlin中 的 Any 与Java中的 Object 有何异同?

同:

  • 都是顶级父类 异:
  • 成员方法不同 Any只声明了toString()、hashCode()和equals()作为成员方法。

我们思考下,为什么 Kotlin 设计了一个 Any ?

当我们需要和 Java 互操作的时候,Kotlin 把 Java 方法参数和返回类型中用到的 Object 类型看作 Any,这个 Any 的设计是 Kotlin 兼容 Java 时的一种权衡设计。

所有 Java 引用类型在 Kotlin 中都表现为平台类型。当在 Kotlin 中处理平台类型的值的时候,它既可以被当做可空类型来处理,也可以被当做非空类型来操作。

试想下,如果所有来自 Java 的值都被看成非空,那么就容易写出比较危险的代码。反之,如果 Java 值都强制当做可空,则会导致大量的 null 检查。综合考量,平台类型是一种折中的设计方案。

13. Kotlin中的数据类型有隐式转换吗?为什么?

参考答案:

kotlin中没有所谓的'基本类型',本质上不存在拆装箱过程,所有的基本类型都可以想象成Java中的包装类型,所以也不存在隐式转换,对应的都是强类型,一旦声明之后,所有转换都是显示转换。

14. 分别通过对象表达式 object 和 lambda 表达式实现的函数式接口内部有何不同?

  1. object是匿名内部类的形式,匿名内部类是在编译后形成一个class

  2. Lambda表达式是在程序运行的时候动态生成class

15. 内联函数和高阶函数

  • 内联函数:编译时把调用代码插入到函数中,避免方法调用的开销。
  • 高阶函数:接受一个或多个函数类型的参数,并/或返回一个函数类型的值

概念就这两句话,实际使用的时候却有很大的用途。比如我们常用的apply、run、let这些其实就是一个内联高阶函数。

image.png

16. 对委托的理解

首先委托的概念就是把一个对象的职责委托给另外一个对象,在kotlin中有属性的委托和类的委托。属性的委托比如by lazy,他的作用是使用到的时候才加载简化了判空代码也节省了性能。类的委托通常是一个接口委托一个对象interface by Class。目的是对一个类的解耦方便以后相同功能的代码复用。例子就不举例了,就是但凡开发中想到有些代码是可以复用的时候可以考虑能不能写成一个接口去交给委托类去实现。

问到by lazy可能还会问你与lateinit的区别。

  • lateinit:延时加载,只是告诉编译器不用检查这个变量的初始化,不能使用val修饰
  • by lazy:懒加载,lazy是一个内联高阶函数,通过传入自身来做一些初始化的判断。

17. 扩展方法以及其原理

实际开发中我们的点击事件、资源获取等都可以使用。好处就不多说了,比如加入防抖,或者获取资源时的捕获异常,都可以减少日后添加需求时的开发量

image.png

  • 原理 Kotlin 中的扩展方法其实是一种静态的语法糖,本质上是一个静态函数,不是实例函数。编译器会将扩展方法转化为静态函数的调用。 比如:

image.png

18. Jetpack使用过哪些库

  1. ViewModel:用于在屏幕旋转或其他配置更改时管理UI数据的生命周期。
  2. LiveData:用于将数据从ViewModel传递到UI组件的观察者模式库。
  3. Room:用于在SQLite数据库上进行类型安全的ORM操作的库。
  4. Navigation:用于管理应用程序导航的库。
  5. WorkManager:用于管理后台任务和作业的库。
  6. Paging:用于处理分页数据的库。
  7. Data Binding:用于将布局文件中的视图绑定到应用程序数据源的库。
  8. Hilt:用于实现依赖注入的库。
  9. Security:提供加密和数据存储的安全功能。
  10. Benchmark:用于测试应用程序性能的库。

19. LiveData和LifeCycle的原理

LiveData

使用上非常简单,就是上下游的通知,就是一个简化版的rxjava

  1. LiveData持有一个观察者列表,可以添加和删除观察者。
  2. 当LiveData数据发生变化时,会通知观察者列表中的所有观察者。
  3. LiveData可以感知Activity和Fragment的生命周期,当它们处于激活状态时才会通知观察者,避免了内存泄漏和空指针异常。
  4. LiveData还支持线程切换,可以在后台线程更新数据,然后在主线程中通知观察者更新UI。

LiveData提供了setValuepostValue两个方法来设置数据通知

  • setValue:方法只能在主线程调用,不依赖Handler机制来回调,
  • postValue:可以在任何线程调,同步到主线程依赖于Handler,需要等待主线程空闲时才会执行更新操作。
LifeCycle

用于监听生命周期,包含三个角色。LifecycleOwner、LifecycleObserver和Lifecycle

  • LifecycleObserver是Lifecycle的观察者。viewmodel默认就实现了这个接口
  • LifecycleOwner是具有生命周期的组件,如Activity、Fragment等,它持有一个Lifecycle对象
  • Lifecycle是LifecycleOwner的生命周期管理器,它定义了生命周期状态和转换关系,并负责通知LifecycleObserver状态变化的事件

了解这三个角色其实就很容易理解了,本质上LifeCycle也是一个观察者模式,管理数据的是LifeCycle,生命周期的状态都是通过它来完成的。而我们写代码的时候要写的一句是getLifecycle().addObserver(xxLifeCycleObserver());是添加一个观察者,这个观察者就能收到相应的通知了。

20. ViewModel的原理

这个问题有可能会问你Viewmodel跟Activity哪个先销毁、Viewmodel跟Activity是怎么进行生命周期的绑定的。

Viewmodel的两个重要类:ViewModelProviderViewmodelStore。其实就是我们使用时用到的

kotlin
复制代码
// 这里this接收的其实是一个`ViewModelStoreOwner`是一个接口,我们的AppCompatActivity已经实现了
aViewModel = ViewModelProvider(this).get(AViewModel::class.java)
  • ViewModelStore 是一个存储 ViewModel 的容器,用于存储与某个特定的生命周期相关联的 ViewModel

是一个全局的容器,实际上就是一个HashMap。

  • ViewModelProvider用于管理ViewModel实例的创建和获取

其实这里设计的理念也比较好理解,比如旋转屏幕这个场景,我们会使用Viewmodel来保存数据,因为他数据不会被销毁,之所以不被销毁不用想也只是肯定是脱离Activity或者Fragment保存的。

知道了Viewmodel会全局保存这一点,应该会有一些疑问,就是这个Viewmodel是什么时候回收的。

在Activity或者Fragment销毁其实只是移除了他的引用,当内存不足时gc会回收或者手动调用clear方法回收。所以回答Activity和Viewmodel谁的生命周期比较长时就知道了,只要不是手动清除肯定是ViewModel的生命周期比Activity长。

因为ViewModel一直存在,所以如果太多需要做一些优化,原则很简单,就是把ViewModel细分,有些没必要保存的手动清除,有些需要全局的就使用单例。

21. WorkManager的使用场景

其实就是一个定时任务,人家问你使用场景是看你有没有真正用过。

  1. 需要在特定时间间隔内执行后台任务,例如每天的定时任务或周期性的数据同步。
  2. 执行大型操作,例如上传或下载文件,这些操作需要时间较长,需要在后台执行。
  3. 应用退出时需要保存数据,以便在下一次启动时可以使用。
  4. 执行重复性的任务,例如日志记录或数据清理。

22. Navigation使用过程中有哪些坑

这个问题首先要明确Navigation是干嘛的才知道有什么坑

  • Navigation翻译过来是导航,其实就是一个管理Fragment的栈类似与我们使用Activity一样,样提供的方法也是一样的比如动画、跳转模式,并且它还可以让我们不用担心Fragment是否被回收直接调用它的跳转,没有的话会帮我们做视图的恢复数据它已经内部处理好了,还支持一些跳转的动画传参等都有相应的api。简而言之,Navigation能做的FragmentManager都能做,只是相对麻烦而已。
  • Navigation优势就不多说了,合适的场景就是线性的跳转,比如A跳B跳C跳D这种,直接一行代码就可以跳转。返回到指定的页面也有方法,比如从D返回到navController.popBackStack(R.id.fragmentA, false)。这里的ture和false要注意,具体的细节就去看官网了。
  • 不太适合的场景就是相互的调用,比如A跳B跳A跳B这种反复的,需要你设置好跳转模式,如果模式不对会出现反复的创建和销毁,这里使用SingleTop跳转模式可以解决。但是要处理的可能是你是什么地方跳过来是,返回方法要处理一下。

23. LiveData有遇到倒灌现象吗,怎样解决?

所谓的“数据倒灌”:其实是类似粘性广播那样,当新的观察者开始注册观察时,会把上次发的最后一次的历史数据传递给当前注册的观察者

原因:其实也很简单,其实就是 LiveData内部有一个mVersion字段,记录版本,其初始的 mVersion 是-1,当我们调用了其 setValue 或者 postValue,其 mVersion+1;对于每一个观察者的封装 ObserverWrapper,其初始 mLastVersion 也为-1,也就是说,每一个新注册的观察者,其 mLastVersion 为-1;当 LiveData 设置这个 ObserverWrapper 的时候,如果 LiveDatamVersion 大于 ObserverWrappermLastVersionLiveData 就会强制把当前 value 推送给 Observer

解决

(1)通过反射修改ObservermVersion版本号与LiveData的一致。

(2)替换LiveData,如使用ShareFlow或者EventBus。

(3)使用MediatorLiveData,通过addSource监听LiveData的数据变换,在onDestory时通过removeSource移除监听。

24. GlobalScope和viewModelScope的区别?

在Kotlin中,GlobalScopeviewModelScope都是用于管理协程的作用域,但它们之间有一些区别。

  1. 范围:GlobalScope是一个全局的作用域,它可以用于应用程序的任何地方。而viewModelScope则是一个与ViewModel关联的作用域,它只能在ViewModel内部使用。
  2. 生命周期:GlobalScope的生命周期与应用程序的生命周期相同,它会在应用程序启动时创建,并在应用程序销毁时销毁。而viewModelScope的生命周期与ViewModel的生命周期相同,它会在ViewModel创建时创建,并在ViewModel销毁时销毁。
  3. 异常处理:在GlobalScope中,如果协程抛出异常,它会被传播到调用者。而在viewModelScope中,如果协程抛出异常,它会被捕获并处理,不会影响到其他协程。
  4. 并发性:GlobalScope中的所有协程都是并发执行的,它们共享同一个线程池。而viewModelScope中的协程则按照先后顺序执行,一个接一个地执行。

总之,GlobalScopeviewModelScope都是用于管理协程的作用域,但它们在范围、生命周期、异常处理和并发性等方面存在一些差异。在选择使用哪个作用域时,需要根据具体的需求和场景来决定。

25. Flow相关

1. 什么是 Kotlin Flow

Kotlin Flow 是基于 Kotlin 协程的库,专门用于处理异步数据流。它的设计灵感来自于响应式编程,通过提供一系列的操作符,可以让开发者以类似于集合操作的方式处理连续的异步事件流。

2. Flow 的基本概念

发射器(Emitter)

在 Kotlin Flow 中,数据的产生者被称为发射器(Emitter)。通过调用 flow { ... },你可以定义一个发射器,并使用 emit() 函数来发射数据。例如:

image.png

收集器(Collector)

收集器(Collector)用于接收发射器发射的数据。通过调用 collect 函数,你可以订阅并处理发射的数据。例如:

image.png

3. Flow 的实现原理

Kotlin Flow 的实现原理基于 Kotlin 协程的基础设施。协程允许在函数执行过程中挂起,等待某些条件满足后恢复执行。Flow 利用了这一特性来实现数据流的处理。 在 Flow 内部,数据流被建模为一系列的悬挂函数调用。每次发射数据时,发射器会暂停并将数据传递给订阅者。而订阅者在收集数据时会挂起,并等待数据传递。这样,通过协程的挂起和恢复机制,Flow 实现了数据的异步传递和处理。 此外,Flow 还支持冷流的特性。只有在有订阅者时,发射器才会开始执行。这有助于避免不必要的计算和资源浪费。

4. flow常用操作符有哪些?

参考答案

5.Flow有哪些分类, 热流与冷流的区别

Flow分类

(1)一般 Flow,仅有一个观察者 ,冷流

image.png

(2)StateFlow

有状态Flow ,可以有多个观察者,热流 构造时需要传入初始值 : initialState
常用作与UI相关的数据观察,类比LiveData

image.png

(3)SharedFlow

可定制化的StateFlow可以有多个观察者,热流. 无需初始值,有三个可选参数:
replay - 重播给新订阅者的值的数量(不能为负,默认为零)。
extraBufferCapacity - 除了replay之外缓冲的值的数量。 当有剩余缓冲区空间时, emit不会挂起(可选,不能为负,默认为零)。
onBufferOverflow - 配置缓冲区溢出的操作(可选,默认为暂停尝试发出值)

image.png

热流与冷流的区别

(1)冷流是指每个订阅者都有自己的数据流。在冷流模式下,每当有新的订阅者订阅数据流时,数据流的发射过程会重新开始。订阅者之间不会共享数据(Kotlin Flow 本身是冷流)。

(2)热流是指数据源开始产生数据后,这些数据会立即传递给所有已经订阅的订阅者。订阅者无论何时订阅,都会从当前数据开始接收(如SharedFlow,可以解决LiveData的数据倒灌等问题)。

6. 错误处理与异常处理

在实际应用中,处理异步操作时必须考虑错误和异常情况。在 Kotlin Flow 中,你可以使用 catch 操作符来捕获和处理异常,确保应用的稳定性。

image.png

7. 异步流的处理

Kotlin Flow 非常适合处理异步操作。通过使用 flowOn 操作符,可以将数据流切换到指定的调度器上,实现在不同线程中执行异步操作。

image.png

8. 调度器和线程切换

调度器和线程切换是实现异步操作的重要部分。Kotlin Flow 允许你使用 flowOn 操作符来切换数据流的执行线程。

在 Android 开发中,通常使用 Dispatchers.IO 调度器来执行网络请求等耗时操作,使用 Dispatchers.Main 调度器在主线程中更新界面。你可以根据不同的需求和场景选择合适的调度器。例如:

image.png

9. 背压处理策略

背压处理策略是指在数据产生速率超过消费速率时的一种处理机制。Kotlin Flow 提供了几种不同的背压处理策略,以适应不同的情况。

(1) Buffer(缓冲)

buffer 策略会在数据流中使用一个缓冲区来存储数据,当数据产生速率超过消费速率时,数据会暂时存储在缓冲区中,直到有足够的空间将其传递给订阅者。这可以确保数据不会丢失,但可能会占用更多的内存。

image.png

(2)Conflate(合并)

conflate 策略会在数据产生速率超过消费速率时,跳过一些数据,只保留最新的数据。这样可以减少内存占用,但会丢失一部分数据。

image.png

(3)CollectLatest

collectLatest 策略会在新的数据到达时取消之前的数据处理,并只处理最新的数据。这在处理用户输入等连续事件时特别有用。

image.png

10. 取消操作

(1)使用协程作用域

在 Flow 中进行取消操作时,建议使用协程作用域来确保操作的一致性。通过 coroutineScope 函数,你可以创建一个协程作用域,然后在作用域内启动 Flow 操作。

image.png

(2)通过 CancellationSignal 进行取消

Kotlin Flow 还提供了 onEach 操作符,允许你在每次发射数据时检查取消状态。你可以使用 CancellableContinuation 来检查取消状态,并在需要时抛出取消异常。

image.png

11. 资源清理

在处理异步操作时,还需要注意及时清理资源,以避免内存泄漏或其他问题。

(1)使用 try-finally 进行资源清理

可以使用 try-finally 块来确保资源得到正确的释放,即使发生异常或取消操作。

image.png

(2)使用 channelFlow 进行资源清理

对于需要手动释放资源的情况,你可以使用 channelFlow 函数,它允许你在 Flow 中执行一些额外的操作,如资源清理。

image.png

(3)结合取消和资源清理

当取消操作和资源清理同时存在时,你可以将它们结合起来,以确保在取消操作发生时进行资源清理。

image.png

11. Kotlin Flow vs. RxJava

异步编程范式

Kotlin Flow 和 RxJava 都是用于实现异步编程的库,但它们在编程范式上有所不同。RxJava 基于响应式编程范式,使用 Observables 和 Observers 来处理异步事件流。而 Kotlin Flow 基于 Kotlin 协程,通过 Flow 和收集器(Collectors)来实现异步数据流的处理。这两种范式各有优势,开发者可以根据个人偏好和项目需求进行选择。

协程集成

Kotlin Flow 是 Kotlin 协程的一部分,因此它天生与 Kotlin 协程无缝集成。这意味着你可以在同一个代码块中使用协程和 Flow,实现更加一致和清晰的异步编程。RxJava 也提供了与协程集成的方式,但与 Kotlin Flow 相比,可能需要更多的适配和配置。

冷流与热流

Kotlin Flow 支持冷流和热流的概念,这有助于惰性计算和资源优化。冷流保证每个订阅者都有自己的数据流,不会共享数据。热流在数据产生后传递给所有订阅者,即使在订阅之后也可以接收之前的数据。RxJava 也有类似的概念,但在使用时需要特别注意避免潜在的内存泄漏和资源浪费。

线程调度

RxJava 和 Kotlin Flow 都提供了线程调度的机制,允许在不同线程中执行异步操作。在 RxJava 中,你可以使用 observeOnsubscribeOn 来切换线程。而在 Kotlin Flow 中,你可以使用 flowOn 操作符来实现线程切换。两者的使用方式相似,但 Kotlin Flow 可以更加自然地与协程集成,避免了额外的配置。

背压处理

RxJava 提供了丰富的背压处理策略,例如缓存、丢弃、最新值等。在处理高频率事件流时,这些策略可以帮助控制数据流的流量。Kotlin Flow 也提供了类似的背压处理策略,如 bufferconflatecollectLatest。选择哪种库取决于你对背压处理的需求和熟悉程度。

适用场景

选择使用 Kotlin Flow 还是 RxJava 取决于你的项目需求和团队经验。以下是一些适用场景的示例:

  • Kotlin Flow 适用场景:

    • 如果你已经在项目中广泛使用了 Kotlin 协程,那么使用 Kotlin Flow 可以更加一致地集成异步处理。
    • 如果你喜欢使用 Kotlin 语言特性,Kotlin Flow 提供了更具 Kotlin 风格的异步编程。
    • 如果你希望简化异步编程,Kotlin Flow 的响应式操作符与集合操作类似,易于理解和使用。
    • 如果你需要使用 Kotlin 协程的其他特性,如取消、超时和异常处理,Kotlin Flow 可以更加自然地与之集成。
  • RxJava 适用场景:

    • 如果你已经在项目中广泛使用了 RxJava,或对 RxJava 有深入的了解,继续使用它可能更加方便。
    • 如果你需要丰富的背压处理策略来控制高频率事件流的流量,RxJava 提供了更多的选择。
    • 如果你需要与其他基于 RxJava 的库集成,继续使用 RxJava 可能更加方便。

结论

Kotlin Flow 是一个强大的库,用于处理异步数据流。通过理解其基本概念、实现原理以及背压处理策略,你可以更好地利用 Kotlin Flow 实现响应式异步编程,以及在不同场景下选择合适的策略来处理数据流。这将帮助你构建更健壮、高效的 Android 应用。

12. 协程内部是怎样做到线程切换的?

Kotlin协程是Kotlin中实现非阻塞IO和并发编程的重要工具。它通过在代码级别提供声明性挂起(suspend)函数和挂起点(coroutine scope)来简化异步代码的编写。

在Kotlin协程内部,线程交互是通过协程调度器(coroutine dispatcher)实现的。协程调度器负责在需要时创建新的协程,并在协程完成后将其销毁。它还负责在协程之间进行上下文切换,以实现线程交互。

Kotlin协程调度器通常使用轻量级线程(lightweight thread)或纤程(fibers)来实现。轻量级线程是一种比传统线程更轻量、更高效的线程实现,而纤程则是一种更为轻量级的协程实现。

在Kotlin中,可以通过launch或async函数来启动一个新的协程。当调用launch或async函数时,Kotlin编译器会生成一个包含挂起点和挂起函数的代码块。当挂起函数被调用时,它会暂停当前的执行,并将控制权传递给协程调度器。

协程调度器会根据当前可用的线程或纤程来执行挂起函数。当挂起函数完成后,协程调度器会将控制权传递回原来的执行上下文,并继续执行后续的代码。

在Kotlin中,可以通过await关键字来等待一个挂起函数的完成。当await被调用时,当前协程会被暂停,并将控制权传递给协程调度器。直到等待的挂起函数完成后,当前协程才会被恢复执行。

通过这种方式,Kotlin协程可以在不同的线程或纤程之间进行上下文切换,从而实现线程交互。同时,由于协程调度器的存在,Kotlin协程还可以避免传统线程中常见的锁竞争和死锁等问题,提高程序的性能和可靠性。

12. Channel相关

1. 介绍下Channel

Channel是一种用于协程之间通信的数据结构。它允许一个协程发送数据到 Channel,而另一个协程从 Channel 接收数据。Channel 可以实现生产者-消费者模式,其中一个协程充当生产者,生成数据并将其发送到 Channel,而另一个协程充当消费者,从 Channel 中接收并处理数据,类似于Java中的BlockingQueue。

2. 内部实现原理

Channel 的内部实现基于协程调度器和锁。它使用了一个队列来存储发送到 Channel 中的数据,并使用锁来实现线程安全的数据访问。当一个协程发送数据到 Channel 时,它会尝试将数据放入队列,如果队列已满,发送协程将被挂起,直到有空间可用。另一方面,接收协程会从队列中取出数据,如果队列为空,接收协程也会被挂起,直到有数据可用。

Channel 可以是有界或无界的,有界 Channel 限制了可以发送到 Channel 的数据量,而无界 Channel 不做限制。

3. 使用示例

val channel = Channel<Int>() // 创建一个Channel  
// 启动一个协程来发送数据到Channel 
launch {  
    for (i in 1..5) {  
        channel.send(i) // 将数据发送到Channel  
        delay(1000) // 模拟耗时操作  
    }  
    channel.close() // 关闭Channel  
}  

// 启动另一个协程来从Channel接收数据  
launch {  
    while (true) {  
        val data = channel.receive() // 从Channel接收数据  
        println("Received: $data")  
        if (data == null) break // 当Channel关闭时,receive()返回null  
    }  
}  

4. channel.send()和channel.offer()的区别

send方法不同的是,offer方法是非阻塞的,如果Channel已满,它不会等待,而是立即返回一个表示操作结果的ChannelResult对象。

使用示例

val channel = Channel<Int>() // 创建一个Channel  
// 启动一个协程来发送数据到Channel  
launch {  
    for (i in 1..5) {  
        val result = channel.offer(i) // 将数据发送到Channel  
        if (result.isSuccess) {  
            println("Offered: $i")  
        } else {  
            println("Offer failed: ${result.exception?.message}")  
        }  
        delay(1000) // 模拟耗时操作  
    }  
    channel.close() // 关闭Channel  
}  

// 启动另一个协程来从Channel接收数据  
launch {  
    while (true) {  
        val data = channel.receive() // 从Channel接收数据  
        println("Received: $data")  
        if (data == null) break // 当Channel关闭时,receive()返回null  
    }  
}  

5. BroadcastChannel

BroadcastChannel 允许多个接收者订阅同一数据流,类似于广播,适用于多个消费者的场景

val broadcastChannel = BroadcastChannel<Int>() // 创建一个BroadcastChannel  
// 启动一个协程来发送数据到BroadcastChannel
launch {  
    for (i in 1..5) {  
        broadcastChannel.send(i) // 将数据发送到BroadcastChannel  
        delay(1000) // 模拟耗时操作  
    }  
    broadcastChannel.close() // 关闭BroadcastChannel  
}  

// 启动多个协程来从BroadcastChannel接收数据  
for (i in 1..3) {  
    launch {  
        while (true) {  
            val data = broadcastChannel.receive() // 从BroadcastChannel接收数据  
            println("Consumer $i received: $data")  
            if (data == null) break // 当BroadcastChannel关闭时,receive()返回null  
        }  
    }  
}