网络优化:从DNS到弱网的全链路实战

网络优化这个话题,说实话,是Android性能优化里最让人头疼的一块。为什么?因为网络环境你控制不了。用户可能在电梯里、地铁上、甚至是在山区。我做了这么多年性能优化,见过太多App在弱网下直接崩掉,或者DNS解析慢到用户直接关掉App。

今天我们就来聊聊网络优化的四个核心方向:DNS解析、连接池复用、数据压缩、弱网适配。每个点我都会结合自己的实战经验来讲。

核心观点:网络优化的本质,是减少等待时间和传输量。说白了,就是让用户少等,让数据少跑。

网络优化四大核心方向 网络优化 DNS解析优化 HTTPDNS / 预解析 连接池复用 Keep-Alive / 复用策略 数据压缩 Gzip / Protobuf 弱网适配 超时重试 / 降级策略 四个方向相互配合,形成完整的网络优化体系

一、DNS解析优化:别再让用户等那几秒了

DNS解析,说白了就是把域名转成IP地址。这个过程有多慢?我实测过,普通DNS解析平均耗时200-500ms,差的能到2秒以上。你想想看,用户打开App,光DNS解析就等2秒,谁受得了?

1.1 传统DNS的问题

传统DNS有几个硬伤:

  • 解析慢:每次请求都要走一遍DNS查询链路
  • 容易被劫持:运营商DNS劫持,你懂的
  • 缓存不可控:系统DNS缓存你管不了,过期了也不知道
  • 不支持精准调度:没法根据用户IP返回最近的节点

我曾经踩过的坑:有一次线上反馈App加载慢,查了半天发现是某运营商的DNS把我们的域名解析到了一个错误的IP上。从那以后,我坚决上了HTTPDNS。

1.2 HTTPDNS方案

HTTPDNS,就是直接用HTTP请求去获取IP地址,不走系统DNS。我推荐用阿里云或腾讯云的HTTPDNS服务。

核心思路:

  1. 启动时预请求HTTPDNS,拿到IP列表
  2. 缓存到本地,设置合理的过期时间
  3. 请求时直接用IP,跳过DNS解析
  4. IP失效时降级回传统DNS
// 伪代码示例:HTTPDNS接入
class HttpDnsManager {
    private val cache = LruCache<String, List<String>>(100)
    
    fun getIpByHost(host: String): String? {
        // 1. 先查缓存
        cache.get(host)?.let { ips ->
            if (ips.isNotEmpty()) return ips[0]
        }
        
        // 2. 异步请求HTTPDNS
        val ips = requestHttpDns(host)
        cache.put(host, ips)
        return ips.firstOrNull()
    }
    
    private fun requestHttpDns(host: String): List<String> {
        // 调用HTTPDNS服务商接口
        // 返回IP列表,按优先级排序
    }
}

个人建议:HTTPDNS的缓存时间不要设太长。我一般设5-10分钟,太长了IP变了不知道,太短了浪费请求。

二、连接池复用:别每次都重新握手

TCP三次握手,加上TLS握手,一次完整的连接建立要几百毫秒。如果每次请求都重新建立连接,那性能就太差了。

连接池复用的核心,就是让多个请求复用同一个TCP连接。说白了,就是Keep-Alive机制。

2.1 OkHttp的连接池

OkHttp默认就支持连接池,但默认配置不一定适合你的场景。我习惯根据业务特点调整参数。

// OkHttp连接池配置
val connectionPool = ConnectionPool(
    maxIdleConnections = 5,    // 最大空闲连接数
    keepAliveDuration = 5,     // 空闲连接存活时间(分钟)
    TimeUnit.MINUTES
)

val okHttpClient = OkHttpClient.Builder()
    .connectionPool(connectionPool)
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .build()

2.2 连接池复用策略

参数 推荐值 说明
maxIdleConnections 5-10 太多浪费内存,太少复用率低
keepAliveDuration 5分钟 太短复用效果差,太长占用资源
connectTimeout 10秒 弱网下可以适当延长
readTimeout 30秒 大文件下载需要更长

关键点:连接池不是越大越好。我见过有人设了100个空闲连接,结果内存暴涨。要根据你的并发请求数来定。

三、数据压缩:让数据瘦身

数据压缩,说白了就是让传输的数据变小。数据小了,传输就快,流量也省。我做过一个项目,压缩前接口返回2MB,压缩后只有200KB,加载速度提升了10倍。

3.1 Gzip压缩

Gzip是最常用的压缩方式,对文本类数据效果特别好。JSON、XML这些,压缩率能达到70%-90%。

// OkHttp启用Gzip
// OkHttp默认支持Gzip,但需要服务端配合
// 客户端无需额外代码,OkHttp会自动处理

