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 的精髓全在拦截器链上。一个请求从发送到收到响应,依次经过五层内置拦截器:
- 应用拦截器 (
addInterceptor加的)------最先进入,最后退出。这里看到的 Request 和 Response 是"最终版"的,不包含重定向信息。 - RetryAndFollowUpInterceptor ------ 负责连接失败重试、重定向跟随。最多跟 20 次重定向。
- BridgeInterceptor ------ 把用户构建的 Request 补全成 HTTP 协议需要的格式:补 Host、补 Content-Type/Content-Length、补 Accept-Encoding(gzip)、补 Cookie、处理 Keep-Alive。
- CacheInterceptor ------ 读缓存、写缓存。命中了内存缓存或磁盘缓存就直接返回,不再往下走。
- ConnectInterceptor ------ 从连接池里拿连接,没有就新建。这里打开 TCP Socket,支持 HTTP/1.1 和 HTTP/2 协议协商。
- 网络拦截器 (
addNetworkInterceptor加的)------在连接建立之后、发送数据之前执行。能看到中间响应(比如 301 重定向的 Location)。 - 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 的 connectTimeout、readTimeout、writeTimeout 分别控制连接建立、读取数据、写入数据的超时。只设了 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写测试很方便,后端没就绪也不阻塞