异步开发是安卓开发中基本的一个技术问题,本文会对比各种异步库的基本使用方式和实现原理。
其实异步开发主要是面对两个场景: 1.类A在新线程调用了类B的方法,获取方法执行结果之后再执行类A的方法;类B需要主动通知类A执行某个方法。以上两个场景可以分别用下面两个例子来代表: 例1:UI层在子线程获取到数据之后再主线程通知UI更新。 例2:数据层监听到数据库变化,主动通知UI层更新。
本文对比分析四种方式:回调,RxJava, kotlin协程,Eventbus
1. 回调
回调是最容易被想到的一种异步实现方法,当然异步一般都是涉及到多线程, 所以回调一般会结合线程池一起使用。
针对上面的例1,实现方式如下,数据层主要是Model类,先在Model类中生命回调接口,然后把UI层(MainActivity)创建回调接口的实例,传给Model类,Model类获取到数据之后调用UI层接口实例的方法即可。
kotlin
class MainActivity : AppCompatActivity() {
override fun onResume() {
// 创建IO线程池
val ioExecutorService = Executors.newFixedThreadPool(1)
//创建回调实例
val listener = object : Model.ModelListner {
override fun onDataGot(data: String) {
runOnUiThread {
// 在主线程更新界面
}
}
}
ioExecutorService.submit {
Model().getData(listener)
}
}
}
// 数据类
class Model {
fun getData(modelListner: ModelListner){
// 获取数据
val result = "hello"
// 执行回调
modelListner.onDataGot(result)
}
interface ModelListner{
fun onDataGot(data: String);
}
}
例2的实现也是如此。当然,在kotlin当中可以用高阶函数来实现回调,这样就不需要单独声明接口了。
2. RxJava
RxJava是一个用于异步编程的 Java 库,它基于响应式编程范式。用观察者模式实现了消息的通知。
2.1 用法
有两套模型来实现消息的通知,一是Observable和Observer, 而是Flowable和Consumer. 二者的差异就是后者支持背压。 考虑用Observable来解决例1的问题就是:首先在数据层创建一个Observable实例,UI层创建Observer实例,并监听数据层的Observable, 然后数据层获取到数据后,通过Observable来通知UI层。同样的,例2也是如此实现。
2.2 实现原理
线程切换
在事件的分发流程中,subscribOn和observeOn方法都可以切换线程.调用这些方法之后,后面的逻辑会在新的线程上执行。 l SubscribOn流程 如下图所示:
调用subsribOn 之后,会创建一个ObservableSubscribeOn( Observable的子类), 然后调用subscribe方法会在指定的线程上执行之后的逻辑 2. observeOn流程与subscribOn同理,只不过让observer的onNext方法在指定线程执行
调度策略
默认提供4个调度器: AndroidSchedulers.mainThread: 安卓平台特有,对应主线程。 Schedulers.NEW_THREAD:适合用于需要在独立线程上执行的短期任务,每次调度的时候创建一个ScheduledThreadPoolExecutor, 核心线程数 1, 最大线程数 Interger.max Schedulers.COMPUTATION:适用于计算密集型的任务,这个调度器对应n(cup核心数)个ScheduledThreadPoolExecutor,每个Executor核心线程数为1,有任务的时候轮流安排到Excutor Schedulers.IO: 适用于IO任务,这个调度器只对应一个ScheduledThreadPoolExecutor,核心线程数为1,
3. kotlin 协程
协程概念是跟着go语言火起来的,但是kotlin中的协程有别于go语言中的协程,go语言中的协程可以直接被go虚拟机调度, 而kotlin中的协程概念可以理解为一个异步库,每个协程相当于线程池中的一个任务。 kotlin协程相比于回调的优点是可以用同步的方式写异步代码,很大程度方便了开发 一些重要的概念:
- CoroutineScope: 协程的作用域,控制协程的生命周期。
- Job: 协程的句柄,可以用来取消协程。
- Deferred: 一个带有返回值的 Job。
- Dispatcher: 协程的调度器,决定协程在哪个线程或线程池中执行。
3.1 用法
启动协程有很多方法, 可以用launch,async, runBlocking, withContext方法来启动。有所区别的是 launch: 是最常用的协程构建器,用于启动一个新的协程。它返回一个 Job对象,可以用于取消协程或检查其状态。 async: 也是一种协程构建器,但它返回一个 Deferred 对象,可以用于获取协程的结果。async 通常用于需要并行执行并返回结果的任务。 withContext: 用于在指定的协程上下文中执行代码块,并返回其结果。它通常用于切换协程上下文,例如从主线程切换到 IO 线程。 runBlocking: 是一种阻塞当前线程的协程构建器,通常用于测试或顶层函数中。它会阻塞当前线程,直到协程内部的所有代码执行完毕。
dart
使用实例: lauch和async启动一个协程
实现原理: 创建一个协程; 交给调度器; 调度器会选择一个线程执行
launch {
println("World!")
}
val deferred = async {
"Hello, World!"
}
来看上面提到的例1,子线程获取到数据之后在主线程通知UI更新。
kotlin
class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
// 使用协程在 IO 线程中获取数据,然后在主线程中更新 UI
CoroutineScope(Dispatchers.Main).launch {
// 在 IO 线程中获取数据, 这个协程会调到IO调度器去执行
val data = withContext(Dispatchers.IO) {
Model().getData()
}
// 在主线程中更新 UI
updateUI(data)
}
}
class Model {
suspend fun getData(): String{
// 获取数据
val result = "hello"
return result
}
}
例2的实现需要借助协程中的flow概念,flow的概念类似RxJava中的flowable和Observable. 例2的实现方法是先在数据层创建一个flow对象,然后UI层去监听这个对象,在数据层监听到数据库数据变化的之后通过flow通知UI层
3.2 实现原理
挂起恢复原理
像例子子线程获取到数据之后再主线程通知UI更新这种情况,如果要在协程1中要在其他线程执行协程2, 得到协程2的结果之后再继续执行协程1。 那么这个过程的实现原理是:把当前协程设置为新协程的回调,执行新协程;新新城执行完之后执行上一个协程,上一个协程的执行位置用状态机保存。
调度策略
kotlin协程默认提供了3种调度器, 用户也可自定义调度器。 Dispatchers.Default: 适用于 CPU 密集型任务。 它是协程的默认调度器。 Dispatchers.IO:适用于 I/O 密集型任务,例如网络请求、文件读写等 Dispatchers.Main: 主要用于与 UI 交互的任务,确保协程在主线程中运行
4 EventBus
eventbus虽然可以很好得解决回调地狱的问题,但是消息的发送和处理难以跟踪和管理也一个问题,而且随意的发送消息也会导致软件架构遭到破坏。而且由于eventbus的方便性,可以很方便地从一个类发送消息给另一个类,所以导致有些场景下,开发者不愿意进行好地软件设计,而偷懒采用eventbus。
总结
回调的方法显得比较臃肿,eventbus会导致消息的难以跟踪和管理,协程相比于rxjava更加原生,且用同步的方式写异步的代码也是更加友好的,类似本文提到的两个例子,在这种一般情况下建议使用kotlin协程作为异步开发框架,其他情况再具体分析。