关于 OkHttp 和 Retrofit 的使用,你需要注意的点
前言
现在做 Android 开发大家基本上都是使用 OkHttp 或者 Retrofit 进行网络请求的。
添加依赖
    // OkHttp 框架:https://github.com/square/okhttp
    implementation 'com.squareup.okhttp3:okhttp:4.9.3'
简单使用
    private val mClient = OkHttpClient.Builder()
        .addInterceptor { chain ->
            val request = chain.request()
            chain.proceed(request)
        }.build()
    fun okHttpExceptionTest() {
        val request = Request.Builder()
            .url("https://www.baidu.com/")
            .build()
        mClient.newCall(request)
            .enqueue(object : Callback {
                override fun onFailure(call: Call, e: IOException) {
                    e.printStackTrace()
                }
                @Throws(IOException::class)
                override fun onResponse(call: Call, response: Response) {
                    println("okHttpExceptionTest:response is ${response.body?.string()}")
                }
            })
    }
正常情况下以上代码没什么问题,对吧?
如果在网络请求时发生了异常,那么 OkHttp 会回调 Callback 的 onFailure 方法。
但是,这个时候我们假如想要在请求前先做一点处理(比如给请求添加一个统一的请求头或者别的参数),然后再去进行网络请求,这个时候如果我们在拦截器里不小心抛出了一个非 IOException ,这个时候程序会直接 crash!
伪代码复现
    private val mClient = OkHttpClient.Builder()
        .addInterceptor { chain ->
            val request = chain.request()
            val method = request.method
            // do something...
            if ("GET" == method) {
                // 模拟抛出未受检的异常(会导致程序 crash)
                throw RuntimeException("are you ok?")
                // 下面抛出的这个异常不会导致程序 crash
                // throw IOException("are you ok?")
            }
            chain.proceed(request)
        }.build()
由于 Retrofit 是基于 OkHttp 的封装,所以使用 Retrofit 的时候也会有这个问题。这里就不再贴出代码了(笔者已经试过了)。
拦截器接口源码阅读
/**
 * Observes, modifies, and potentially short-circuits requests going out and the corresponding
 * responses coming back in. Typically interceptors add, remove, or transform headers on the request
 * or response.
 *
 * Implementations of this interface throw [IOException] to signal connectivity failures. This
 * includes both natural exceptions such as unreachable servers, as well as synthetic exceptions
 * when responses are of an unexpected type or cannot be decoded.
 *
 * Other exception types cancel the current call:
 *
 *  * For synchronous calls made with [Call.execute], the exception is propagated to the caller.
 *
 *  * For asynchronous calls made with [Call.enqueue], an [IOException] is propagated to the caller
 *    indicating that the call was canceled. The interceptor's exception is delivered to the current
 *    thread's [uncaught exception handler][Thread.UncaughtExceptionHandler]. By default this
 *    crashes the application on Android and prints a stacktrace on the JVM. (Crash reporting
 *    libraries may customize this behavior.)
 *
 * A good way to signal a failure is with a synthetic HTTP response:
 *
 * ```
 *   @Throws(IOException::class)
 *   override fun intercept(chain: Interceptor.Chain): Response {
 *     if (myConfig.isInvalid()) {
 *       return Response.Builder()
 *           .request(chain.request())
 *           .protocol(Protocol.HTTP_1_1)
 *           .code(400)
 *           .message("client config invalid")
 *           .body("client config invalid".toResponseBody(null))
 *           .build()
 *     }
 *
 *     return chain.proceed(chain.request())
 *   }
 * ```
 */
fun interface Interceptor {
  @Throws(IOException::class)
  fun intercept(chain: Chain): Response
  companion object {
    /**
     * Constructs an interceptor for a lambda. This compact syntax is most useful for inline
     * interceptors.
     *
     * ```
     * val interceptor = Interceptor { chain: Interceptor.Chain ->
     *     chain.proceed(chain.request())
     * }
     * ```
     */
    inline operator fun invoke(crossinline block: (chain: Chain) -> Response): Interceptor =
      Interceptor { block(it) }
  }
  interface Chain {
    fun request(): Request
    @Throws(IOException::class)
    fun proceed(request: Request): Response
    /**
     * Returns the connection the request will be executed on. This is only available in the chains
     * of network interceptors; for application interceptors this is always null.
     */
    fun connection(): Connection?
    fun call(): Call
    fun connectTimeoutMillis(): Int
    fun withConnectTimeout(timeout: Int, unit: TimeUnit): Chain
    fun readTimeoutMillis(): Int
    fun withReadTimeout(timeout: Int, unit: TimeUnit): Chain
    fun writeTimeoutMillis(): Int
    fun withWriteTimeout(timeout: Int, unit: TimeUnit): Chain
  }
}
关键源码
  @Throws(IOException::class)
  fun intercept(chain: Chain): Response
从上方部分代码可以看出 intercept 只会向上抛出 IOException 异常。英语好一点的同学可能会去读一下上面的文档注释,这里我给大家机翻一下吧~
译:
观察、修改并可能使发出的请求和返回的相应响应短路。通常,拦截器会在请求或响应上添加、删除或转换标头。
此接口的实现抛出IOException以表示连接失败。这包括自然异常(例如无法访问的服务器)以及响应为意外类型或无法解码时的合成异常。
其他异常类型取消当前调用:
- 
对于使用Call.execute进行的同步调用,异常会传播给调用者。
 - 
对于使用Call.enqueue进行的异步调用, IOException会传播到调用者,指示调用已被取消。拦截器的异常被传递给当前线程的未捕获异常处理程序。默认情况下,这会使 Android 上的应用程序崩溃并在 JVM 上打印堆栈跟踪。 (崩溃报告库可以自定义此行为。)
 
从上面的注释可以看出最关键点的一句话:默认情况下,这会使 Android 上的应用程序崩溃 并在 JVM 上打印堆栈跟踪。
总结
所以我们要想避免这个问题的话,我们可以将拦截器中的异常捕获到并包装成 IOException 抛出即可解决该问题。
如果对你有帮助的话,欢迎一键三连+关注哦~


