OkHttp 的使用

OkHttp 是什么

OkHttp 是 Square 出品的 HTTP 客户端库,2013 年开源,现在由 Square 和 Google 共同维护。Android 4.4 之后 HttpURLConnection 的底层实现就切成了 OkHttp,可以说 OkHttp 已经是 Android 平台事实上的网络标准了。

核心能力:HTTP/2 多路复用、连接池复用、GZIP 透明压缩、响应缓存、拦截器链。比起 HttpURLConnection,OkHttp 的 API 比较干净------不用手写 URL 拼接、不用单独处理重定向、不用操心连接泄漏。

Retrofit 的底层用的就是 OkHttp,大部分 Android 项目即使不直接依赖 OkHttp,也会通过 Retrofit 间接用到。直接使用 OkHttp 的场景主要是文件上传下载、WebSocket、或者不想引入 Retrofit 那一层注解的简单请求。

接入项目

kotlin 复制代码
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") // 可选

Groovy:

groovy 复制代码
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'

OkHttp 本身不依赖 Android SDK,纯 Java/Kotlin 项目也能用。Android 项目最低支持 API 21。

OkHttp 4.x 用 Kotlin 重写了内部实现,但对外 API 和 3.x 基本兼容。如果在用 3.x,升级的时候注意包名不变,主要是内部协议的实现方式改了。

基础用法

GET 请求

kotlin 复制代码
val client = OkHttpClient()

val request = Request.Builder()
    .url("https://api.example.com/user/123")
    .get()
    .build()

// 同步------别在主线程调
val response = client.newCall(request).execute()
println(response.body?.string())

// 异步
client.newCall(request).enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        // 网络异常
    }

    override fun onResponse(call: Call, response: Response) {
        val body = response.body?.string()
        // 切主线程更新 UI
    }
})

协程里的用法后面封装部分会写。

POST JSON

kotlin 复制代码
val json = """
    {"username":"jay","password":"123456"}
""".trimIndent()

val body = json.toRequestBody("application/json".toMediaType())

val request = Request.Builder()
    .url("https://api.example.com/login")
    .post(body)
    .build()

client.newCall(request).enqueue(...)

POST 表单

kotlin 复制代码
val formBody = FormBody.Builder()
    .add("username", "jay")
    .add("password", "123456")
    .build()

val request = Request.Builder()
    .url("https://api.example.com/login")
    .post(formBody)
    .build()

添加 Header

kotlin 复制代码
val request = Request.Builder()
    .url("https://api.example.com/user/me")
    .header("Authorization", "Bearer $token")
    .header("Accept", "application/json")
    .addHeader("X-Custom", "value")  // addHeader 可以追加同名 key
    .get()
    .build()

header() 会覆盖同名 key,addHeader() 是追加。大部分场景用 header() 就满足了。

文件上传

kotlin 复制代码
val file = File("/sdcard/photo.jpg")
val mediaType = "image/jpeg".toMediaType()

val multipartBody = MultipartBody.Builder()
    .setType(MultipartBody.FORM)
    .addFormDataPart("file", file.name, file.asRequestBody(mediaType))
    .addFormDataPart("description", "头像")
    .build()

val request = Request.Builder()
    .url("https://api.example.com/upload")
    .post(multipartBody)
    .build()

多文件上传只需要多调几次 addFormDataPart()

文件下载

kotlin 复制代码
val request = Request.Builder()
    .url("https://example.com/file.zip")
    .get()
    .build()

val response = client.newCall(request).execute()
if (response.isSuccessful) {
    val inputStream = response.body?.byteStream()
    val outputFile = File("/sdcard/Download/file.zip")
    inputStream?.use { input ->
        outputFile.outputStream().use { output ->
            input.copyTo(output)
        }
    }
}

大文件下载记得在子线程跑,同步 execute() 会一直阻塞到下载完。

DELETE / PUT / PATCH

OkHttp 支持所有 HTTP 方法,DELETE 通常不带 Body:

