基于 MrPeakTech 原文《深度优化iOS网络模块》,在 DNS映射、请求压缩、请求合并、安全性、并发数、可靠性保障、多通道、网络监控等方面进行纵深挖掘与横向扩展。
一、DNS 优化(纵深)
iOS 上 DNS 缓存极易失效(断网重连、重启、切换 Wi-Fi/Cellular、VPN 连接等都会清空缓存),每次都要重新走完整解析链路,耗时 100ms~600ms 不等。
1.1 传统 Local DNS 的三大痼疾
- DNS 劫持:运营商篡改解析结果,插入广告或重定向到错误 IP,尤其在 HTTP 场景下极为常见
- 调度不精准:Local DNS 的出口 IP 常常与用户真实地理位置不一致,导致 CDN 把用户调度到远端节点,延迟暴增
- 解析延迟不可控:递归查询经过根域名 → 顶级域名 → 权威域名服务器,弱网下可能耗时 500ms+
1.2 HTTPDNS 方案
绕过运营商的 Local DNS,客户端通过 HTTP 请求直接向 HTTPDNS 服务器获取域名对应的 IP。
核心流程:App 启动时预解析高频域名 → 结果写入本地缓存(内存+磁盘双层)→ 发起请求时用 IP 直连 → 缓存过期后异步刷新。
关键坑点:
- IP 直连后 HTTPS 的 SNI 问题:TLS 握手时 ClientHello 中的 Server Name 必须是域名而非 IP,否则证书校验失败。需要在 TLS 层手动设置 SNI
- HTTP 请求头中的
Host字段需要手动写回原始域名 - Cookie 是按域名存储的,IP 直连后 Cookie 匹配会失效,需要手动注入
1.3 DNS 预解析 + 持久化
App 冷启动时从磁盘加载上次的 DNS 缓存(即使可能过期,也比重新解析快),同时异步发起预解析刷新。用户在看到启动页的那几百毫秒内,DNS 已经准备好了。
1.4 IPv6 兼容
Apple 强制要求 IPv6-only 网络可用。HTTPDNS 需同时返回 A 记录(IPv4)和 AAAA 记录(IPv6),客户端采用 Happy Eyeballs 算法:同时发起 IPv4 和 IPv6 连接,谁先成功用谁。
二、连接层优化(非常关键)
DNS 之后是建立连接,这一层的优化空间巨大。
2.1 TCP 握手开销
每个新建 TCP 连接消耗至少 1 个 RTT(Round Trip Time)。3G 下一个 RTT 约 200-500ms,4G 约 50-100ms,Wi-Fi 约 5-50ms。如果加上 TLS 握手,HTTPS 首次连接需要消耗 3 个 RTT 才能开始传输数据。这就是为什么用户在弱网下感觉"打开页面很慢"。
2.2 连接复用(Keep-Alive)
HTTP/1.1 默认开启 Keep-Alive,同一个 TCP 连接可发送多个请求。但 HTTP/1.1 的致命问题是队头阻塞 — 一个请求卡住,后面的请求全部排队等待。浏览器的做法是对同一 Host 开 6 个并发连接来缓解,但这增加了服务器负担。
2.3 HTTP/2 多路复用(重要升级)
HTTP/2 用二进制分帧彻底解决了 HTTP 层队头阻塞:
- 单个 TCP 连接上可以并发数百个请求,帧(Frame)级别交错传输,互不阻塞
- HPACK 头部压缩:维护动态表做增量编码,首次请求头可能 800 字节,后续同类请求只需 20 字节
- 流优先级:客户端可标记哪些请求更紧急,服务器优先处理
- 服务器推送:服务器预判客户端需要的资源并主动推送
iOS 9+ 的 NSURLSession 原生支持 HTTP/2,只要服务器配置了 ALPN 协议协商即可自动启用,客户端无需改代码。
2.4 TLS 优化
HTTPS 的 TLS 握手本身就很昂贵,优化手段:
- TLS Session Resumption:首次握手后缓存会话参数(Session Ticket),后续连接跳过完整握手,节省 1 RTT
- TLS 1.3:iOS 12.2+ 支持,将握手从 2 RTT 压缩到 1 RTT,甚至支持 0-RTT 恢复(有重放攻击风险,仅用于幂等请求)
- 证书链优化:精简证书链长度;使用 ECC 证书替代 RSA(密钥短、握手快)
- OCSP Stapling:服务端预置证书吊销状态,避免客户端额外请求 CA 验证
2.5 QUIC / HTTP/3(前沿方向)
QUIC 基于 UDP,是 HTTP/3 的传输层,解决了 TCP 的根本性缺陷:
| 痛点 | TCP | QUIC |
|---|---|---|
| 连接建立 | TCP+TLS = 2~3 RTT | 首次 1 RTT,后续 0 RTT |
| 队头阻塞 | TCP 层丢一个包,所有流都阻塞 | 流级别独立,丢包只影响单个流 |
| 网络切换 | Wi-Fi↔4G 时 IP 变化,连接断开重建 | 用 Connection ID 标识,网络切换无缝衔接 |
| 拥塞控制 | 内核实现,升级困难 | 用户态实现,可随时更换算法 |
网络切换场景是 QUIC 最大的亮点:用户从公司 Wi-Fi 走到电梯切到 4G,TCP 连接必断,QUIC 则无感知恢复。iOS 15+ 的 URLSession 已原生支持 HTTP/3 自动协商。
三、请求数据优化(深入)
3.1 TCP Slow Start 的影响
新连接的初始拥塞窗口(initcwnd)通常是 10 个 MSS,即约 14.6KB。如果数据超过这个大小,TCP 必须等待服务器的 ACK 确认后才能继续发送更多数据,等于多了一个 RTT。
优化目标:让首屏关键请求的数据量尽可能控制在 14KB 以内,这样一个 RTT 就能完成传输。
3.2 数据序列化格式对比
| 格式 | 体积 | 解析速度 | 可读性 | 适用场景 |
|---|---|---|---|---|
| JSON | 基准 | 基准 | 好 | 通用 API |
| Protocol Buffers | JSON 的 1/3~1/10 | 20~100 倍 | 差 | 高频/大数据量接口 |
| FlatBuffers | 最小 | 最快(零拷贝解析) | 差 | 游戏、实时数据 |
| MessagePack | JSON 的 1/2 | 2~5 倍 | 差 | JSON 的二进制替代 |
Protobuf 不仅体积小,还有严格的 schema 定义和向前/向后兼容性,适合长期迭代的项目。对于体量大的 App,高频接口(如 Feed 流、IM 消息)从 JSON 迁移到 Protobuf,网络流量可降低 60%+。
3.3 图片/多媒体优化
网络流量中图片通常占 60% 以上:
- 格式升级:JPEG → WebP(小 25-35%,iOS 14+)→ AVIF(再小 20%,iOS 16+)
- 按需裁剪:通过 CDN 参数请求与屏幕尺寸匹配的图片,而非原图
- 渐进式加载:先加载低质量缩略图(几 KB),再加载高清图
- 预加载策略:根据用户滑动方向和速度,预测即将可见的图片并提前请求
3.4 增量更新
对于配置数据、列表数据等,不需要每次全量拉取。客户端带上本地数据的版本号或哈希,服务器对比后只返回 diff 部分。一个 10KB 的配置,可能增量 diff 只有 200 字节。
四、缓存体系(重要补充)
4.1 多级缓存架构
请求发起
→ L1 内存缓存(NSCache / LRU) 命中 → 耗时 < 1ms
→ L2 磁盘缓存(SQLite / 文件) 命中 → 耗时 5~20ms
→ L3 网络请求 成功 → 写入 L1 + L2,耗时 100ms~数秒
4.2 HTTP 缓存机制
两种机制配合使用:
强缓存(不发请求,直接用本地缓存):
Cache-Control: max-age=3600— 1 小时内直接用缓存- 适合静态资源(图片、JS、CSS)
协商缓存(发请求但可能不传数据):
ETag / If-None-Match(基于内容哈希)Last-Modified / If-Modified-Since(基于修改时间)- 服务器返回 304 Not Modified → 只传一个很小的响应头,省掉响应体
4.3 业务级缓存策略
最佳用户体验的策略是 CacheThenNetwork:打开页面瞬间展示上次的缓存数据(无白屏),同时后台请求最新数据,拿到后刷新 UI。用户感知到的加载时间几乎为零。
其他常见策略:
- NetworkFirst:优先网络,失败降级缓存(适合实时性要求高的数据)
- CacheFirst:优先缓存,过期才请求(适合变动少的配置数据)
- StaleWhileRevalidate:立即返回缓存,后台静默更新(下次打开时数据已是新的)
五、可靠性保障(深入)
将请求按业务属性分为三类:
- 第一类:关键核心数据,期望 100% 送达(如 IM 消息)→ 持久化到 DB
- 第二类:重要内容请求,需要较高成功率(如首页数据)→ 自动重试
- 第三类:一般性请求,对成功率无要求(如 UV 打点)→ 发一次即可
5.1 指数退避 + 抖动
重试间隔不应该是固定的(如每 3 秒重试一次),而应使用指数退避:
第1次重试:1秒后
第2次重试:2秒后
第3次重试:4秒后
第4次重试:8秒后
第5次重试:16秒后(达到上限则停止)
但如果大量用户同时遇到服务器故障,所有客户端都在相同时间点重试,会形成惊群效应,压垮刚恢复的服务器。解决方案是加随机抖动(Jitter):在退避时间基础上加减一个随机偏移,让重试时间分散开。
5.2 区分可重试错误
不是所有错误都应该重试:
- 应该重试:超时、连接被重置(RST)、DNS 解析失败、5xx 服务器错误
- 不应该重试:4xx 客户端错误(400 参数错误、401 未授权、403 禁止、404 不存在)— 重试多少次结果都一样
5.3 持久化队列的完整生命周期
- 请求创建 → 序列化写入 DB(状态:pending)
- 从 DB 读取 → 发送(状态:sending)
- 成功 → 从 DB 删除
- 失败 → 更新状态为 failed,加入重试队列
- App 被 kill → 重启后扫描 DB 中状态为 pending/sending/failed 的请求 → 重新发送
- 重试次数耗尽 → 通过 UI 告知用户手动重试
关键细节:请求的幂等性必须保证。因为网络请求可能实际已到达服务器但响应丢失,客户端误以为失败而重试,服务器需要通过请求 ID 去重,防止重复操作。
六、多通道(深入)
6.1 长连接通道的价值
HTTP 是短连接模型(请求-响应),每次通信都有协议开销(头部、握手等)。维护一条 TCP 长连接的价值:
- 服务器推送:服务器可以主动下发消息(新消息通知、配置更新、强制升级等)
- 低延迟:省去了 DNS + TCP + TLS 的建连开销
- 心跳保活:客户端定期发送心跳包维持连接,心跳间隔需要智能调整 — Wi-Fi 下可以较长(几分钟),Cellular 下需要较短(NAT 超时问题)
6.2 多通道容灾策略
核心消息发送策略:
主通道(TCP 长连接)发送
↓ 超时(如 5 秒无 ACK)
备用通道(HTTP 短连接)发送
↓ 仍然超时
进入重试队列,等待网络恢复
更激进的策略:对极关键的请求(如 IM 消息),TCP 和 HTTP 同时发送,谁先成功用谁,服务端通过消息 ID 去重。流量换可靠性,在 Wi-Fi 下这个代价可以接受。
6.3 UDP 通道的场景
UDP 在丢包率高的网络下反而表现更好(不需要等重传),适用于:
- 实时音视频(RTC)
- 实时游戏数据同步
- 在线直播的弹幕/互动消息
七、网络质量感知与自适应(深入扩展)
7.1 网络状态感知的层次
| 层次 | 信息 | 获取方式 |
|---|---|---|
| 连接类型 | Wi-Fi / 4G / 3G / 2G / 无网络 | Reachability / NWPathMonitor |
| 信号强度 | RSSI、信号格数 | CTTelephonyNetworkInfo |
| 实际网络质量 | RTT、丢包率、带宽 | 需要主动探测或从历史请求统计 |
仅靠连接类型远远不够 — 连着 Wi-Fi 不代表网络好(如酒店/机场的劣质 Wi-Fi),4G 信号满格不代表不拥塞(万人演唱会)。真正有用的是实际网络质量。
7.2 实时网络质量评估
通过统计最近 N 次请求的表现来评估真实网络质量:
- 平均 RTT:< 100ms 优秀,100-300ms 正常,300ms-1s 较差,> 1s 极差
- 请求成功率:最近 20 次请求中成功的比例
- 丢包率:TCP 层的重传比例
综合打分后分为几个档位(Excellent / Good / Moderate / Poor / VeryPoor),不同档位采取不同策略。
7.3 自适应策略
| 策略 | 网络好 | 网络差 |
|---|---|---|
| 图片质量 | 高清原图 | 缩略图/低质量图 |
| 预加载 | 积极预加载 | 仅加载当前可见内容 |
| 请求超时 | 15 秒 | 30 秒(给弱网更多时间) |
| 重试次数 | 1 次 | 3 次 |
| 数据格式 | 可以用 JSON | 优先 Protobuf(更小) |
| 视频清晰度 | 1080P | 360P 或暂停预加载 |
抖音、快手等 App 的"智能降级"就是这个思路 — 弱网时自动降低视频清晰度,保证流畅性。
八、请求优先级调度
8.1 优先级分类
- P0 紧急:用户正在等待的请求(当前页面接口、支付)— 立即执行
- P1 重要:即将需要的数据(下一页预加载)— 正常排队
- P2 一般:后台数据同步、配置拉取 — 低优先级
- P3 可延迟:埋点上报、日志收集 — 空闲时批量发送
8.2 调度机制
维护一个优先级队列,高优先级请求可以"插队"。关键规则:
- P0 请求到来时,如果并发已满,可以取消或暂停最低优先级的正在执行的请求,让出连接
- P3 请求不应在关键操作(如 App 启动、页面打开)期间发起,等到空闲期再批量处理
- 同一优先级内按 FIFO 顺序
8.3 带宽竞争问题
在有限带宽下(如 3G),多个请求同时进行会相互争抢带宽,导致所有请求都慢。正确做法是:弱网下降低并发数(如从 4 降到 2),让少量请求快速完成,而不是多个请求都半死不活。
九、安全性(深入扩展)
9.1 证书锁定(Certificate Pinning)
即使使用 HTTPS,中间人攻击仍然可能通过伪造 CA 证书实现(如公司网络的代理、恶意 Wi-Fi)。证书锁定的思路是:App 内预埋服务器证书(或公钥)的哈希,TLS 握手时校验服务器证书是否与预埋值匹配,不匹配则拒绝连接。
两种锁定方式:
- 证书锁定:锁定整个证书,证书到期更换时 App 必须同步更新,否则全部无法联网(风险高)
- 公钥锁定:只锁定公钥,证书续签时只要公钥不变就不影响(推荐)
9.2 请求签名防篡改
对请求参数做签名,防止中间人篡改:将关键参数排序拼接 + 加上密钥 → 做 HMAC-SHA256 → 签名放入 Header。服务端用同样逻辑验证,签名不匹配则拒绝。
9.3 防重放攻击
攻击者可以截获一个合法请求,反复重发。防御手段:
- 请求中加入时间戳,服务端拒绝超过 5 分钟的请求
- 请求中加入一次性随机数(nonce),服务端校验是否已使用过
- 两者结合使用
9.4 敏感数据加密
HTTPS 只保证传输过程加密,如果需要端到端加密(即使服务端被入侵也看不到明文),需要对敏感字段单独做 AES 加密,密钥通过 RSA 或 ECDH 协商。
十、监控与度量(深入扩展)
10.1 核心指标
| 指标 | 含义 | 告警阈值 |
|---|---|---|
| 请求成功率 | 成功请求 / 总请求 | < 95% 告警 |
| 平均耗时 | DNS + 连接 + 首字节 + 传输完成 | > 3s 告警 |
| DNS 耗时 | DNS 解析时间 | > 200ms 告警 |
| TCP 握手耗时 | TCP 建连时间 | > 300ms 告警 |
| TLS 握手耗时 | TLS 建连时间 | > 500ms 告警 |
| 首字节时间(TTFB) | 从请求发出到收到第一个字节 | > 1s 告警 |
10.2 分阶段耗时采集
iOS 的 NSURLSessionTaskMetrics(iOS 10+)提供了请求全链路的分阶段耗时数据:
DNS 解析开始/结束
TCP 连接开始/结束
TLS 握手开始/结束
请求发送开始/结束
响应接收开始/结束
这些数据可以精确定位瓶颈在哪个阶段。比如发现 DNS 阶段占了 40% 的总耗时,就重点优化 DNS(第一节的方案)。
10.3 监控数据上报策略
监控数据本身也是网络请求,需要避免"监控数据影响正常业务"的问题:
- 批量上报:攒够一批(如 50 条)或到达时间窗口(如 30 秒)再一次性上报
- 采样上报:不需要 100% 上报,按用户采样(如 10%),足够做统计分析
- 低优先级:使用 P3 优先级,空闲期才上报
- 数据压缩:监控数据用 Protobuf 或 gzip 压缩后上报
十一、弱网优化专题(实战价值高)
11.1 弱网的定义
不只是"没网"才叫弱网。以下都算弱网场景:
- 电梯、地铁、隧道(信号遮挡)
- 万人演唱会/体育场(基站拥塞)
- Wi-Fi↔4G 切换瞬间(连接重建)
- 劣质公共 Wi-Fi(高延迟高丢包)
11.2 弱网优化策略全景
- 超时时间动态调整:根据网络质量评分动态设置,弱网给更长时间
- 请求精简:弱网下只请求核心数据,非关键字段延迟加载
- 离线优先架构:先展示本地数据,网络恢复后同步。用户操作先落本地 DB,后台队列异步发送
- 断点续传:大文件上传/下载支持断点续传,网络中断后从断点恢复而非从头开始
- 数据预取:在网络好的时候(如 Wi-Fi 充电中)预加载用户可能需要的数据
十二、整体架构总结
一个成熟的 iOS 网络模块应该是分层架构:
┌─────────────────────────────────────┐
│ 业务层 API │ ← 简洁的调用接口
├─────────────────────────────────────┤
│ 缓存管理 / 请求优先级调度 │ ← 策略层
├─────────────────────────────────────┤
│ 请求构建 / 签名 / 压缩 / 序列化 │ ← 数据处理层
├─────────────────────────────────────┤
│ 可靠性保障(持久化 / 重试 / 多通道) │ ← 可靠性层
├─────────────────────────────────────┤
│ 连接管理 / 连接池 / 协议协商 │ ← 连接层(HTTP/2、QUIC)
├─────────────────────────────────────┤
│ DNS 解析(HTTPDNS + 缓存) │ ← DNS 层
├─────────────────────────────────────┤
│ 网络质量感知 / 监控上报 │ ← 贯穿所有层的基础设施
└─────────────────────────────────────┘
每一层各司其职,上层不需要关心下层的实现细节。优化是逐层推进的 — 先做投入产出比最高的(HTTP/2、缓存策略、HTTPDNS),再做精细化的(QUIC、自适应策略、多通道容灾)。