安卓原生开发的痛点
自Android平台推出以来,Java一直是开发Android应用的主要语言。尽管后来Kotlin成为了谷歌主推的编程语言,Java仍然被广泛使用,在Android开发中仍占有重要地位。从github的数据看,用Java写的安卓项目仍然是Kotlin的2倍以上;我们的项目创建较早,大部分代码是用Java编写的,当我们用Java处理异步任务的时候,容易陷入回调地狱,下面用伪代码给出一个例子:
login(mobile, password, new Callback{ onSuccess(response){ token = resonse.getToken getUesrInfo(token, new Callback{ onSuccess(response){ name = response.getName display(name) } onError{ // } } } onError{ // }}
以上代码仅演示了2个接口串联调用的场景,我们需要2个callback对象,每个callback对象又包含成功和失败2个方法,想象一下,如果有更多的接口需要串联,则代码的逻辑分支就变成了复杂的树形结构,可读性很差;
在安卓开发中,容易陷入回调地狱的典型场景包括:
弹窗;
页面路由;
接口请求;
授权;
基于callback的三方库调用;
kotlin
Kotlin在2017年被谷歌宣布为Android的官方语言,Kotlin是一种由JetBrains开发的静态类型编程语言,它运行在Java虚拟机(JVM)上,也可以编译成JavaScript或本机代码。Kotlin的设计目标是成为一种现代化的、安全的、简洁的编程语言,能够在各种平台上进行开发,并且与Java互操作性良好。
Kotlin提供了协程的支持,这是一种轻量级的并发编程工具,可简化异步操作的管理。协程可以避免回调地狱,通过使用挂起函数(suspending functions)来简化异步代码的编写,使其看起来更像是同步代码,从而提高了代码的可读性和可维护性。
协程(Coroutine)
协程是一段代码,不同的协程之间可协作式的执行,协程和线程不是同一层次的东西,协程是建立在线程之上的概念,多个协程可跑在同一个线程,而一个协程也可以在多个线程之间切换。创建线程的代价是比较高的,通常你只能创建有限数量的线程,而协程是非常轻量级的,你几乎可创建任意多的协程。线程是由操作系统管理的,而协程是由kotlin库管理的。
为了用好协程,有几个基本的概念需要了解:
Builder
协程的构造器,用于新建一个协程,launch和async是两个最常见的构造函数,如果你不想从协程得到返回值,就用launch,否则用async;
Dispatcher
线程分发器,用于指定协程跑在哪个线程,一种典型的使用场景就是我们需要在IO线程做网络请求,然后回到UI线程操作View;常用的Dispatcher有Default,IO和Main,Default适用于在工作线程执行CPU密集型任务,IO适用于网络请求,Main适用于操作UI;
Scope
协程的上下文,用来管理协程的,每个协程都需要关联一个scope,常见的scope有Global Scope,LifeCycle Scope和ViewModel Scope。当你希望你的协程生命周期等同于整个app,就用Global Scope,当你希望协程的生命周期等同于Activity/Fragment的,则使用LifeCycle Scope,当你希望协程的生命周期等同于ViewModel的,就用ViewModel Scope。当scope的生命周期结束时,关联的协程也会被cancel。
Job
协程的句柄,当你调用launch或async的时候就会得到一个job,你可以调用Job的cancel方法结束协程;
用协程消灭回调地狱
下面,我们将用协程依次消灭上述case中的回调地狱;
弹窗
lifecycleScope.launch { // 显示弹窗,并异步等待用户操作 val result = showDialog toast("user clicked $result")}suspend fun showDialog: String { // 包装成suspend函数 return suspendCoroutine
我们用suspendCoroutine函数将原来的基于回调的代码包装起来,这个函数提供一个Continuation,当callback发生的时候,可调用Continuation的resume,于是调用方就能以同步的形式拿到返回值,并执行后续的逻辑;顺便提一句,你也可以调用Continuation.resumeWithException方法抛出异常,调用方可用try-catch捕捉异常,用于处理某些异常场景;值得一提的是,suspend函数必须在另一个supend函数或者协程中调用,这个例子中,我们借助launch函数创建了一个协程;
路由
class MyActivity : AppCompatActivity { var requestCode = 1 var defer :CompletableDeferred
调用startActivityForResult的时候,我们新建一个CompletableDeferred对象,在onActivityResult中,我们调用其complete方法,并传入返回值;
调用方拿到CompletableDeferred实例后,调用await异步等待返回值,拿到结果后继续后续流程;
实际使用的时候,可以将这段逻辑封装到基类,并可以维护一个requestCode到CompletableDeferred的Map,这样子类就无需重复编写这些代码了;
另外请注意,上面的代码未处理Activity在后台被杀并重启的场景;
接口请求
假设我们用retrofit库做网络请求,
public interface TaskService { @GET("/tasks") Call
以上代码声明了一个同步使用的接口,这个接口的返回值是Call,Call提供一个execute方法;
fun getUser: User{ TaskService taskService = ServiceGenerator.createService(TaskService.class); Call
定义一个方法,用于接口请求,并返回结果,注意,如果这个方法在UI线程直接调用,将阻塞UI线程,导致ANR;
lifecycleScope.launch { withContext(Dispatchers.IO) { val user = getUser withContext(Dispatchers.Main) { showUser(user) } }}
Activity/Fragment提供lifcecycleScope,这个Scope是和组件的生命周期绑定的,当组件销毁的时候,相关的协程也会销毁,不用担心内存泄漏;我们调用launch方法并传入一个lambda,在lambda内部,我们用withContext(Dispatchers.IO)将协程dispatch到IO线程,防止阻塞UI线程,等接口返回后,我们再次调用withContext(Dispatchers.Main)将协程dispatch到UI线程,将User信息显示在UI上;
在实际的开发中,还有一个很常见的场景,就是并发调用2个接口,等到2个接口全部返回结果后,显示数据。
lifecycleScope.launch { withContext(Dispatchers.IO) { val first: Deferred
launch是start-and-forget模式,而async是start-and-get-result模式,允许从内部返回一个结果,我们将2个请求分别包在async中,他们会并行执行,然后我们调用await等待返回值,等到他们全部返回结果后,后续的代码才会执行;
其他
授权和调用三方库的场景,类似弹窗的场景,不再赘述;
总结
作为原生的安卓开发,我们苦异步编程久已,自从kotlin和协程被引入了原生安卓开发,我们终于找到了优雅的异步编程写法,再也不用眼馋Javascript的async/await了。
作者介绍
Pony,现任移动研发资深专家