9、网络优化(上):HTTP与HTTPS、DNS解析优化、连接池复用、请求合并与压缩

网络优化这个话题,说实话,是Android性能优化里最容易被忽视但又最值得投入的一块。你想想看,用户打开App,第一眼看到的是界面,但真正决定他等多久的,往往是网络请求。我见过太多App,UI动画做得飞起,结果一个接口卡了3秒,用户直接划走了。

今天我们先聊网络优化的前半部分——HTTP/HTTPS、DNS、连接池、请求合并与压缩。这些都是基础,但基础往往最要命。

HTTP与HTTPS:选哪个?怎么选?

这个问题看似简单,但我在项目中遇到过不止一次因为协议选择不当导致的性能问题。先明确一点:能用HTTPS就别用HTTP。这不是什么情怀问题,而是现实——Android 9之后,明文流量默认是被禁掉的。你非要用HTTP,还得在network_security_config.xml里手动放行,麻烦不说,还不安全。

核心结论:HTTPS的握手开销可以通过会话复用(Session ID / Session Ticket)大幅降低。实际测试中,首次握手约1-2个RTT,复用后几乎可以降到0.5个RTT以内。

但这里有个坑——TLS 1.3。我记得有一次排查一个线上问题,发现某些老旧手机在TLS 1.3握手时会出现奇怪的超时。后来查了日志,是服务器端的TLS 1.3实现有bug。所以我的建议是:客户端优先使用TLS 1.3,但必须降级兼容TLS 1.2。别为了追求极致性能把兼容性丢了。

注意:不要随意关闭证书校验。我曾经见过有团队为了省事,直接信任所有证书。结果呢?用户数据在中间人攻击面前就是裸奔。性能再快,安全没了,一切归零。

DNS解析优化:一个容易被忽略的瓶颈

DNS解析,说白了就是把你输入的域名翻译成IP地址。这个过程有多慢?我实测过,普通DNS解析耗时在50ms到500ms之间,差的甚至能到1秒以上。你想想看,用户点开App,光DNS就卡了半秒,这体验能好吗?

为什么会这么慢?因为传统的DNS解析是同步的,而且每次请求都要走一遍完整的递归查询。更糟糕的是,Android系统默认的DNS缓存时间很短,有的甚至不缓存。

那怎么优化?我个人的习惯是这么做的:

  • 使用HTTPDNS:直接绕过系统DNS,通过HTTP请求获取IP。这样能避免Local DNS劫持,还能自定义调度策略。我在项目中用过阿里云的HTTPDNS,效果很明显,平均解析时间从300ms降到了20ms以内。
  • 预解析:在App启动时,或者用户进入某个页面之前,提前把可能用到的域名解析好。比如你可以在Application.onCreate()里启动一个异步任务,把首页、登录页等核心页面的域名都解析一遍。
  • 缓存IP:把解析结果存到本地数据库或SharedPreferences里。下次启动时直接读取缓存,连HTTPDNS都不用走。当然,缓存要有过期策略,我一般设置24小时。

小技巧:如果你用OkHttp,可以自定义Dns接口,把HTTPDNS集成进去。代码大概长这样:

class HttpDns : Dns {
    override fun lookup(hostname: String): List<InetAddress> {
        // 先从本地缓存查
        val cached = cache.get(hostname)
        if (cached != null) return cached
        
        // 走HTTPDNS
        val ip = httpDnsService.resolve(hostname)
        val addresses = InetAddress.getAllByName(ip).toList()
        cache.put(hostname, addresses)
        return addresses
    }
}

连接池复用:别让TCP握手成为瓶颈

TCP三次握手的开销有多大?每次握手至少1个RTT。如果每次请求都新建连接,那光握手就浪费了大量时间。更别说还有TLS握手,那更慢。

连接池复用的思路很简单:用完的连接别急着关,留着给下一个请求用。OkHttp默认就支持连接池,最大空闲连接数默认是5个,每个连接的最大空闲时间是5分钟。这个配置在大多数场景下够用,但如果你有特殊需求,可以调整:

val client = OkHttpClient.Builder()
    .connectionPool(ConnectionPool(10, 30, TimeUnit.SECONDS))
    .build()

这里我踩过一个坑:连接池不是越大越好。有一次我把最大空闲连接数调到了50,结果发现内存占用飙升,而且很多连接其实根本用不上。后来我根据实际并发量做了压测,发现10个连接就足够了。

关键点:连接池复用的前提是Keep-Alive。如果服务器不支持Keep-Alive,或者设置了很短的超时时间,那连接池的效果会大打折扣。建议和服务端同学确认一下Keep-Alive的配置。

请求合并与压缩:少发请求,多发数据

网络请求的耗时,很大一部分花在建立连接和传输头部上。如果你有多个小请求,不如把它们合并成一个。举个例子:

  • 首页需要展示用户信息、消息列表、推荐内容。如果分三个接口请求,那就是三次握手、三次DNS解析、三次头部传输。如果合并成一个接口,一次搞定。
  • 当然,合并也有代价——接口的粒度变粗了,复用性变差。所以我的建议是:按页面维度合并,一个页面一个接口,不要搞成全局大接口。

再说压缩。HTTP/1.1支持gzip压缩,HTTP/2支持HPACK头部压缩。这些压缩技术能显著减少传输数据量。我实测过,开启gzip后,JSON数据的大小能减少70%以上。

注意:压缩不是万能的。对于已经压缩过的数据(比如图片、视频),再压缩一次反而会增大体积。另外,压缩和解压会消耗CPU,如果设备性能较差,可能得不偿失。我的经验是:文本类数据(JSON、XML)必开压缩,二进制数据视情况而定

知识体系总览

下面这张图是我自己整理的,把今天讲的内容串了起来。你可以把它当作一个检查清单,看看自己的App在哪些环节还有优化空间。

网络优化(上)知识体系 网络优化核心 HTTP/HTTPS DNS解析优化 连接池复用 请求合并与压缩 TLS 1.3优先 降级兼容 HTTPDNS 预解析+缓存 Keep-Alive 合理配置大小 按页面合并 gzip压缩

好了,今天的内容就到这里。网络优化是个系统工程,今天讲的这些是基础中的基础。你可以在自己的项目里先挑一两个点试试,比如把DNS换成HTTPDNS,或者调整一下连接池的配置。效果应该很快就能看到。


专注资料整理