kotlin 复制代码
// DELETE
Request.Builder().url("...").delete().build()

// PUT
Request.Builder().url("...").put(body).build()

// PATCH(用 method() 自定义)
Request.Builder().url("...").method("PATCH", body).build()

封装

直接裸用 OkHttpClient + Request.Builder 有几个问题:每个地方都要写 URL 拼接、重复创建 Client 实例、回调嵌套难读、异常处理分散。下面把常用的封装方式整理出来。

全局 OkHttpClient 单例

kotlin 复制代码
object HttpClient {

    val instance: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .connectTimeout(15, TimeUnit.SECONDS)
            .readTimeout(15, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .addInterceptor(LoggingInterceptor())
            .addInterceptor(AuthInterceptor())
            .build()
    }
}

全局只建一个 OkHttpClient,复用连接池和线程池。不要在每次请求时 new 新的 OkHttpClient------每个实例各带一套连接池和线程池,内存和连接数很快就爆了。

协程封装

OkHttp 原生的 Callback 回调在协程项目里很别扭。用 Kotlin 扩展函数桥接到协程:

kotlin 复制代码
suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation ->
    enqueue(object : Callback {
        override fun onResponse(call: Call, response: Response) {
            continuation.resume(response)
        }

        override fun onFailure(call: Call, e: IOException) {
            if (continuation.isCancelled) return
            continuation.resumeWithException(e)
        }
    })

    continuation.invokeOnCancellation {
        if (isExecuted) cancel()
    }
}

调用:

kotlin 复制代码
scope.launch {
    val response = client.newCall(request).await()
    if (response.isSuccessful) {
        val json = response.body?.string()
    }
}

协程取消时自动 cancel 底层 OkHttp Call,不会出现页面销毁了请求还在跑的情况。

统一请求入口

把 URL 拼接、Header 注入、异步调度全部收进一个入口:

kotlin 复制代码
class ApiHttpClient(
    private val baseUrl: String,
    private val client: OkHttpClient = HttpClient.instance
) {
    suspend fun get(
        path: String,
        params: Map<String, String> = emptyMap()
    ): Response {
        val urlBuilder = "$baseUrl/$path".toHttpUrl().newBuilder()
        params.forEach { (k, v) -> urlBuilder.addQueryParameter(k, v) }

        val request = Request.Builder()
            .url(urlBuilder.build())
            .get()
            .build()

        return client.newCall(request).await()
    }

    suspend fun post(path: String, body: RequestBody): Response {
        val request = Request.Builder()
            .url("$baseUrl/$path")
            .post(body)
            .build()

        return client.newCall(request).await()
    }
}

用的时候:

kotlin 复制代码
val api = ApiHttpClient("https://api.example.com")
val response = api.get("user/123", mapOf("fields" to "name,avatar"))

统一结果处理

拿到 Response 之后紧接着要做反序列化和异常处理。配合 Kotlin 的 sealed class

kotlin 复制代码
sealed class HttpResult<out T> {
    data class Success<T>(val data: T) : HttpResult<T>()
    data class Error(val code: Int, val msg: String) : HttpResult<Nothing>()
    data class NetworkError(val e: IOException) : HttpResult<Nothing>()
}

suspend fun <T> httpCall(
    call: suspend () -> Response,
    parser: (String) -> T
): HttpResult<T> {
    return try {
        val response = call()
        val body = response.body?.string() ?: ""
        if (response.isSuccessful) {
            HttpResult.Success(parser(body))
        } else {
            HttpResult.Error(response.code, body)
        }
    } catch (e: IOException) {
        HttpResult.NetworkError(e)
    }
}

调用:

kotlin 复制代码
val result = httpCall(
    call = { api.get("user/123") },
    parser = { Gson().fromJson(it, User::class.java) }
)

when (result) {
    is HttpResult.Success -> updateUI(result.data)
    is HttpResult.Error -> showToast(result.msg)
    is HttpResult.NetworkError -> showToast("网络异常")
}

Interceptor 封装

