在 Android 开发中,很多网络性能问题并不是接口本身慢,而是请求在真正到达服务端之前,就已经被 DNS 解析、跨网访问、弱网重试、TLS 握手、连接复用不足、CDN 调度不合理 等环节拖慢了。
一个接口从 App 发出到拿到响应,大致会经历下面这些过程:
text
App 发起请求
↓
DNS 解析域名
↓
选择 IP / CDN 节点
↓
建立 TCP 连接
↓
TLS 握手
↓
发送 HTTP 请求
↓
服务端或 CDN 响应
↓
读取响应数据
↓
业务解析和渲染
如果只盯着服务端接口耗时,很容易忽略移动端网络链路中的大头。尤其在国内复杂网络环境下,LocalDNS 劫持、跨运营商访问、CDN 节点调度不准、IPv6/IPv4 选择不佳,都可能让一个原本 100ms 的接口变成 1 秒甚至超时。
本文会从两个层面展开:
- 通用方案:如何通过 CDN 和 DNS 优化移动端网络请求。
- OkHttp 实践方案:Android 项目中如何落地自定义 DNS、HTTPDNS、连接复用、缓存、失败切换和监控上报。
一、先明确目标:我们到底要优化什么?
移动端网络优化的核心目标不是"把所有请求都变快",而是让用户关键路径上的请求:
- 更快:减少 DNS、连接、TLS、传输耗时。
- 更稳:弱网、跨网、DNS 异常时有兜底。
- 更省:减少重复请求、重复握手、重复下载。
- 可观测:知道慢在哪里,而不是凭感觉调参。
- 可灰度:DNS、CDN、域名策略可以动态调整。
所以,一个成熟的移动端网络优化体系通常包括:
text
CDN 加速
DNS 优化
连接复用
HTTP 缓存
失败重试
多域名容灾
弱网降级
监控上报
动态配置
二、CDN 优化:让请求离用户更近
CDN 的核心价值是:就近接入、减少跨网、降低源站压力、提升稳定性。
对于 Android App 来说,CDN 不只是图片加速,它还可以承载很多非强实时的资源请求。
1. 哪些资源适合走 CDN?
适合走 CDN 的资源包括:
| 类型 | 是否推荐 CDN | 说明 |
|---|---|---|
| 图片 | 推荐 | 头像、封面、缩略图、详情图等 |
| 视频/音频 | 推荐 | 通常需要专用 CDN 和分片能力 |
| H5 静态资源 | 推荐 | JS、CSS、字体、HTML 模板 |
| APK / 补丁包 | 推荐 | 大文件下载,建议单独下载域名 |
| 配置文件 | 推荐 | 适合短缓存 + 本地兜底 |
| 公共 GET 接口 | 视情况 | 不含用户态、可缓存时适合 |
| 登录/支付/下单接口 | 不推荐缓存 | 可走加速转发,但不要缓存 |
| 上传接口 | 单独处理 | 通常走对象存储或上传加速域名 |
核心原则:
静态、公共、可缓存、可版本化的资源,优先走 CDN。强实时、强鉴权、强一致性的业务接口,不要盲目缓存。
2. CDN 缓存策略:URL 版本化是关键
很多 CDN 缓存问题不是 CDN 本身的问题,而是 URL 设计不合理。
推荐做法是:资源 URL 版本化。
例如:
text
https://cdn.example.com/static/main.8f3a2c.js
https://cdn.example.com/app/config/v23/config.json
https://cdn.example.com/image/avatar/123_200x200.webp
这样可以放心给静态资源设置长缓存:
http
Cache-Control: public, max-age=31536000, immutable
ETag: "xxx"
Last-Modified: Wed, 03 Jun 2026 10:00:00 GMT
不同资源可以采用不同缓存策略:
| 资源类型 | 推荐缓存策略 |
|---|---|
| 带 hash 的 JS/CSS/图片 | 长缓存,半年到一年 |
| App 配置文件 | 短缓存,30 秒到 5 分钟 |
| 运营配置 | 短缓存 + 本地缓存兜底 |
| 用户态接口 | 通常不缓存 |
| 大文件 | 长缓存 + 断点续传 |
需要注意的是:
- 不要让同一个固定 URL 的内容频繁变化。
- 资源更新应该通过 URL 变化,而不是强制刷新 CDN。
- 配置类资源要有本地缓存兜底,避免 CDN 短时异常导致 App 不可用。
3. 图片和大文件优化
Android App 中图片和大文件通常是流量大户。
图片优化建议:
- 使用 WebP/AVIF 等更高压缩率格式。
- 根据设备尺寸请求合适分辨率,不要下载原图再本地缩放。
- 缩略图、预览图、原图使用不同 URL。
- 图片 URL 参数规范化,避免 CDN 缓存碎片。
例如:
text
https://cdn.example.com/image/123?w=200&h=200&format=webp
https://cdn.example.com/image/123?w=1080&format=webp
大文件优化建议:
- 使用独立下载域名,避免占用 API 域名连接。
- 支持 Range 请求和断点续传。
- 下载任务支持暂停、恢复、失败重试。
- 对大文件做 MD5/SHA256 校验。
- 使用 CDN 分片和就近调度。
4. 多 CDN 和多域名容灾
对于核心业务,可以准备多个 CDN 或多个加速域名:
text
cdn-a.example.com
cdn-b.example.com
cdn-c.example.com
容灾策略可以是:
text
主 CDN 请求失败
↓
判断是否为安全 GET 请求
↓
切换备用 CDN 域名
↓
再次请求
↓
记录失败域名并短时间降权
但要注意:
- 不要为了容灾把域名拆得太碎。
- 域名越多,连接复用越差。
- HTTP/2 对同 host 的连接复用收益很高。
- 多域名切换更适合静态资源,不适合支付、登录、下单等强业务接口。
推荐做法:
text
api.example.com 核心 API
cdn.example.com 静态资源
upload.example.com 上传
download.example.com 大文件下载
按照业务类型拆域名,而不是每个模块都随意建一个域名。
三、DNS 优化:别让请求输在起跑线上
一次网络请求的第一步通常是 DNS 解析。DNS 解析慢、解析错、解析到跨网 IP,都会让后续请求变慢。
1. 移动端常见 DNS 问题
Android App 经常遇到这些 DNS 问题:
- LocalDNS 被运营商劫持。
- DNS 缓存污染。
- 解析结果跨运营商或跨地域。
- DNS TTL 不可控。
- 弱网下 DNS 解析超时。
- IPv6/IPv4 选择不合理。
- 网络切换后仍使用旧 IP。
- CDN 节点调度不准确。
这些问题的后果通常是:
text
DNS 慢 → 首包慢
IP 不优 → 连接慢
跨网访问 → 传输慢
DNS 异常 → 请求失败
IP 失效 → 重试慢
2. HTTPDNS 是移动端常用方案
HTTPDNS 的基本思路是:App 不完全依赖系统 LocalDNS,而是通过 HTTP/HTTPS 请求 DNS 服务,拿到更准确的 IP 列表。
text
App
↓
HTTPDNS 服务
↓
返回 api.example.com 对应的 IP 列表
↓
App 按质量排序
↓
OkHttp 使用这些 IP 建连
HTTPDNS 的优势:
- 绕过 LocalDNS 劫持。
- 可以根据用户出口 IP、地域、运营商返回更优 IP。
- 可以做 IP 级别容灾。
- TTL 更可控。
- 可以和业务监控联动,动态调整调度策略。
但 HTTPDNS 也不是银弹:
- HTTPDNS 服务自身需要高可用。
- HTTPDNS 失败时必须回退系统 DNS。
- 不能无限缓存 IP。
- HTTPS 场景不能粗暴把域名替换成 IP。
3. DNS 缓存设计
推荐维护一份本地 DNS 缓存:
kotlin
data class DnsRecord(
val host: String,
val ips: List<String>,
val expireAt: Long,
val source: String, // httpdns / system / cache
val updatedAt: Long
)
缓存策略建议:
- 尊重 HTTPDNS 返回的 TTL。
- 设置最小 TTL,避免频繁解析。
- 设置最大 TTL,避免 IP 长时间失效。
- 允许弱网下短时间使用过期缓存。
- 网络从 Wi-Fi 切到蜂窝时刷新核心域名。
- 请求失败时对相关 IP 降权,而不是立刻全量清空。
一个简单的策略:
text
缓存未过期 → 使用缓存
缓存已过期但网络差 → 临时使用旧缓存,同时异步刷新
HTTPDNS 成功 → 更新缓存
HTTPDNS 失败 → 回退系统 DNS
系统 DNS 也失败 → 使用可接受时间内的旧缓存兜底
4. IP 质量排序
HTTPDNS 返回多个 IP 时,不应该永远使用第一个。
可以综合这些因素排序:
- 最近成功率
- 最近失败时间
- TCP 连接耗时
- TLS 握手耗时
- 请求总耗时
- 当前网络类型
- IPv6/IPv4 可用性
- 服务端返回错误类型
推荐排序逻辑:
text
近期成功 IP
> 低 RTT IP
> 未失败 IP
> HTTPDNS 推荐顺序
> 系统 DNS 结果
注意区分失败类型:
| 失败类型 | 是否应该降权 IP |
|---|---|
| TCP connect timeout | 应该 |
| DNS 解析失败 | 不针对单 IP |
| TLS 握手失败 | 谨慎,需要排查证书/SNI |
| HTTP 5xx | 谨慎,可能是服务端问题 |
| HTTP 4xx | 不应该 |
| SocketTimeoutException | 视阶段而定 |
四、OkHttp 实战:把 CDN 和 DNS 优化真正落到代码里
OkHttp 是 Android 生态里非常常见的网络库。它本身已经提供了连接池、HTTP/2、缓存、拦截器、事件监听、自定义 DNS 等能力。
我们可以基于这些能力搭建一套完整的网络优化方案。
五、通过 OkHttp Dns 接口接入 HTTPDNS
最推荐的方式是实现 OkHttp 的 Dns 接口,而不是手动替换 URL 中的 host。
1. 基础版自定义 Dns
kotlin
class HttpDns(
private val httpDnsClient: HttpDnsClient,
private val enableHosts: Set<String>
) : Dns {
override fun lookup(hostname: String): List<InetAddress> {
// 只处理白名单域名,避免影响三方 SDK、支付、登录等特殊域名
if (!enableHosts.contains(hostname)) {
return Dns.SYSTEM.lookup(hostname)
}
val ips = try {
httpDnsClient.query(hostname)
} catch (e: Exception) {
emptyList()
}
if (ips.isNotEmpty()) {
val result = ips.mapNotNull { ip ->
try {
InetAddress.getByName(ip)
} catch (e: Exception) {
null
}
}
if (result.isNotEmpty()) {
return result
}
}
// 兜底系统 DNS
return Dns.SYSTEM.lookup(hostname)
}
}
使用方式:
kotlin
val client = OkHttpClient.Builder()
.dns(
HttpDns(
httpDnsClient = myHttpDnsClient,
enableHosts = setOf(
"api.example.com",
"cdn.example.com",
"upload.example.com"
)
)
)
.build()
这类方案的好处是:
- 请求 URL 仍然保持域名形式。
- HTTPS 证书校验不会被破坏。
- SNI 仍然可以正常工作。
- Cookie、重定向、HTTP/2 连接复用更稳定。
2. 加入本地缓存和失败兜底
实际项目中,lookup() 不应该每次都请求 HTTPDNS。可以加入缓存:
kotlin
class DnsCache {
private val cache = ConcurrentHashMap<String, DnsRecord>()
fun get(host: String): List<String>? {
val record = cache[host] ?: return null
return if (System.currentTimeMillis() < record.expireAt) {
record.ips
} else {
null
}
}
fun put(host: String, ips: List<String>, ttlMillis: Long) {
cache[host] = DnsRecord(
host = host,
ips = ips,
expireAt = System.currentTimeMillis() + ttlMillis,
source = "httpdns",
updatedAt = System.currentTimeMillis()
)
}
}
增强版 DNS:
kotlin
class SmartDns(
private val httpDnsClient: HttpDnsClient,
private val dnsCache: DnsCache,
private val ipQualityStore: IpQualityStore,
private val enableHosts: Set<String>
) : Dns {
override fun lookup(hostname: String): List<InetAddress> {
if (!enableHosts.contains(hostname)) {
return Dns.SYSTEM.lookup(hostname)
}
val cachedIps = dnsCache.get(hostname)
if (!cachedIps.isNullOrEmpty()) {
return toInetAddress(ipQualityStore.sort(hostname, cachedIps))
}
val httpDnsIps = try {
httpDnsClient.query(hostname)
} catch (e: Exception) {
emptyList()
}
if (httpDnsIps.isNotEmpty()) {
dnsCache.put(hostname, httpDnsIps, ttlMillis = 60_000)
return toInetAddress(ipQualityStore.sort(hostname, httpDnsIps))
}
return Dns.SYSTEM.lookup(hostname)
}
private fun toInetAddress(ips: List<String>): List<InetAddress> {
return ips.mapNotNull { ip ->
try {
InetAddress.getByName(ip)
} catch (e: Exception) {
null
}
}
}
}
六、不同业务使用不同 OkHttpClient
不要把所有请求都塞进一个完全相同配置的 OkHttpClient。
推荐按业务类型拆分:
text
apiClient 核心 API 请求
cdnClient 图片、配置、静态资源
uploadClient 上传请求
downloadClient 大文件下载
示例:
kotlin
val connectionPool = ConnectionPool(
10,
5,
TimeUnit.MINUTES
)
val baseClient = OkHttpClient.Builder()
.connectionPool(connectionPool)
.retryOnConnectionFailure(true)
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.eventListenerFactory { NetworkEventListener() }
.build()
val apiClient = baseClient.newBuilder()
.dns(apiDns)
.build()
val cdnClient = baseClient.newBuilder()
.dns(cdnDns)
.cache(Cache(File(context.cacheDir, "cdn_cache"), 100L * 1024 * 1024))
.build()
val uploadClient = baseClient.newBuilder()
.dns(uploadDns)
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val downloadClient = baseClient.newBuilder()
.dns(downloadDns)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build()
这样做的好处是:
- API 不会被大文件下载拖慢。
- 上传可以使用更长的 writeTimeout。
- CDN 资源可以单独开启缓存。
- 不同域名可以使用不同 DNS 策略。
- 监控指标更容易按业务类型分析。
七、CDN 资源启用 OkHttp 缓存
OkHttp 支持 HTTP 缓存,适合 GET 静态资源和部分配置类请求。
kotlin
val cache = Cache(
directory = File(context.cacheDir, "okhttp_cache"),
maxSize = 100L * 1024 * 1024
)
val cdnClient = OkHttpClient.Builder()
.cache(cache)
.build()
如果服务端缓存头配置合理,客户端通常不需要做太多额外逻辑。
服务端推荐返回:
http
Cache-Control: public, max-age=300
ETag: "config-v23"
客户端请求时,OkHttp 会根据缓存策略自动处理缓存复用和条件请求。
如果服务端缓存头暂时不完善,可以通过拦截器做临时兜底:
kotlin
class CacheControlInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val url = request.url.toString()
val isStaticResource =
url.endsWith(".png") ||
url.endsWith(".jpg") ||
url.endsWith(".webp") ||
url.endsWith(".json")
return if (request.method == "GET" && isStaticResource) {
response.newBuilder()
.header("Cache-Control", "public, max-age=300")
.build()
} else {
response
}
}
}
不过更推荐从服务端和 CDN 层正确配置缓存头,客户端不要长期维护复杂的缓存补丁逻辑。
八、DNS 预解析:减少首个请求等待时间
对于关键域名,可以在 App 启动、用户登录后、进入关键页面前做预解析。
kotlin
class DnsPreloader(
private val dns: Dns,
private val executor: ExecutorService
) {
fun preload(hosts: List<String>) {
hosts.distinct().forEach { host ->
executor.execute {
try {
dns.lookup(host)
} catch (_: Exception) {
// ignore
}
}
}
}
}
使用:
kotlin
dnsPreloader.preload(
listOf(
"api.example.com",
"cdn.example.com",
"upload.example.com"
)
)
注意事项:
- 不要在主线程预解析。
- 不要一次预解析几十个域名。
- 网络切换后重新预解析核心域名。
- 预解析结果必须遵守 TTL。
- 预解析失败不能影响正常请求。
九、CDN 失败切换:只对安全请求做容灾
对于静态资源 GET 请求,可以在主 CDN 失败后切换备用 CDN。
kotlin
class CdnFallbackInterceptor(
private val backupHosts: List<String>
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.method != "GET") {
return chain.proceed(request)
}
try {
val response = chain.proceed(request)
if (response.isSuccessful) {
return response
}
response.close()
} catch (_: IOException) {
// try backup hosts
}
for (backupHost in backupHosts) {
val newUrl = request.url.newBuilder()
.host(backupHost)
.build()
val newRequest = request.newBuilder()
.url(newUrl)
.build()
try {
val response = chain.proceed(newRequest)
if (response.isSuccessful) {
return response
}
response.close()
} catch (_: IOException) {
// try next host
}
}
return chain.proceed(request)
}
}
配置:
kotlin
val cdnClient = OkHttpClient.Builder()
.addInterceptor(
CdnFallbackInterceptor(
backupHosts = listOf(
"cdn-b.example.com",
"cdn-c.example.com"
)
)
)
.build()
这里有一个重要原则:
CDN 域名切换更适合静态 GET 请求,不要随便用于登录、支付、下单、写入类接口。
原因是写入类接口可能存在幂等性、鉴权、签名、风控、重放等问题。盲目重试或切域名,可能造成重复下单、重复支付、状态不一致。
十、用 EventListener 建立网络性能监控闭环
只做优化不做监控,最后一定会变成玄学。
OkHttp 的 EventListener 可以记录一次请求的完整生命周期,包括:
- DNS 开始和结束
- TCP 连接开始和结束
- TLS 握手开始和结束
- 请求头发送
- 响应头接收
- 请求结束或失败
示例:
kotlin
class NetworkEventListener : EventListener() {
private var callStartNs: Long = 0
private var dnsStartNs: Long = 0
private var connectStartNs: Long = 0
private var secureConnectStartNs: Long = 0
override fun callStart(call: Call) {
callStartNs = System.nanoTime()
}
override fun dnsStart(call: Call, domainName: String) {
dnsStartNs = System.nanoTime()
}
override fun dnsEnd(
call: Call,
domainName: String,
inetAddressList: List<InetAddress>
) {
val costMs = (System.nanoTime() - dnsStartNs) / 1_000_000
log("dns cost=$costMs ms, host=$domainName, ips=$inetAddressList")
}
override fun connectStart(
call: Call,
inetSocketAddress: InetSocketAddress,
proxy: Proxy
) {
connectStartNs = System.nanoTime()
}
override fun connectEnd(
call: Call,
inetSocketAddress: InetSocketAddress,
proxy: Proxy,
protocol: Protocol?
) {
val costMs = (System.nanoTime() - connectStartNs) / 1_000_000
log("connect cost=$costMs ms, address=$inetSocketAddress, protocol=$protocol")
}
override fun secureConnectStart(call: Call) {
secureConnectStartNs = System.nanoTime()
}
override fun secureConnectEnd(call: Call, handshake: Handshake?) {
val costMs = (System.nanoTime() - secureConnectStartNs) / 1_000_000
log("tls cost=$costMs ms")
}
override fun responseHeadersEnd(call: Call, response: Response) {
val costMs = (System.nanoTime() - callStartNs) / 1_000_000
log("ttfb cost=$costMs ms, code=${response.code}, url=${call.request().url}")
}
override fun callEnd(call: Call) {
val costMs = (System.nanoTime() - callStartNs) / 1_000_000
log("call cost=$costMs ms, url=${call.request().url}")
}
override fun callFailed(call: Call, ioe: IOException) {
val costMs = (System.nanoTime() - callStartNs) / 1_000_000
log("call failed cost=$costMs ms, url=${call.request().url}, error=$ioe")
}
private fun log(message: String) {
// 上报到 APM、日志系统或内部监控平台
println(message)
}
}
配置:
kotlin
val client = OkHttpClient.Builder()
.eventListenerFactory { NetworkEventListener() }
.build()
建议上报这些核心指标:
| 指标 | 作用 |
|---|---|
| DNS 耗时 | 判断 DNS 是否拖慢首请求 |
| TCP 连接耗时 | 判断 IP、网络、跨网质量 |
| TLS 握手耗时 | 判断连接复用、证书链、协议问题 |
| TTFB | 判断 CDN/服务端响应速度 |
| 总耗时 | 用户真实体感 |
| HTTP 状态码 | 区分网络失败和业务失败 |
| IP 地址 | 分析 CDN 节点和 IP 质量 |
| 网络类型 | 区分 Wi-Fi、4G、5G、弱网 |
| 协议类型 | 分析 HTTP/1.1、HTTP/2 效果 |
| 失败异常 | 判断是否需要切 IP 或切 CDN |
十一、IP 质量库:让调度越来越聪明
有了监控数据,就可以做一个简单的 IP 质量库。
kotlin
data class IpStat(
val ip: String,
var rttMs: Long = Long.MAX_VALUE,
var failCount: Int = 0,
var lastFailedAt: Long = 0L
)
class IpQualityStore {
private val map = ConcurrentHashMap<String, IpStat>()
fun sort(host: String, ips: List<String>): List<String> {
val now = System.currentTimeMillis()
return ips.sortedWith(
compareBy<String> { ip ->
val stat = map["$host-$ip"]
val recentlyFailed = stat != null && now - stat.lastFailedAt < 60_000
if (recentlyFailed) 1 else 0
}.thenBy { ip ->
map["$host-$ip"]?.rttMs ?: Long.MAX_VALUE
}
)
}
fun markSuccess(host: String, ip: String, rttMs: Long) {
val key = "$host-$ip"
val stat = map.getOrPut(key) { IpStat(ip) }
stat.rttMs = rttMs
stat.failCount = 0
}
fun markFailed(host: String, ip: String) {
val key = "$host-$ip"
val stat = map.getOrPut(key) { IpStat(ip) }
stat.failCount += 1
stat.lastFailedAt = System.currentTimeMillis()
}
}
这套逻辑可以逐步演进:
第一阶段:只做 DNS 耗时和请求耗时上报。
第二阶段:接入 HTTPDNS 和本地缓存。
第三阶段:根据失败率对 IP 降权。
第四阶段:结合地域、运营商、网络类型做动态调度。
第五阶段:配置中心下发域名、CDN、IP 优先级策略。
十二、不要踩坑:HTTPS 下不要粗暴替换 IP
很多人接入 HTTPDNS 时会这么做:
text
原始 URL:
https://api.example.com/user/info
替换后:
https://1.2.3.4/user/info
这在 HTTPS 场景下非常容易出问题:
- 证书是签给
api.example.com的,不是签给1.2.3.4的。 - SNI 可能不正确。
- Host 头需要额外处理。
- HTTP/2 连接复用可能异常。
- Cookie 域、重定向、证书绑定都可能受影响。
更推荐的方式是:
kotlin
OkHttpClient.Builder()
.dns(customDns)
.build()
也就是说:URL 仍然使用域名,只是在 DNS 解析阶段返回你想要的 IP。
十三、推荐的整体架构
最终可以形成下面这套架构:
text
业务层
↓
请求分类:API / CDN / 上传 / 下载
↓
不同 OkHttpClient
↓
自定义 Dns
↓
HTTPDNS 查询
↓
本地 DNS 缓存
↓
IP 质量排序
↓
OkHttp 连接池 / HTTP/2 / TLS
↓
请求执行
↓
EventListener 采集耗时
↓
APM 上报
↓
配置中心动态调整 CDN / DNS / 域名策略
可以把网络优化分成三层:
第一层:资源层
- 静态资源 CDN 化。
- 图片尺寸和格式优化。
- 大文件断点续传。
- URL 版本化。
- 合理缓存头。
第二层:链路层
- HTTPDNS。
- DNS 缓存。
- IP 排序。
- 连接复用。
- HTTP/2。
- TLS 优化。
- 多 CDN 容灾。
第三层:治理层
- EventListener 监控。
- APM 上报。
- 失败分类。
- 配置中心。
- 灰度策略。
- 地域和运营商分析。
十四、落地清单
如果你要在项目中落地,可以按这个顺序做:
阶段一:低成本收益
- 静态资源接入 CDN。
- 图片 URL 支持尺寸裁剪。
- 配置合理的 Cache-Control。
- OkHttp 开启缓存。
- 复用 OkHttpClient,避免重复创建。
- 使用连接池。
- 开启 EventListener 采集基础耗时。
阶段二:DNS 优化
- 核心域名接入 HTTPDNS。
- 实现 OkHttp
Dns接口。 - 增加 DNS 本地缓存。
- HTTPDNS 失败回退系统 DNS。
- App 启动预解析核心域名。
- 网络切换后刷新 DNS 缓存。
阶段三:稳定性和容灾
- CDN 静态资源支持备用域名。
- IP 失败降权。
- 区分连接失败、TLS 失败、HTTP 失败。
- 上传、下载、API 使用不同 client。
- 配置中心支持动态调整域名。
- 弱网场景增加本地缓存兜底。
阶段四:精细化治理
- 按国家、地区、运营商分析耗时。
- 按网络类型分析成功率。
- 按 CDN 节点分析 TTFB。
- 对慢 IP 自动降权。
- 对异常 CDN 自动切流。
- 建立网络质量大盘。
十五、总结
Android 网络优化不是简单地把接口超时时间调大,也不是接一个 CDN 就万事大吉。
真正有效的优化,应该覆盖完整链路:
text
DNS 解析是否快?
IP 是否选得准?
CDN 节点是否离用户近?
连接是否复用?
TLS 是否重复握手?
资源是否缓存?
失败是否能快速切换?
慢在哪里是否能被监控到?
对于大多数 Android 项目,推荐从这些点开始:
- 静态资源 CDN 化,URL 版本化,正确配置缓存头。
- 核心域名接入 HTTPDNS,通过 OkHttp
Dns接口注入。 - 不要手动把 HTTPS 域名替换成 IP。
- 使用 OkHttp 连接池、HTTP/2、缓存和 EventListener。
- 按 API、CDN、上传、下载拆分不同 OkHttpClient。
- 对 DNS、连接、TLS、TTFB、总耗时做监控上报。
- 基于失败率和 RTT 做 IP 降权和 CDN 容灾。
网络优化最重要的不是一次性写完所有代码,而是建立一个可以持续迭代的体系:
先可观测,再优化;先稳定,再激进;先局部灰度,再全量放开。
当你能清楚知道每一次请求慢在哪里,CDN、DNS 和 OkHttp 的优化才真正开始发挥价值。