All Articles

Retrofit coroutines 异常引发的崩溃

最近项目中突然爆发了一波由网络超时造成的崩溃问题(之前也有过几次,但是没有引起足够的重视).花费了一天的时间终于解决了[开心]

事情是这样的:

我在项目中使用kotlin作为开发语言,同时也引入了coroutnies(协程),使用协程替代了线程池.想要在api层使用协程,于是Github一波决定引入 JakeWharton/retrofit2-kotlin-coroutines-adapter

  1. 一开始还觉得很诡异,因为我在网络请求的外围写了try cache 捕获异常然后交给个上层做处理,而且不是所有的网络超时异常都捕获不到
  2. 通过打印日志发现只有在页面销毁调的时候会引发异常
  3. 继续跟踪发现在页面销毁的时候调用coroutinesjob取消协程时网络请求并没用被取消. 上一段代码:
private class BodyCallAdapter<T>(
      private val responseType: Type
  ) : CallAdapter<T, Deferred<T>> {

    override fun responseType() = responseType

    override fun adapt(call: Call<T>): Deferred<T> {
      val deferred = CompletableDeferred<T>()

      deferred.invokeOnCompletion {
        if (deferred.isCancelled) {
          // 这里打印日志
          call.cancel()
        }
      }

      call.enqueue(object : Callback<T> {
        override fun onFailure(call: Call<T>, t: Throwable) {
          // 这里打印日志
          deferred.completeExceptionally(t)
        }

        override fun onResponse(call: Call<T>, response: Response<T>) {
          if (response.isSuccessful) {
            deferred.complete(response.body()!!)
          } else {
            deferred.completeExceptionally(HttpException(response))
          }
        }
      })

      return deferred
    }
  }

发现cancle的日志在onFailuer后面,debug去看才发现原来deferred.isCancelled 返回true是因为deferred.completeExceptionally(t) 触发的. 如果是这种原因才触发取消网络请求那取消就没有意义了 下面这两个是这个库的issues: [1][Question] Coroutine cancellation is not handled, right? #7]( https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter/issues/7) [2][is there any way to catch timeout exception using your coroutines?](https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter/issues/33) 想了半天也没办法把job或者coroutinesContext传给api返回的Deferred对象,而且suspend方法也不能获取 coroutinesContext对象所以最终只能在调用的时候把job 对象作为参数传到下层了…虽然不优雅但是能解决问题了

如果你也遇到了这样的问题希望我的解决方法能给你一个思路,如果你有更好的解决方法请你也告诉我一声 :D 写一下伪代码:

// api service
fun api(): Deferred<String> = CompletableDeferred()

class Activity(): CoroutineScope {
  val job = Job()
  override val coroutineContext: CoroutineContext = Dispatchers.Default + job

  fun runApi(){
    launch {
      val deferred = api()
      job.invokeOnCompletion {
        if (job.isCancelled)
         // 在检测到job取消的时候取消网络请求
          deferred.cancel()
      }
      try{
        val result = deferred.await()
        //todo
      } catch (t: Throwable){
        // exception control
      }
    }
  }

  fun destory(){
    job.cancel()
  }

}

emmm 还是打算插一句为什么要使用这个库:

由于网络请求是超时操作,而安卓的页面[activity, fragment]什么时候销毁一般是由用户操作决定的.所以会有生命周期不一致的问题,网络请求又会持有页面的索引[内部类,会持有外部类的索引].所以在设计的时候一般会在页面销毁的时候取消网络请求,最开始使用的是RxJava + CompositeDisposable 在页面销毁的时候通过调用 CompositeDisposable 的 close 方法取消网络请求,之后使用LiveData + RxJava + AutoDispose 后来觉得既然引入了LiveData 在引入 RxJava有些多余了(因为大部分RxJava的使用场景都在网络请求上),同时也接触了 koltincoroutnies 找到了大神写的库竟然不用关心网络请求的取消[嗯,大神就是大神]于是就引入了…