关于 axiom-networking-diag
面向 iOS 开发者的 Network.framework 网络诊断技能,用决策树和分步模式定位连接、TLS、数据传输及性能问题。它强调先收集状态与日志,再逐项验证原因和修复。
主要功能
- 01按连接状态引导诊断
- 02覆盖 DNS 与 TLS 故障
- 03排查消息分帧问题
- 04诊断拥塞和缓冲问题
- 05提供验证与测试步骤
适用场景
- 01排查连接卡在准备或等待状态
- 02分析 TLS 证书验证失败
- 03诊断发送成功但接收不到数据
- 04调查网络切换后的连接或性能问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-networking-diag安装后在 Claude Code 中输入/axiom-networking-diag确认已加载。
详细介绍
Network.framework 诊断
概述
核心原则: 85% 的网络问题源于误解连接状态、未处理网络切换或错误处理不当,而非 Network.framework 缺陷。
Network.framework 经受了实战考验,存在于每个 iOS 应用中(为 URLSession 提供底层支持),每天处理数万亿个请求,并通过 Happy Eyeballs、代理评估和 WiFi Assist 提供智能连接建立能力。如果连接失败、超时或行为异常,问题几乎总是出在框架的使用方式上,而非框架本身。
本技能可帮助你通过系统化诊断,在几分钟而非几小时内找出根本原因。
危险信号——怀疑存在网络问题
如果出现以下任一情况,应怀疑网络配置错误,而非框架故障:
-
连接超时 60 秒,且没有明确错误
-
某些网络上 TLS 握手因“证书无效”而失败
-
数据已发送,但接收方始终收不到
-
从 WiFi 切换到蜂窝网络时连接断开
-
在 WiFi 上运行正常,但在蜂窝网络上 100% 失败
-
在模拟器中运行正常,但在真机上失败
-
在你的网络上连接成功,但用户连接失败
-
❌ 禁止“Network.framework 坏了,我们应该改用套接字重写”
- Network.framework 为 URLSession 提供底层支持,而 URLSession 用于每个 iOS 应用
- 它能处理你使用套接字时可能要花数月才能发现的边缘情况
- Apple 工程师在生产环境中积累的 10 多年调试经验都融入了这个框架
- 切换到套接字会让你暴露于 100 多种边缘情况
关键区别: 模拟器使用 macOS 网络堆栈(而非 iOS),会掩盖蜂窝网络特有的问题(仅 IPv6 网络),也不会模拟网络切换。必须:在真实设备和真实网络条件下测试。
必须执行的第一步
务必先运行以下命令(然后再修改代码):
// 1. Enable Network.framework logging
// Add to Xcode scheme: Product → Scheme → Edit Scheme → Arguments
// -NWLoggingEnabled 1
// -NWConnectionLoggingEnabled 1
// 2. Check connection state history
connection.stateUpdateHandler = { state in
print("\(Date()): Connection state: \(state)")
// Log every state transition with timestamp
}
// 3. Check TLS configuration
// If using custom TLS parameters:
print("TLS version: \(tlsParameters.minimumTLSProtocolVersion)")
print("Cipher suites: \(tlsParameters.tlsCipherSuites ?? [])")
// 4. Test with packet capture (Charles Proxy or Wireshark)
// On device: Settings → WiFi → (i) → Configure Proxy → Manual
// Charles: Help → SSL Proxying → Install Charles Root Certificate on iOS
// 5. Test on different networks
// - WiFi
// - Cellular (disable WiFi)
// - Airplane Mode → WiFi (test waiting state)
// - VPN active
// - IPv6-only (some cellular carriers)
这些信息说明什么
| 现象 | 诊断 | 下一步 |
|---|---|---|
| 卡在 .preparing 状态超过 5 秒 | DNS 解析失败或网络已断开 | 模式 1a |
| 立即转为 .waiting 状态 | 无网络连接(飞行模式、无信号) | 模式 1b |
| .failed 且 POSIX 错误为 61 | 连接被拒绝(服务器未在监听) | 模式 1c |
| .failed 且 POSIX 错误为 50 | 网络已断开(接口已禁用) | 模式 1d |
| .ready 后立即变为 .failed | TLS 握手失败 | 模式 2b |
| .ready,发送成功,但没有数据到达 | 帧格式问题或接收方未处理数据 | 模式 3a |
| WiFi 可用,蜂窝网络失败 | 仅 IPv6 网络(硬编码 IPv4) | 模式 5a |
| 不使用 VPN 时正常,使用 VPN 时失败 | 代理干扰或 DNS 覆盖 | 模式 5b |
必须进行的判断
在修改任何代码之前,先确定以下一种情况:
- 如果卡在 .preparing 状态且网络可用 → DNS 解析失败(检查 nslookup)
- 如果立即进入 .waiting 状态且飞行模式已关闭 → 接口特定问题(蜂窝网络被阻止)
- 如果 .failed 且 POSIX 错误为 61 → 服务器问题(检查服务器日志)
- 如果 .failed 且出现 TLS 错误 -9806 → 证书验证问题(使用 openssl 检查)
- 如果 .ready 但数据未到达 → 帧格式或接收方问题(启用数据包捕获)
如果诊断结果互相矛盾或不明确
- 停止。暂时不要继续处理各种模式
- 为每次发送和接收调用添加时间戳日志
- 启用数据包捕获(Charles/Wireshark)
- 在另一台设备上测试,以区分硬件问题和软件问题
决策树
使用此决策树,在 2 分钟内找到正确的诊断模式:
Network problem?
├─ Connection never reaches .ready?
│ ├─ Stuck in .preparing for >5 seconds?
│ │ ├─ DNS lookup timing out? → Pattern 1a (DNS Failure)
│ │ ├─ Network available but can't reach host? → Pattern 1c (Connection Refused)
│ │ └─ First connection slow, subsequent fast? → Pattern 1e (DNS Caching)
│ │
│ ├─ Moves to .waiting immediately?
│ │ ├─ Airplane Mode or no signal? → Pattern 1b (No Connectivity)
│ │ ├─ Cellular blocked by parameters? → Pattern 1b (Interface Restrictions)
│ │ └─ VPN connecting? → Wait and retry
│ │
│ ├─ .failed with POSIX error 61?
│ │ └─ → Pattern 1c (Connection Refused)
│ │
│ └─ .failed with POSIX error 50?
│ └─ → Pattern 1d (Network Down)
│
├─ Connection reaches .ready, then fails?
│ ├─ Fails immediately after .ready?
│ │ ├─ TLS error -9806? → Pattern 2b (Certificate Validation)
│ │ ├─ TLS error -9801? → Pattern 2b (Protocol Version)
│ │ └─ POSIX error 54? → Pattern 2d (Connection Reset)
│ │
│ ├─ Fails after network change (WiFi → cellular)?
│ │ ├─ No viabilityUpdateHandler? → Pattern 2a (Viability Not Handled)
│ │ ├─ Didn't detect better path? → Pattern 2a (Better Path)
│ │ └─ IPv6 → IPv4 transition? → Pattern 5a (Dual Stack)
│ │
│ ├─ Fails after timeout?
│ │ └─ → Pattern 2c (Receiver Not Responding)
│ │
│ └─ Random disconnects?
│ └─ → Pattern 2d (Network Instability)
│
├─ Data not arriving?
│ ├─ Send succeeds, receive never returns?
│ │ ├─ No message framing? → Pattern 3a (Framing Problem)
│ │ ├─ Wrong byte count? → Pattern 3b (Min/Max Bytes)
│ │ └─ Receiver not calling receive()? → Check receiver code
│ │
│ ├─ Partial data arrives?
│ │ ├─ receive(exactly:) too large? → Pattern 3b (Chunking)
│ │ ├─ Sender closing too early? → Check sender lifecycle
│ │ └─ Buffer overflow? → Pattern 3b (Buffer Management)
│ │
│ ├─ Data corrupted?
│ │ ├─ TLS disabled? → Pattern 3c (No Encryption)
│ │ ├─ Binary vs text encoding? → Check ContentType
│ │ └─ Byte order (endianness)? → Use network byte order
│ │
│ └─ Works sometimes, fails intermittently?
│ └─ → Pattern 3d (Race Condition)
│
├─ Performance degrading?
│ ├─ Latency increasing over time?
│ │ ├─ TCP congestion? → Pattern 4a (Congestion Control)
│ │ ├─ No contentProcessed pacing? → Pattern 4a (Buffering)
│ │ └─ Server overloaded? → Check server metrics
│ │
│ ├─ Throughput decreasing?
│ │ ├─ Network transition WiFi → cellular? → Pattern 4b (Bandwidth Change)
│ │ ├─ Packet loss increasing? → Pattern 4b (Network Quality)
│ │ └─ Multiple streams competing? → Pattern 4b (Prioritization)
│ │
│ ├─ High CPU usage?
│ │ ├─ Not using batch for UDP? → Pattern 4c (Batching)
│ │ ├─ Too many small sends? → Pattern 4c (Coalescing)
│ │ └─ Using sockets instead of Network.framework? → Migrate (30% CPU savings)
│ │
│ └─ Memory growing?
│ ├─ Not releasing connections? → Pattern 4d (Connection Leaks)
│ ├─ Not cancelling on deinit? → Pattern 4d (Lifecycle)
│ └─ Missing [weak self]? → Pattern 4d (Retain Cycles)
│
└─ Works on WiFi, fails on cellular/VPN?
├─ IPv6-only cellular network?
│ ├─ Hardcoded IPv4 address? → Pattern 5a (IPv4 Literal)
│ ├─ getaddrinfo with AF_INET only? → Pattern 5a (Address Family)
│ └─ Works on some carriers, not others? → Pattern 5a (Regional IPv6)
│
├─ Corporate VPN active?
│ ├─ Proxy configuration failing? → Pattern 5b (PAC)
│ ├─ DNS override blocking hostname? → Pattern 5b (DNS)
│ └─ Certificate pinning failing? → Pattern 5b (TLS in VPN)
│
├─ Port blocked by firewall?
│ ├─ Non-standard port? → Pattern 5c (Firewall)
│ ├─ Outbound only? → Pattern 5c (NATing)
│ └─ Works on port 443, not 8080? → Pattern 5c (Port Scanning)
│
├─ Peer-to-peer connection failing?
│ ├─ NAT traversal issue? → Pattern 5d (STUN/TURN)
│ ├─ Symmetric NAT? → Pattern 5d (NAT Type)
│ └─ Local network only? → Pattern 5d (Bonjour/mDNS)
│
└─ URLSession fails but NWConnection works?
├─ HTTP URL blocked? → Pattern 6a (ATS HTTP Block)
├─ "SSL error" on HTTPS? → Pattern 6b (ATS TLS Version)
└─ Works on older iOS? → Pattern 6a/6b (ATS enforcement)
模式选择规则(强制)
开始使用某个模式前:
- 连接始终无法达到 .ready → 从模式 1 开始(DNS、连接问题、连接被拒绝)
- TLS 错误代码 → 直接跳到模式 2b(证书验证)
- 未收到数据 → 先启用数据包捕获,然后再使用模式 3
- 特定网络问题(WiFi 上正常,蜂窝网络上失败) → 在该确切网络上测试,使用模式 5
- 性能下降 → 使用 Instruments Network 模板进行性能分析,使用模式 4
一次只应用一种模式
- 应用一种模式中的修复方案
- 彻底测试
- 只有问题仍然存在时,才尝试下一种模式
- 不要同时应用多种模式(无法确定原因)
禁止事项
- 未经诊断就猜测解决方案
- 一次更改多个事项
- 认定“只是需要更长的超时时间”
- “暂时”禁用 TLS
- 为了“规避框架问题”而切换到套接字
诊断模式
模式 1a:DNS 解析失败
耗时 10-15 分钟
症状
- 连接卡在 .preparing 状态超过 5 秒
- 最终失败或超时
- 使用 IP 地址可以正常工作,但使用主机名不行
- 在一个网络上正常工作,在另一个网络上失败
诊断
// Enable DNS logging
// -NWLoggingEnabled 1
// Check DNS resolution manually
// Terminal: nslookup example.com
// Terminal: dig example.com
// Logs show:
// "DNS lookup timed out"
// "getaddrinfo failed: 8 (nodename nor servname provided)"
常见原因
- 无法访问 DNS 服务器(企业网络会屏蔽外部 DNS)
- 主机名拼写错误或不存在
- DNS 缓存中的条目已过期(不常见,但会发生)
- VPN 阻止 DNS 解析
修复
// ❌ WRONG — Adding timeout doesn't fix DNS
/*
let parameters = NWParameters.tls
parameters.expiredDNSBehavior = .allow // Doesn't help if DNS never resolves
*/
// ✅ CORRECT — Verify hostname, test DNS manually
// 1. Test DNS manually:
// $ nslookup your-hostname.com
// If this fails, DNS is the problem (not your code)
// 2. If DNS works manually but not in app:
// Check if VPN or enterprise config blocking app DNS
// 3. If hostname doesn't exist:
let connection = NWConnection(
host: NWEndpoint.Host("correct-hostname.com"), // Fix typo
port: 443,
using: .tls
)
// 4. If DNS caching issue (rare):
// Restart device to clear DNS cache
// Or use IP address temporarily while investigating DNS server issue
验证
- 运行
nslookup your-hostname.com— 应在不到 1 秒内返回 IP - 在蜂窝网络上测试(使用不同的 DNS 服务器)— 应能正常工作
- 检查企业网络的 DNS 配置
预防
- 使用知名主机名(不要依赖内部 DNS)
- 开发期间在多个网络上测试
- 不要硬编码 IP(如果 DNS 失败,应修复 DNS,而不是绕过它)
模式 2b:TLS 证书验证失败
耗时 15-20 分钟
症状
- 连接短暂达到 .ready 状态,然后立即变为 .failed
- 错误:
-9806(kSSLPeerCertInvalid) - 错误:
-9807(kSSLPeerCertExpired) - 错误:
-9801(kSSLProtocol) - 在某些服务上正常工作,在其他服务上失败
诊断
# Test TLS manually with openssl
openssl s_client -connect example.com:443 -showcerts
# Check certificate details
openssl s_client -connect example.com:443 | openssl x509 -noout -dates
# notBefore: Jan 1 00:00:00 2024 GMT
# notAfter: Dec 31 23:59:59 2024 GMT ← Check if expired
# Check certificate chain
openssl s_client -connect example.com:443 -showcerts | grep "CN="
# Should show: Subject CN=example.com, Issuer CN=Trusted CA
常见原因
- 自签名证书(开发/预发布服务)
- 证书已过期
- 证书主机名不匹配(证书适用于“example.com”,但连接的是“www.example.com”)
- 缺少中间 CA 证书
- TLS 1.0/1.1(iOS 13+ 要求 TLS 1.2+)
修复
适用于证书无效的生产服务
// ❌ WRONG — Never disable certificate validation in production
/*
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { ... }, .main)
// This disables validation → security vulnerability
*/
// ✅ CORRECT — Fix the certificate on server
// 1. Renew expired certificate (Let's Encrypt, DigiCert, etc.)
// 2. Ensure hostname matches (CN=example.com or SAN includes example.com)
// 3. Include intermediate CA certificates on server
// 4. Test with: openssl s_client -connect example.com:443
适用于开发服务(临时)
// ⚠️ ONLY for development/staging
#if DEBUG
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(
tlsOptions.securityProtocolOptions,
{ (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
// Trust any certificate (DEV ONLY)
sec_protocol_verify_complete(true)
},
.main
)
let parameters = NWParameters(tls: tlsOptions)
let connection = NWConnection(host: "dev-server.example.com", port: 443, using: parameters)
#endif
适用于证书固定
// Production-grade certificate pinning
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(
tlsOptions.securityProtocolOptions,
{ (metadata, trust, complete) in
let trust = sec_protocol_metadata_copy_peer_public_key(metadata)
// Compare trust with pinned certificate
let pinnedCertificateData = Data(/* your cert */)
let serverCertificateData = SecCertificateCopyData(trust) as Data
if serverCertificateData == pinnedCertificateData {
complete(true)
} else {
complete(false) // Reject non-pinned certificates
}
},
.main
)
验证
openssl s_client -connect example.com:443显示Verify return code: 0 (ok)- 证书将在 30 天以后才过期
- 证书 CN 与主机名匹配
- 在真实 iOS 设备上测试(不只是模拟器)
模式 3a:消息分帧问题
耗时 20-30 分钟
症状
- connection.send() 成功且没有错误
- connection.receive() 始终不返回数据
- 或 receive() 返回部分数据
- 数据包捕获显示线路上有字节,但应用未处理这些字节
诊断
// Enable detailed logging
connection.send(content: data, completion: .contentProcessed { error in
if let error = error {
print("Send error: \(error)")
} else {
print("✅ Sent \(data.count) bytes at \(Date())")
}
})
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, context, isComplete, error in
if let error = error {
print("Receive error: \(error)")
} else if let data = data {
print("✅ Received \(data.count) bytes at \(Date())")
}
}
// Use Charles Proxy or Wireshark to verify bytes on wire
常见原因 流协议(TCP/TLS)不会保留消息边界。
示例
// Sender sends 3 messages:
send("Hello") // 5 bytes
send("World") // 5 bytes
send("!") // 1 byte
// Receiver might get:
receive() → "HelloWorld!" // All 11 bytes at once
// Or:
receive() → "Hel" // 3 bytes
receive() → "loWorld!" // 8 bytes
// Message boundaries lost!
修复
方案 1:使用 TLV 分帧(iOS 26+)
// NetworkConnection with TLV
let connection = NetworkConnection(
to: .hostPort(host: "example.com", port: 1029)
) {
TLV {
TLS()
}
}
// Send typed messages
enum MessageType: Int {
case chat = 1
case ping = 2
}
let chatData = Data("Hello".utf8)
try await connection.send(chatData, type: MessageType.chat.rawValue)
// Receive typed messages
let (data, metadata) = try await connection.receive()
if metadata.type == MessageType.chat.rawValue {
print("Chat message: \(String(data: data, encoding: .utf8)!)")
}
方案 2:手动添加长度前缀(iOS 12-25)
// Sender: Prefix message with UInt32 length
func sendMessage(_ message: Data) {
var length = UInt32(message.count).bigEndian
let lengthData = Data(bytes: &length, count: 4)
connection.send(content: lengthData, completion: .contentProcessed { _ in
connection.send(content: message, completion: .contentProcessed { _ in
print("Sent message with length prefix")
})
})
}
// Receiver: Read length, then read message
func receiveMessage() {
// 1. Read 4-byte length
connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { lengthData, _, _, error in
guard let lengthData = lengthData else { return }
let length = lengthData.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
// 2. Read message of exact length
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { messageData, _, _, error in
guard let messageData = messageData else { return }
print("Received complete message: \(messageData.count) bytes")
}
}
}
验证
- 发送 10 条消息,确认接收方恰好收到 10 条消息
- 发送不同大小的消息(1 字节、1000 字节、64KB)
- 使用丢包模拟进行测试(Network Link Conditioner)
模式 4a:TCP 拥塞和缓冲
耗时 15-25 分钟
症状
- 开始几次发送速度很快,之后逐渐变慢
- 延迟随时间从 50ms 增至 500ms,再增至 2000ms
- 内存用量增加(未发送数据被缓冲)
- 用户反映应用在运行 5 分钟后“感觉迟钝”
诊断
// Monitor send completion time
let sendStart = Date()
connection.send(content: data, completion: .contentProcessed { error in
let elapsed = Date().timeIntervalSince(sendStart)
print("Send completed in \(elapsed)s") // Should be < 0.1s normally
// If > 1s, TCP congestion or receiver not draining fast enough
})
// Profile with Instruments
// Xcode → Product → Profile → Network template
// Check "Bytes Sent" vs "Time" graph
// Should be smooth line, not stepped/stalled
常见原因
- 发送方的发送速度超过接收方的处理速度(背压)
- 网络拥塞(丢包、重传)
- 未使用 contentProcessed 回调进行速率控制
- 在已失去可用性的连接上发送数据
修复方法
// ❌ WRONG — Sending without pacing
/*
for frame in videoFrames {
connection.send(content: frame, completion: .contentProcessed { _ in })
// Buffers all frames immediately → memory spike → congestion
}
*/
// ✅ CORRECT — Pace with contentProcessed callback
func sendFrameWithPacing() {
guard let nextFrame = getNextFrame() else { return }
connection.send(content: nextFrame, completion: .contentProcessed { [weak self] error in
if let error = error {
print("Send error: \(error)")
return
}
// contentProcessed = network stack consumed frame
// NOW send next frame (pacing)
self?.sendFrameWithPacing()
})
}
// Start pacing
sendFrameWithPacing()
替代方案:Async/await(iOS 26+)
// NetworkConnection with natural back pressure
func sendFrames() async throws {
for frame in videoFrames {
try await connection.send(frame)
// Suspends automatically if network can't keep up
// Built-in back pressure, no manual pacing needed
}
}
验证
- 发送 1000 条消息,监控内存使用情况(应保持稳定)
- 监控发送完成时间(应低于 100ms)
- 使用 Network Link Conditioner 进行测试(100ms 延迟,3% 丢包)
模式 5a:仅支持 IPv6 的蜂窝网络(硬编码 IPv4)
耗时 10-15 分钟
症状
- 在 WiFi(双栈 IPv4/IPv6)上运行正常
- 在蜂窝网络(仅 IPv6)上始终失败
- 在部分运营商网络上正常(T-Mobile),在其他运营商网络上失败(Verizon)
- 日志显示“Host unreachable”或 POSIX 错误 65(EHOSTUNREACH)
诊断
# Check if hostname has IPv6
dig AAAA example.com
# Check if device is on IPv6-only network
# Settings → WiFi/Cellular → (i) → IP Address
# If starts with "2001:" or "fe80:" → IPv6
# If "192.168" or "10." → IPv4
# Test with IPv6-only simulator
# Xcode → Devices → (device) → Use as Development Target
# Settings → Developer → Networking → DNS64/NAT64
常见原因
- 硬编码 IPv4 地址(“192.168.1.1”)
- getaddrinfo 仅使用 AF_INET(过滤掉 IPv6)
- 服务没有 IPv6 地址(AAAA 记录)
- 未使用 Connect by Name(手动 DNS)
修复方法
// ❌ WRONG — Hardcoded IPv4
/*
let host = "192.168.1.100" // Fails on IPv6-only cellular
*/
// ❌ WRONG — Forcing IPv4
/*
let parameters = NWParameters.tcp
parameters.requiredInterfaceType = .wifi
parameters.ipOptions.version = .v4 // Fails on IPv6-only
*/
// ✅ CORRECT — Use hostname, let framework handle IPv4/IPv6
let connection = NWConnection(
host: NWEndpoint.Host("example.com"), // Hostname, not IP
port: 443,
using: .tls
)
// Framework automatically:
// 1. Resolves both A (IPv4) and AAAA (IPv6) records
// 2. Tries IPv6 first (if available)
// 3. Falls back to IPv4 (Happy Eyeballs)
// 4. Works on any network (IPv4, IPv6, dual-stack)
验证
- 在真实设备的蜂窝网络上测试(关闭 WiFi)
- 使用多个运营商进行测试(Verizon、AT&T、T-Mobile)
- 在开发者设置中启用 DNS64/NAT64
- 运行
dig AAAA your-hostname.com,确认 IPv6 记录存在
生产环境危机情景
背景:iOS 更新导致 15% 的连接失败
情况
- 公司于周一上午发布 iOS 应用更新(v4.2)
- 到中午,客户支持部门报告“应用无法使用”工单激增
- 分析数据显示,15% 的用户遇到连接失败(超过 10,000 名用户)
- CEO 发送 Slack 消息:“出什么事了?我们多快能修好?”
- 工程经理询问预计完成时间
- 你是负责网络的工程师
压力信号
- 🚨 生产环境故障 超过 10K 名用户受影响,营收受损,App Store 负面评价不断涌入
- ⏰ 时间紧迫 “需要尽快修复,Twitter 上都在热议”
- 👔 高管关注 CEO 亲自询问进展
- 📊 公众形象 App Store 评分在 3 小时内从 4.8 降至 4.1
- 💸 财务影响 这是一个电商应用,每分钟损失 5K 美元的销售额
合理化陷阱(切勿落入)
-
“直接回滚到 v4.1”
- 看似诱人,但应用审核需要 1-2 小时,用户更新还要再等 24 小时
- 无法找到根本原因(问题可能再次发生)
- 会失去你花费数周开发的 v4.2 功能
-
“暂时禁用 TLS,缩小排查范围”
- 这会造成安全漏洞,并导致 App Store 审核被拒
- 无法解决实际问题(只是掩盖症状)
- 何时重新启用?(剧透:永远不会,因为“之后再修”往往永远不会发生)
-
“我的设备上能用,一定是用户操作有误”
- 这是傲慢,不是诊断
- 1 万名用户都遇到同一个“错误”?这不可能是用户操作有误。
-
“我们加上重试逻辑和更长的超时时间吧”
- 无法解决根本原因
- 只会让问题恶化(重试越多,故障路径承受的负载就越大)
强制诊断流程
你有 1 小时向 CEO 提供以下信息:
- 根本原因
- 修复时间线
- 缓解计划
步骤 1:确定基线(5 分钟)
// Check what changed in v4.2
git diff v4.1 v4.2 -- NetworkClient.swift
// Most likely culprits:
// - TLS configuration changed
// - Added certificate pinning
// - Changed connection parameters
// - Updated hostname
步骤 2:在生产环境中复现(10 分钟)
// Check failure pattern:
// - Random 15%? Or specific user segment?
// - Specific iOS version? (check analytics)
// - Specific network? (WiFi vs cellular)
// Enable logging on production builds (emergency flag):
#if PRODUCTION
if UserDefaults.standard.bool(forKey: "EnableNetworkLogging") {
// -NWLoggingEnabled 1
}
#endif
// Ask Customer Support to enable for affected users
// Check logs for specific error code
步骤 3:检查近期代码变更(5 分钟)
// Found in git diff:
// v4.1:
let parameters = NWParameters.tls
// v4.2:
let tlsOptions = NWProtocolTLS.Options()
tlsOptions.minimumTLSProtocolVersion = .TLSv13 // ← SMOKING GUN
let parameters = NWParameters(tls: tlsOptions)
已查明根本原因 部分用户的后端基础设施(负载均衡器、代理服务器)不支持 TLS 1.3。v4.1 协商使用 TLS 1.2,v4.2 要求使用 TLS 1.3 → 连接失败。
步骤 4:实施有针对性的修复(15 分钟)
// Fix: Support both TLS 1.2 and TLS 1.3
let tlsOptions = NWProtocolTLS.Options()
tlsOptions.minimumTLSProtocolVersion = .TLSv12 // ✅ Support older infrastructure
// TLS 1.3 will still be used where supported (automatic negotiation)
let parameters = NWParameters(tls: tlsOptions)
步骤 5:部署热修复(20 分钟)
# Build hotfix v4.2.1
# Test on affected user's network (critical!)
# Submit to App Store with expedited review request
# Explain: "Production outage affecting 15% of users"
专业沟通模板
发给 CEO(危机开始 15 分钟后)
Found root cause: v4.2 requires TLS 1.3, but 15% of users on older infrastructure
(enterprise proxies, older load balancers) that only support TLS 1.2.
Fix: Change minimum TLS version to 1.2 (backward compatible, 1.3 still used when available).
ETA: Hotfix v4.2.1 in App Store in 1 hour (expedited review).
Full rollout to users: 24 hours.
Mitigation now: Telling affected users to update immediately when available.
发给工程经理
Root cause: TLS version requirement changed in v4.2 (TLS 1.3 only).
15% of users behind infrastructure that doesn't support TLS 1.3.
Technical fix: Set tlsOptions.minimumTLSProtocolVersion = .TLSv12
This allows backward compatibility while still using TLS 1.3 where supported.
Testing: Verified fix on user's network (enterprise VPN with old proxy).
Deployment: Hotfix build in progress, ETA 30 minutes to submit.
Prevention: Add TLS compatibility testing to pre-release checklist.
发给客户支持部门
Update: We've identified the issue and have a fix deploying within 1 hour.
Affected users: Those on enterprise networks or older ISP infrastructure.
Workaround: None (network level issue).
Expected resolution: v4.2.1 will be available in App Store in 1 hour.
Ask users to update immediately.
Updates: I'll notify you every 30 minutes.
节省的时间
| 方法 | 解决所需时间 | 用户影响 |
|---|---|---|
| ❌ 恐慌式回滚 | 应用审核 1-2 小时 + 用户更新 24 小时 = 26 小时 | 10K 用户中断 26 小时 |
| ❌“增加重试次数” | 未知(无法解决根本原因) | 永久维持 15% 的失败率 |
| ❌“我这里能用” | 花几天时间排查错误的问题 | 用户感到沮丧,留下差评 |
| ✅ 系统化诊断 | 诊断 30 分钟 + 修复 20 分钟 + 审核 1 小时 = 2 小时 | 10K 用户中断 2 小时 |
经验教训
- 在不同网络上测试 不要只在 WiFi 上测试。还要在蜂窝网络、VPN 和企业网络上测试。
- 监控 TLS 兼容性 如果更改 TLS 配置,请确认后端支持该配置。
- 逐步发布 使用分阶段发布(10% → 50% → 100%),尽早发现问题。
- 紧急日志记录 准备好在生产环境中启用详细日志记录的方法,以便诊断问题。
- 沟通频率 每 30 分钟向利益相关方更新进展,即使只是说“仍在调查”。
快速参考表
| 症状 | 可能原因 | 首要检查项 | 模式 | 修复时间 |
|---|---|---|---|---|
| 卡在 .preparing | DNS 故障 | nslookup hostname | 1a | 10-15 分钟 |
| 立即进入 .waiting | 无连接 | 是否开启了飞行模式? | 1b | 5 分钟 |
| .failed POSIX 61 | 连接被拒绝 | 服务器正在监听吗? | 1c | 5-10 分钟 |
| .failed POSIX 50 | 网络断开 | 检查接口 | 1d | 5 分钟 |
| TLS 错误 -9806 | 证书无效 | openssl s_client | 2b | 15-20 分钟 |
| 未收到数据 | 帧格式问题 | 数据包捕获 | 3a | 20-30 分钟 |
| 数据不完整 | 最小/最大字节数错误 | 检查 receive() 参数 | 3b | 10 分钟 |
| 延迟不断增加 | TCP 拥塞 | contentProcessed 节流 | 4a | 15-25 分钟 |
| CPU 使用率高 | 未进行批处理 | 使用 connection.batch | 4c | 10 分钟 |
| 内存不断增长 | 连接泄漏 | 检查 [weak self] | 4d | 10-15 分钟 |
| WiFi 可用,蜂窝网络失败 | IPv6-only 网络 | dig AAAA hostname | 5a | 10-15 分钟 |
| 未使用 VPN 时可用,使用 VPN 时失败 | 代理干扰 | 测试 PAC 文件 | 5b | 20-30 分钟 |
| 端口被阻止 | 防火墙 | 尝试 443,而不是 8080 | 5c | 10 分钟 |
| HTTP URL 被静默阻止 | ATS 强制执行 | 检查 Info.plist | 6a | 5-10 分钟 |
| “发生了 SSL 错误” | ATS TLS 要求 | 检查服务器 TLS 版本 | 6b | 10-15 分钟 |
模式 6:应用传输安全(ATS)故障
耗时 5-15 分钟
默认情况下,ATS 会强制所有连接使用 HTTPS(iOS 9+)。ATS 故障不会明确提示——连接会失败并返回通用错误,控制台中不会显示 ATS 专属消息。
模式 6a:HTTP 被 ATS 阻止
症状
- URLSession 请求失败,错误为
NSURLErrorSecureConnectionFailed(-1200) 或NSURLErrorAppTransportSecurityRequiresSecureConnection(-1022) - Network.framework 连接可用,但 URLSession 不可用
- 在较旧的 iOS 版本上可用,在较新的版本上失败
- 没有明确的错误消息——只显示“连接失败”
诊断
# Check if ATS is blocking the connection
nscurl --ats-diagnostics https://yourserver.com
# Shows exactly which ATS policy the server fails
// In console, look for:
// "App Transport Security has blocked a cleartext HTTP (http://) resource load"
// This only appears if OS-level logging is enabled
修复——允许特定 HTTP 域名(首选)
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>api.legacy-server.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
不要使用 NSAllowsArbitraryLoads——这会完全禁用 ATS。App Store 审核会对此标记,应用可能因此被拒。请使用针对特定域名的例外。
模式 6b:ATS TLS 版本要求
症状
- HTTPS 连接失败并显示“SSL 错误”,尽管证书有效
- 服务器使用 TLS 1.0 或 1.1(ATS 要求 TLS 1.2+)
nscurl --ats-diagnostics显示 TLS 版本失败
诊断
# Check server's TLS version
openssl s_client -connect yourserver.com:443 -tls1_2
# If this fails but -tls1 succeeds → server doesn't support TLS 1.2
修复——升级服务器(首选)或添加例外
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>legacy-api.example.com</key>
<dict>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
</dict>
</dict>
</dict>
更好的修复方法:将服务器升级到 TLS 1.2+。针对 TLS 降级设置的 ATS 例外会受到 App Store 审核的严格审查。
ATS 与 Network.framework 的区别
ATS 适用于 URLSession 和 WKWebView 连接。Network.framework(NWConnection/NetworkConnection)不受 ATS 限制——它通过 tlsOptions 直接处理 TLS 配置。如果 URLSession 连接失败,但 NWConnection 连接到同一服务器时成功,几乎可以确定原因是 ATS。
常见错误
错误 1:调试前未启用日志记录
问题 在看不到框架内部状态的情况下尝试调试网络问题。
为什么会失败 你只能猜测发生了什么。日志会显示精确的状态转换、错误代码和时间信息。
修复方法
// Add to Xcode scheme BEFORE debugging:
// -NWLoggingEnabled 1
// -NWConnectionLoggingEnabled 1
// Or programmatically:
#if DEBUG
ProcessInfo.processInfo.environment["NW_LOGGING_ENABLED"] = "1"
#endif
错误 2:只在 WiFi 下测试
问题 WiFi 和蜂窝网络的特性不同(仅支持 IPv6、代理配置、丢包)。
为什么会失败 40% 的连接失败是由网络环境导致的。如果你只测试 WiFi,就会漏掉蜂窝网络的问题。
修复方法
- 在关闭 WiFi 的真实设备上测试
- 在多个运营商的网络上测试(Verizon、AT&T、T-Mobile 的配置各不相同)
- 在启用 VPN 的情况下测试(企业用户)
- 使用 Network Link Conditioner(Xcode → Devices)
错误 3:忽略 POSIX 错误代码
问题 看到 .failed(let error),却只向用户显示笼统的“连接失败”。
为什么会失败 不同的错误代码需要不同的修复方法。POSIX 61 = 服务端问题,POSIX 50 = 客户端网络问题。
修复方法
if case .failed(let error) = state {
let posixError = (error as NSError).code
switch posixError {
case 61: // ECONNREFUSED
print("Server not listening, check server logs")
case 50: // ENETDOWN
print("Network interface down, check WiFi/cellular")
case 60: // ETIMEDOUT
print("Connection timeout, check firewall/DNS")
default:
print("Connection failed: \(error)")
}
}
错误 4:未测试状态转换
问题 只测试正常流程(.preparing → .ready),不测试 .waiting、网络变化和失败情况。
为什么会失败 真实用户会遇到网络切换(WiFi → 蜂窝网络)、飞行模式和信号弱等情况。
修复方法
// Test with Network Link Conditioner:
// 1. 100% Loss — verify .waiting state shows "Waiting for network"
// 2. WiFi → None → WiFi — verify automatic reconnection
// 3. 3% packet loss — verify performance graceful degradation
错误 5:假设模拟器等同于设备
问题 只在模拟器中测试。模拟器使用 macOS 网络(与 iOS 不同),也没有蜂窝网络。
为什么会失败 模拟器无法暴露仅支持 IPv6 的问题,也不会模拟网络切换,而且 DNS 行为也不同。
修复方法
- 发布前务必在真实设备上测试
- 切换飞行模式进行测试(模拟网络切换)
- 仅使用蜂窝网络进行测试(关闭 WiFi)
交叉引用
预防性模式
networking skill — 用于规范行为、避免反模式:
- 危险信号:SCNetworkReachability、阻塞式套接字、硬编码 IP
- 模式 1a:使用 TLS 的 NetworkConnection(正确实现)
- 模式 2a:正确处理状态的 NWConnection
- 压力场景:如何在截止日期压力下避免走捷径
API 参考
network-framework-ref skill — 完整的 API 文档:
- NetworkConnection(iOS 26+):全部 12 个 WWDC 2025 示例
- NWConnection(iOS 12-25):完整 API 和示例
- TLV framing、Coder protocol、NetworkListener、NetworkBrowser
- 从套接字、URLSession、NWConnection 迁移的策略
相关问题
swift-concurrency skill — 使用 async/await 时:
- 模式 3:Task 闭包中的 weak self(类似的内存泄漏预防方法)
- 在连接状态更新中使用 @MainActor
- 连接失败时取消 Task
最后更新 2025-12-02 状态 基于 WWDC 2018/2025 的生产就绪诊断方案 已测试 诊断模式已通过真实生产问题验证