每个请求自动加公共 Header:

kotlin 复制代码
class AuthInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val request = original.newBuilder()
            .header("Authorization", "Bearer ${TokenStore.token}")
            .header("X-Platform", "Android")
            .header("X-Version", BuildConfig.VERSION_NAME)
            .build()
        return chain.proceed(request)
    }
}

日志拦截器:

kotlin 复制代码
class LoggingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val startMs = System.currentTimeMillis()

        if (BuildConfig.DEBUG) {
            Log.d("HTTP", "--> ${request.method} ${request.url}")
        }

        val response = chain.proceed(request)

        if (BuildConfig.DEBUG) {
            val duration = System.currentTimeMillis() - startMs
            Log.d("HTTP", "<-- ${response.code} ${request.url} (${duration}ms)")
        }

        return response
    }
}

带进度的文件下载

OkHttp 不直接提供进度回调,但可以通过自定义 ResponseBody 在读取流的时候计数:

kotlin 复制代码
class ProgressResponseBody(
    private val delegate: ResponseBody,
    private val onProgress: (bytesRead: Long, totalBytes: Long) -> Unit
) : ResponseBody() {

    override fun contentType(): MediaType? = delegate.contentType()
    override fun contentLength(): Long = delegate.contentLength()

    override fun source(): BufferedSource {
        val totalBytes = contentLength()
        return object : ForwardingSource(delegate.source()) {
            var bytesRead = 0L

            override fun read(sink: Buffer, byteCount: Long): Long {
                val read = super.read(sink, byteCount)
                if (read != -1L) {
                    bytesRead += read
                    onProgress(bytesRead, totalBytes)
                }
                return read
            }
        }.buffer()
    }
}

通过 Interceptor 把这个包装体替换到 Response 里:

kotlin 复制代码
class ProgressInterceptor(
    private val onProgress: (bytesRead: Long, totalBytes: Long) -> Unit
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalResponse = chain.proceed(chain.request())
        val body = originalResponse.body ?: return originalResponse
        return originalResponse.newBuilder()
            .body(ProgressResponseBody(body, onProgress))
            .build()
    }
}

底层怎么跑的

拦截器链

OkHttp 的精髓全在拦截器链上。一个请求从发送到收到响应,依次经过五层内置拦截器:

  1. 应用拦截器addInterceptor 加的)------最先进入,最后退出。这里看到的 Request 和 Response 是"最终版"的,不包含重定向信息。
  2. RetryAndFollowUpInterceptor ------ 负责连接失败重试、重定向跟随。最多跟 20 次重定向。
  3. BridgeInterceptor ------ 把用户构建的 Request 补全成 HTTP 协议需要的格式:补 Host、补 Content-Type/Content-Length、补 Accept-Encoding(gzip)、补 Cookie、处理 Keep-Alive。
  4. CacheInterceptor ------ 读缓存、写缓存。命中了内存缓存或磁盘缓存就直接返回,不再往下走。
  5. ConnectInterceptor ------ 从连接池里拿连接,没有就新建。这里打开 TCP Socket,支持 HTTP/1.1 和 HTTP/2 协议协商。
  6. 网络拦截器addNetworkInterceptor 加的)------在连接建立之后、发送数据之前执行。能看到中间响应(比如 301 重定向的 Location)。
  7. CallServerInterceptor ------ 真正向服务器写数据和读数据的地方。前面所有的拦截器都在准备,到这一层才真正发起网络 I/O。

每一层 chain.proceed(request) 把请求交给下一层,返回值依次往上层传。这个设计把重试、缓存、协议协商这些关注点完全解耦了。

连接池

OkHttp 的连接池基于 RealConnectionPool,默认保活 5 个空闲连接,每个连接空闲 5 分钟后关闭。同一个地址的请求会复用已有的 TCP 连接,省掉三次握手和 TLS 开销。HTTP/2 下同一连接还可以并发多个请求(多路复用)。

连接池默认参数:maxIdleConnections = 5,keepAliveDuration = 5 分钟。