// 如果需要手动控制,可以添加拦截器
class GzipInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val compressedRequest = originalRequest.newBuilder()
            .header("Accept-Encoding", "gzip")
            .build()
        return chain.proceed(compressedRequest)
    }
}

注意:Gzip对图片、视频等二进制数据效果很差,甚至可能变大。别什么数据都Gzip,要区分场景。

3.2 Protobuf序列化

Protobuf是Google的序列化协议,比JSON小3-10倍,解析速度也快很多。我强烈推荐在接口设计中使用Protobuf。

// Protobuf定义示例
message UserInfo {
    int32 id = 1;
    string name = 2;
    string avatar = 3;
    int32 age = 4;
}

// 对比:JSON vs Protobuf
// JSON: {"id":12345,"name":"张三","avatar":"url","age":28}  -> 约50字节
// Protobuf: 二进制编码 -> 约15字节

我的经验:如果服务端不支持Protobuf,可以用Gzip压缩JSON。如果服务端支持,果断上Protobuf。我在一个项目中把接口数据从JSON换成Protobuf,流量直接降了60%。

四、弱网环境适配策略

弱网环境,说白了就是网络质量差。丢包率高、延迟大、带宽低。这种情况下,常规的网络请求策略会出问题。

4.1 超时策略

弱网下,超时时间要动态调整。不能一刀切。

// 动态超时策略
class TimeoutStrategy {
    private val networkMonitor = NetworkMonitor()
    
    fun getTimeout(): Long {
        return when (networkMonitor.getNetworkQuality()) {
            NetworkQuality.GOOD -> 10_000L    // 好网络:10秒
            NetworkQuality.NORMAL -> 20_000L  // 一般网络:20秒
            NetworkQuality.POOR -> 30_000L    // 差网络:30秒
            NetworkQuality.VERY_POOR -> 45_000L // 极差网络:45秒
        }
    }
}

4.2 重试策略

弱网下请求失败是常态。但重试不能无脑重试,要有策略。

// 指数退避重试
class RetryStrategy {
    private val maxRetries = 3
    private val baseDelay = 1000L // 1秒
    
    fun retry(block: () -> Result<Response>): Result<Response> {
        var retryCount = 0
        var delay = baseDelay
        
        while (retryCount < maxRetries) {
            try {
                val result = block()
                if (result.isSuccess) return result
            } catch (e: Exception) {
                // 记录失败原因
                Log.w("Retry", "请求失败,第${retryCount + 1}次重试")
            }
            
            // 指数退避:1s, 2s, 4s
            Thread.sleep(delay)
            delay *= 2
            retryCount++
        }
        
        return Result.failure(Exception("重试${maxRetries}次后仍然失败"))
    }
}

4.3 降级策略

弱网下,有些功能可以降级。比如图片加载,可以先加载缩略图,等网络好了再加载原图。

// 图片加载降级
class ImageLoader {
    fun loadImage(url: String, quality: ImageQuality) {
        when (quality) {
            ImageQuality.HIGH -> loadOriginal(url)
            ImageQuality.MEDIUM -> loadThumbnail(url, 480)
            ImageQuality.LOW -> loadThumbnail(url, 240)
            ImageQuality.PLACEHOLDER -> showPlaceholder()
        }
    }
}

核心原则:弱网下,保证核心功能可用,非核心功能降级或延迟。用户能看内容,比什么都看不到强。

4.4 网络状态监听

实时监听网络状态变化,动态调整策略。

// 网络状态监听
class NetworkMonitor(private val context: Context) {
    private val connectivityManager = 
        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    
    fun getNetworkQuality(): NetworkQuality {
        val network = connectivityManager.activeNetwork ?: return NetworkQuality.NONE
        val caps = connectivityManager.getNetworkCapabilities(network) ?: return NetworkQuality.NONE
        
        return when {
            caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> NetworkQuality.GOOD
            caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
                // 根据信号强度判断
                evaluateCellularQuality()
            }
            else -> NetworkQuality.NORMAL
        }
    }
}

避坑指南:我曾经在弱网下用了无限重试,结果用户手机发烫、流量跑光。记住,重试要有上限,而且每次重试之间要有间隔。

总结

网络优化,说白了就是四个字:快、省、稳、智。

  • 快:HTTPDNS + 连接池复用,减少等待时间
  • 省:Gzip + Protobuf,减少传输数据量
  • 稳:合理的超时和重试策略,保证请求成功率
  • 智:根据网络质量动态调整策略,适配各种场景

这些优化手段,我建议你从最简单的开始做。先上Gzip压缩,再上连接池优化,最后考虑HTTPDNS和弱网适配。一步一步来,别想一口吃成胖子。


公众号:蓝海资料掘金营,微信deep3321