1.2.2 异常处理

同步逻辑的异常处理非常直观,我们可以简单地用try...catch语句来实现对整个流程任意位置的异常的捕获,但异步逻辑的异常处理就显得不是很直接了。

首先简化asyncBitmap函数,去掉立即返回结果的路径,保留常见的回调写法,并增加对异常的处理,如代码清单1-7所示。

代码清单1-7 异步回调的异常处理


fun asyncBitmap(
  url: String, onSuccess: (Bitmap) -> Unit,
  onError: (Throwable) -> Unit
) {
  thread {
    try {
      download(url).also(onSuccess)
    } catch (e: Exception) {
      onError(e)
    }
  }
}

调用的时候我们很自然地传入一个异常处理函数,如代码清单1-8所示。

代码清单1-8 传递异常处理函数


val url = "https://www.bennyhuo.com/assets/avatar.jpg"
checkUrl(url)
asyncBitmap(url, onSuccess = ::show, onError = ::showError)

当异步调用出现异常时,我们调用showError来处理异常的输出,不过这只是对异步调用的异常做了处理。如果url不合法,checkUrl函数抛出了异常呢?或者asyncBitmap内部在启动异步任务时就抛出了未捕获的异常呢?如代码清单1-9所示。

代码清单1-9 完善异常捕获


try {
  val url = "https://www.bennyhuo.com/assets/avatar.jpg"
  checkUrl(url)
  asyncBitmap(url, onSuccess = ::show, onError = ::showError)
} catch (e: Exception) {
  showError(e)
}

我们看到,一旦产生了异步调用,异常处理就变得复杂起来了,这里showError被调用了两次,实际生产实践中的情况可能更复杂。

换个角度,异常也是函数调用结果的一种,既然asyncBitmap本身也可能抛出异常,那我们完全可以对抛出的异常与返回的结果一视同仁。如果有一些手段能帮我们把异常的处理合并,我们处理起来就会相对轻松一些。仔细对比图1-4和图1-5,同样存在异步逻辑,只不过后者的异步的调用流程通过编译器或者其他手段简化成了“同步化”的调用,因此前者需要分别处理A到B和C到D处的异常,而后者对整体流程做一次处理即可,复杂度明显降低。

图1-4 异步任务的异常处理

图1-5 同步流程的异常处理

异步逻辑同步化正是Kotlin协程要解决的问题。