调整:

kotlin 复制代码
OkHttpClient.Builder()
    .connectionPool(ConnectionPool(10, 10, TimeUnit.MINUTES))

分发器

Dispatcher 控制并发请求数。默认最多 64 个并发请求、同一个 Host 最多 5 个并发。超过限制的请求排队等待。

kotlin 复制代码
val dispatcher = Dispatcher().apply {
    maxRequests = 64       // 全局最大并发
    maxRequestsPerHost = 5 // 同一 Host 最大并发
}

OkHttpClient.Builder()
    .dispatcher(dispatcher)

缓存

OkHttp 内置基于 DiskLruCache 的响应缓存。开了之后,只要服务端返回了合适的 Cache-Control 头,OkHttp 就会自动缓存响应:

kotlin 复制代码
val cacheDir = File(context.cacheDir, "http_cache")
val cache = Cache(cacheDir, 10 * 1024 * 1024) // 10MB

OkHttpClient.Builder()
    .cache(cache)

GET 请求默认会读缓存。POST 请求不缓存(HTTP 协议规定)。强制跳过缓存在 Request 上加 CacheControl.FORCE_NETWORK

DNS 自定义

默认走系统 DNS,但可以换成自己的 DNS 解析逻辑------比如接入 HttpDNS、或者做测试时把域名指向指定 IP:

kotlin 复制代码
OkHttpClient.Builder()
    .dns { hostname ->
        if (hostname == "api.example.com") {
            listOf(InetAddress.getByName("192.168.1.100"))
        } else {
            Dns.SYSTEM.lookup(hostname)
        }
    }

证书固定(Certificate Pinner)

防止中间人攻击,把服务端证书的公钥哈希写死在客户端:

kotlin 复制代码
val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .build()

OkHttpClient.Builder()
    .certificatePinner(certificatePinner)

证书过期了更新很麻烦,生产环境用要规划好证书轮换策略。

WebSocket

OkHttp 内置 WebSocket 支持,几行就能建一个长连接:

kotlin 复制代码
val request = Request.Builder()
    .url("wss://chat.example.com/ws")
    .build()

val webSocket = client.newWebSocket(request, object : WebSocketListener() {
    override fun onOpen(webSocket: WebSocket, response: Response) {
        webSocket.send("hello")
    }

    override fun onMessage(webSocket: WebSocket, text: String) {
        // 收到消息
    }

    override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
        webSocket.close(1000, null)
    }

    override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
        // 连接断掉,在这里做重连
    }
})

关闭:

kotlin 复制代码
webSocket.close(1000, "bye")

踩过的坑

OkHttpClient 不要每次请求都 new

每个 OkHttpClient 实例自带一套连接池和线程池。每个请求 new 一个,连接池没法复用,线程数疯长,内存也兜不住。全局只建一个,通过单例或 DI 提供。

response.body()?.string() 只能读一次

ResponseBody 的 string() 是流式读取,读一次之后流就空了。如果一个 Response 需要在两个地方读 body(比如 Interceptor 里打了日志,业务层又要读),用 peekBody() 或者先 string() 缓存起来。

kotlin 复制代码
// Interceptor 里
val responseBody = response.peekBody(Long.MAX_VALUE)
Log.d("HTTP", responseBody.string())
return response // 还能正常读

没有关闭 Response

Response 实现了 Closeable,body 是流,不关会导致连接泄漏。Kotlin 的 use {} 扩展可以自动关:

kotlin 复制代码
client.newCall(request).execute().use { response ->
    // 用 response
}

在主线程调 execute()

execute() 是同步阻塞的。主线程调,网络超时就 ANR。异步要么用 enqueue,要么走封装好的协程 await()

HTTPS 握手失败,低版本 Android 的锅

Android 7.0 以下默认只支持部分 TLS 1.2 加密套件,有些服务器配了高安全性的套件,低版本就连不上。解法是自定义 ConnectionSpec

