面试题十一:如何在Android中写出优雅的异步代码?

如何在Android中写出优雅的异步代码?

一、面试官视角:这道题想考察什么?

  • 是否熟练编写异步和同步的代码
  • 是否熟练回调地狱
  • 是否能够熟练使用RxJava
  • 是否对kotlin协程有了解
  • 是否具备编写良好的代码的意识和能力

二、题目剖析:

1、什么是异步?

1
2
3
4
5
6
7
8
9
10
11
12
13
// 异步就是,代码并不是按照你写的这种方式顺序执行的,比如sendRequest执行完成之后,就会跑到代码最后面,那它里面的onSuccess方法什么时候执行呢?在结果返回的时候执行,这就是异步!所以异步并不是有多个线程,同一个线程也可以的,比如setOnClickListener和sendRequest,这些都是异步代码,异步和同步,主要取决于代码是不是按照顺序执行的。

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendRequest(req, new Callback() {
@Override
public void onSuccess(Response resp) {
...
}
});
}
});

2、异步的目的是什么?

  • 提高CPU利用率
  • 提升GUI程序的响应速度
  • 异步不一定快!

异步的过程中,要看程序是I/O密集型还是CPU密集型的,如果是CPU密集型的话,其实异步也好,高并发也好,往往会降低CPU的利用率,因为切换线程的时候,会有一些开销,所以对于一些GUI程序来说,特别是切线程切到I/O线程的这种异步的话,它其实是为了提高CPU的利用率,因为大多数I/O线程会被I/O阻塞掉。

3、回调地狱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendRequest(req, new Callback() {
@Override
public void onSuccess(final Response resp) {
handler.post(new Runnable() {
@Override
public void run() {
updateUI(resp);
}
});
}
});
}
});

4、为了解决上面的回调地狱的问题,RxJava闪亮登场!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// RxJava基本用法
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// 链式调用
sendRequest(req)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Response>() {
public void accept(Response response) throws Exception {
updateUI(response);
}
});
}
}
});

// 使用Java的Lambda表达式简化上面的代码
button.setOnClickListener(v -> sendRequest(req)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> updateUI(response)));

5、RxJava异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 以上代码中的response -> updateUI(response)这行代码很危险,一旦出了异常,它马上就会抛出来并crash掉!
button.setOnClickListener(v -> sendRequest(req)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
// 为了捕获异常,尽量传一个完整的Observer进去,但这样写不美观
.subscribe(new Observer<Response>(){
public void onSubscribe(Disponsable d) {}
public void onNext(Response response) {updateUI(response);}
public void onError(Throwable e) {e.printStackTrace();}
public void onComplete() {}
}));

// 为了解决不美观问题,我们可以将上面的代码再次进行优化
button.setOnClickListener(v -> sendRequest(req)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
// 捕获到异常,对其进行映射,映射成某种类型的Response,Response我们知道里面有成功的也有不成功的,所以相当于人为的定义了一个由异常映射过来的Response的类型。
.onErrorReturnItem(t -> mapThrowableToResponse(t))
.subscribe(response -> updateUI(response)));

// 但有些异常我们没办法把它映射成一个正常的Response,该怎么办呢?
// 这种情况其实就是意味着我们没有捕获,因此只能做一个全局的异常捕获了,捕获的同时还需要做一些上报工作,但上报的时候千万要记住,如果你不将捕获的异常信息做任何的修改的话,只是简单的把异常上报,但它很有可能是OnErrorNotImplementedException类型,虽然它里面会包含真正的 e.getCause 异常原因,但你会看到上报来的异常信息里面包含很多无用的信息,所以全局上报注意上报的是e.getCause。
RxJavaPlugins.setErrorHandler(e -> {
// 全局上报注意上报的是e.getCause
report(e instanceof OnErrorNotImplementedException ? e.getCause() : e);
// 此处表示,如果该异常很严重就抛出去,否则就算了
Exceptions.throwIfFatal(e);
});

6、RxJava取消处理

RxJava这个东西,执行的是异步任务,异步任务就有一个问题,那就是如果有个页面已经销毁了、退出了,这个时候就会遇到一个什么情况呢?其实我们都知道RxJava的链式调用也都是匿名内部类,只是扁平化了,那么匿名内部类它就会持有外部类的引用,比如updateUI()修改UI,就需要持有外部类的引用才能进行修改,那么这个时候Activity被持有,不可回收,但如果response -> updateUI(response),response迟迟没有返回的话,那就会造成内存泄漏,这是第一个问题,第二个问题就是,response返回了结果了,但UI已经销毁了,就有可能出现空指针问题,所以RxJava取消处理也是一个值得注意的点。

1
2
3
4
5
6
button.setOnClickListener(v -> sendRequest(req)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorReturnItem(t -> mapThrowableToResponse(t))
// 容易造成内存泄漏和空指针异常
.subscribe(response -> updateUI(response)));

如何优雅的解决上面的问题呢?(使用开源框架:AutoDispose)

1
2
3
4
5
6
7
8
button.setOnClickListener(v -> sendRequest(req)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
// 异常处理
.onErrorReturnItem(t -> mapThrowableToResponse(t))
// 取消处理:监听View状态自动取消订阅
.as(AutoDispose.autoDisposable(ViewScopeProvider.from(buttom)))
.subscribe(response -> updateUI(response)));

7、使用Kotlin协程来替代RxJava

kotlin的协程的好处就是,代码写起来跟写同步代码几乎一模一样。

将回调转换为协程的挂起函数:

1
2
3
4
suspend fun sendRequest(req: Request) = suspendCoroutine<Response> {
contiunation ->
sendRequest(req, continuation::resume)
}

使用挂起函数:(异步代码的同步写法)

1
2
3
4
5
6
button.onClick {
val req = Request()
val resp = async {sendRequest(req)}.await()
// updateUI(resp),可以看成,挂起函数之后的部分运行再回调中
updateUI(resp)
}

8、Kotlin协程异常处理

1
2
3
4
5
6
7
8
9
10
button.onClick {
// 直接try、catch
try {
val req = Request()
val resp = async {sendRequest(req)}.await()
updateUI(resp)
}catch(e: Exception) {
e.printStackTrace()
}
}

9、Kotlin协程取消处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
将onClick方法进行扩展:
fun View.onClickAutoDisposable(
context.CoroutineContext = Dispatchers.Main, handler: suspend CoroutineScope.(v: View?) -> Unit) {
setOnClickListener{ v ->
GlobalScope.launch(context, CoroutineStart.DEFAULT) {
handler(v)
}.asAutoDisposable(v)
}
}

// 具体实现
fun Job.asAutoDisposable(view: View) = AutoDisposableJob(view, this)

class AutoDisposableJob(private val view: view, private val wrapped: Job): Job by wrapped, OnAttachStateChangeListener{
override fun onviewAttachedToWindow(v: View?) = Unit
override fun onViewDetachedFromWindow(v: View?) {
cancel()
view.removeOnAttachStateChangeListener(this)
}
init {
if(view.isAttachedToWindow) {
view.addOnAttachStateChangeListener(this)
}else {
cancel()
}
invokeOnCompletion {
view.removeOnAttachStateChangeListener(this)
}
}
}

题目结论:

  • 回调地狱是可怕的,可读性差,容易出问题
  • 使用RxJava将异步逻辑扁平化,注意异常处理和取消处理
  • 使用Kotlin协程将异步逻辑同步化,注意异常处理和取消处理