性能优化之网络优化:DNS优化、连接池、数据压缩、缓存策略

网络优化这个话题,说实话,是Android开发里最容易出彩也最容易踩坑的地方。我见过太多应用,功能写得挺完善,一上弱网环境就卡成PPT。说白了,用户感知到的“快”与“慢”,很大程度上取决于你的网络层扛不扛得住。

今天咱们就聊聊网络优化的四个核心方向:DNS优化、连接池、数据压缩、缓存策略。这四个点做好了,你的App在网络层面基本就稳了。

核心观点:网络优化的本质是减少等待时间、减少数据传输量、减少不必要的请求。别想着一次搞定所有,先从最痛的点下手。

网络优化四大支柱 DNS优化 预解析 · 缓存 · HTTPDNS 连接池 复用 · 保活 · 超时控制 数据压缩 Gzip · Brotli · 序列化 缓存策略 内存 · 磁盘 · 过期策略 目标:减少等待 → 减少传输 → 减少请求 最终效果:弱网环境下的流畅体验

一、DNS优化:别让域名解析拖后腿

DNS解析,说白了就是把域名转成IP地址。这个过程看似简单,但坑特别多。我遇到过最夸张的一次,某个功能在WiFi下秒开,切到4G就卡死,查了半天发现是DNS解析耗时从50ms飙到了3秒。

为什么会这样?因为运营商DNS经常不稳定,而且容易被劫持。你想想看,用户点开你的App,光解析域名就花了2秒,这体验能好吗?

1.1 预解析

我个人习惯在App启动时,就把后续会用到的域名提前解析好。比如首页需要请求A、B、C三个域名,那在闪屏页展示的时候,后台线程就把它们全解析了。

// 使用OkHttp的Dns接口做预解析
class PrefetchDns : Dns {
    private val cache = mutableMapOf<String, List<InetAddress>>()
    
    override fun lookup(hostname: String): List<InetAddress> {
        return cache[hostname] ?: run {
            val result = Dns.SYSTEM.lookup(hostname)
            cache[hostname] = result
            result
        }
    }
    
    fun preFetch(vararg hosts: String) {
        hosts.forEach { host ->
            thread {
                lookup(host) // 提前触发解析
            }
        }
    }
}

1.2 HTTPDNS

传统DNS走UDP协议,容易被中间人篡改。HTTPDNS走HTTPS,直接绕过运营商。我在项目中用过阿里云的HTTPDNS服务,效果立竿见影——劫持率从5%降到了0.1%以下。

小技巧:HTTPDNS的解析结果可以本地持久化,下次启动直接读缓存,连网络请求都省了。但记得设置合理的过期时间,我一般设24小时。

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

TCP三次握手的开销其实不小。你想想看,每次请求都新建连接、握手、再释放,光握手就浪费了1-2个RTT。连接池的作用就是复用已有的连接,避免重复握手。

OkHttp默认就带了连接池,但很多人不知道调参。默认最大空闲连接数是5,保活时间是5分钟。说实话,这个配置对大多数场景够用,但如果你做的是即时通讯类应用,就得改一改。

参数 默认值 建议值(高并发场景) 说明
maxIdleConnections 5 20-50 最大空闲连接数,太多会浪费内存
keepAliveDuration 5分钟 10-30秒 保活时间,太长可能导致连接失效
connectionPool 默认实现 自定义 可以按域名拆分连接池

注意:连接池不是越大越好。我曾经在一个项目里把maxIdleConnections设成了200,结果内存直接飙了100多MB。后来压测发现,50个连接就完全够用了。记住,连接池是复用,不是预创建。

三、数据压缩:能省一点是一点

网络传输的瓶颈往往不在带宽,而在延迟。但数据量大了,带宽也会成为瓶颈。压缩数据,说白了就是用CPU时间换网络时间。

3.1 Gzip vs Brotli

Gzip是标配,几乎所有服务器都支持。Brotli是Google推出的新算法,压缩率比Gzip高20%-30%。但Brotli在Android上的支持需要API 28+,低版本得自己集成。

// OkHttp开启Gzip
val client = OkHttpClient.Builder()
    .addInterceptor { chain ->
        val request = chain.request().newBuilder()
            .header("Accept-Encoding", "gzip")
            .build()
        val response = chain.proceed(request)
        // OkHttp会自动解压gzip
        response
    }
    .build()

// 手动解压(如果服务器返回了压缩数据)
fun unGzip(body: ResponseBody): String {
    return GzipSource(body.source()).buffer().readUtf8()
}

3.2 序列化优化

JSON虽然方便,但冗余字符太多。我习惯用Protocol Buffers或者FlatBuffers来替代JSON。举个例子,一个用户信息对象,JSON要200字节,Protobuf只要80字节。在列表接口里,这个差距就很明显了。

我的经验:不要一股脑全上Protobuf。对于内部接口(比如App和自家服务器通信),用Protobuf很划算。但对于第三方接口,JSON还是更通用。说白了,看场景选方案。

四、缓存策略:让数据离用户更近

缓存是网络优化的终极武器。你想想看,如果数据已经在本地了,那还请求什么网络?但缓存策略设计不好,要么数据太旧,要么缓存太大撑爆存储。

4.1 多级缓存架构

我一般用三层缓存:内存缓存 → 磁盘缓存 → 网络请求。内存最快但容量小,磁盘慢一点但容量大,网络最慢但数据最新。

// 一个简单的多级缓存示例
class MultiLevelCache(private val api: ApiService) {
    private val memoryCache = LruCache<String, String>(50) // 内存缓存
    
    fun getData(key: String): String {
        // 1. 查内存
        memoryCache.get(key)?.let { return it }
        
        // 2. 查磁盘
        val diskData = readFromDisk(key)
        if (diskData != null) {
            memoryCache.put(key, diskData) // 回填内存
            return diskData
        }
        
        // 3. 请求网络
        val networkData = api.fetchData(key)
        memoryCache.put(key, networkData)
        writeToDisk(key, networkData)
        return networkData
    }
}

4.2 缓存过期策略

缓存不是永久有效的。我常用的策略有三种:

  • TTL(Time To Live):固定时间过期,比如5分钟。适合新闻列表这类数据。
  • LRU(Least Recently Used):最近最少使用淘汰。适合图片缓存。
  • 版本号控制:服务端返回数据版本号,本地版本号不一致就更新。适合配置类数据。

避坑指南:我曾经在一个项目里把缓存过期时间设成了1小时,结果用户反馈数据一直不更新。后来改成“先展示缓存,后台静默更新”的策略,体验就好了很多。记住,缓存策略要结合业务场景来设计,别一刀切。

总结

网络优化这四个方向,说白了就是:让请求更快到达、让连接更少建立、让数据更小传输、让请求更少发出。每个方向都有很多细节,但核心思路是一致的——减少等待时间。

我个人建议,先从DNS优化和缓存策略入手,这两个见效最快。连接池和数据压缩属于锦上添花,但做得好也能明显提升体验。嗯,今天就聊到这儿,希望对你有帮助。


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