kotlin 复制代码
val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
    .tlsVersions(TlsVersion.TLS_1_2)
    .cipherSuites(
        CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
        CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
    )
    .build()

OkHttpClient.Builder()
    .connectionSpecs(listOf(spec, ConnectionSpec.CLEARTEXT))

现在最低 API 一般都 21+ 了,这个问题不常见了。

超时不生效

OkHttp 的 connectTimeoutreadTimeoutwriteTimeout 分别控制连接建立、读取数据、写入数据的超时。只设了 connectTimeout 不代表整个请求就 15 秒超时------readTimeout 是收到第一个字节之后的等待时间,服务端慢响应可能拖很久。三个超时都要配。

上传大文件 OOM

MultipartBody 构建时会把所有文件内容读进内存。几百 MB 的视频直接 OOM。改用 RequestBody 的流式写法:

kotlin 复制代码
val fileBody = object : RequestBody() {
    override fun contentType() = "video/mp4".toMediaType()
    override fun writeTo(sink: BufferedSink) {
        File("/sdcard/video.mp4").inputStream().use { input ->
            sink.writeAll(input.source())
        }
    }
}

这种写法不会把整个文件加载到内存,边读边写。

GZIP 透明解压和 Response Header 的坑

OkHttp 请求时自动加 Accept-Encoding: gzip,收到 gzip 响应后透明解压。这意味着 response.body?.contentLength() 返回的是 -1(解压后长度不确定),response.header("Content-Length") 拿到的是压缩前的大小。如果要做下载进度,记得用 Interceptor 手动去掉 Accept-Encoding,自己处理压缩。

WebSocket 断线重连

OkHttp 的 WebSocket 不会自动重连。连接断了 onFailure 会被回调,要在里面手动重建:

kotlin 复制代码
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
    handler.postDelayed({ connectWebSocket() }, 3000)
}

DNS 缓存导致切环境不生效

OkHttp 默认走系统 DNS,系统层会缓存 DNS 结果。切换网络环境(比如从办公网切到 VPN)之后,域名解析还是旧的 IP。在 OkHttpClient 上配短 TTL 或自己实现 DNS:

kotlin 复制代码
OkHttpClient.Builder()
    .dns { Dns.SYSTEM.lookup(it) }

总结

OkHttp 干了 HTTP 客户端该干的所有事------连接池、拦截器链、缓存、HTTP/2、WebSocket。。 关键要点回顾:

  • OkHttpClient 全局只建一个,别每个请求 new
  • 三个超时(connect / read / write)都要设
  • response.body 用完关掉,string() 只能读一次
  • 拦截器链是扩展核心------Header 注入、日志、缓存、重试全在这层做
  • 文件上传下载注意 OOM,大文件用流式 RequestBody
  • WebSocket 不会自动重连,要手动处理
  • Android 低版本的 TLS 兼容性问题需要单独调 ConnectionSpec
  • MockWebServer 写测试很方便,后端没就绪也不阻塞
相关推荐
朝星11 天前
Android开发[14]:网络优化之OkHttp
android·okhttp·kotlin
之歆13 天前
Promise 基础技术深度解析:从回调地狱到链式调用
前端·okhttp·promise
之歆13 天前
Ajax 基础技术深度解析:XHR 从入门到跨域
前端·ajax·okhttp
YHHLAI14 天前
Ajax — 异步数据交互
ajax·okhttp·交互
Xaire23 天前
行行查案例-数据解密-国密s4-webpack打包模块补齐
okhttp
霸道流氓气质23 天前
Spring AI Ollama 连接超时问题排查与解决:OkHttp 读超时配置全指南
人工智能·spring·okhttp
你觉得脆皮鸡好吃吗1 个月前
XSS渗透 COOKIE
网络·http·okhttp·网络安全学习
彭于晏Yan1 个月前
OkHttp 与 RestTemplate 技术选型对比
java·spring boot·后端·okhttp
JohnnyDeng941 个月前
OkHttp 拦截器链与缓存策略:深度解析网络层的核心机制
okhttp·缓存