移动 App 网络请求最佳实践:超时、重连与弱网环境优化
在移动端开发中,网络请求的稳定性直接影响用户体验。相比服务端,移动网络具有高延迟、易中断、带宽波动大等特点。本文从超时策略、自动重连机制和弱网环境适配三个维度,梳理一套可落地的优化方案。
一、超时策略:分级设置,避免一刀切
1. 区分场景设定超时时间
不要对所有接口使用同一个超时值。推荐按业务重要性分级:
场景 | 连接超时 | 读取超时 | 说明 |
|---|---|---|---|
关键数据(登录、支付) | 10s | 15s | 需容忍短暂网络抖动 |
常规列表/详情 | 5s | 8s | 快速失败,避免卡顿 |
上传/下载大文件 | 30s | 60s+ | 根据文件大小动态调整 |
后台预加载 | 3s | 5s | 非关键任务,快速放弃 |
2. 实现示例(OkHttp + Kotlin)
val client = OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS) .build() // 对特定请求单独设置 val request = Request.Builder() .url(url) .header("X-Timeout", "fast") .build() val call = client.newCall(request).apply { // 通过拦截器动态调整超时 }3. 渐进式超时策略
对于重要接口,可采用“先短后长”的策略:
第1次尝试:连接5s,读取8s → 失败 第2次尝试:连接8s,读取12s → 失败 第3次尝试:连接12s,读取20s → 成功(降级体验)注意:每次重试间隔应递增(如 500ms → 2s → 5s),避免雪崩效应。
二、自动重连机制:有状态、有限度
1. 幂等性检查
只有幂等请求才能自动重试:
GET / HEAD / OPTIONS → 安全
PUT / DELETE → 需确认后端是否幂等
POST → 默认不重试,除非业务层保证
2. 指数退避算法
fun retryDelay(attempt: Int): Long { val baseDelay = 1000L // 1秒 val maxDelay = 30000L // 30秒上限 return minOf(baseDelay * (1 shl attempt), maxDelay) } // 使用示例 repeat(maxRetries) { attempt -> try { val response = executeRequest() if (response.isSuccessful) break } catch (e: IOException) { if (attempt == maxRetries - 1) throw e delay(retryDelay(attempt)) } }3. 智能触发条件
不是所有错误都值得重试:
错误类型 | 是否重试 | 原因 |
|---|---|---|
SocketTimeoutException | ✅ | 可能瞬间网络波动 |
UnknownHostException | ❌ | DNS 问题,重试无效 |
HTTP 503 Service Unavailable | ✅ | 服务暂时过载 |
HTTP 401 Unauthorized | ❌ | 需要重新认证 |
HTTP 429 Too Many Requests | ⏸️ 等待后重试 | 限流,需等待 Retry-After |
4. 连接池复用
保持 TCP 连接存活,减少三次握手开销:
val connectionPool = ConnectionPool( maxIdleConnections = 5, keepAliveDuration = 5, TimeUnit.MINUTES )三、弱网环境优化:让 App 更“耐抗”
1. 网络状态感知
实时监听网络变化,动态调整请求行为:
class NetworkMonitor(private val context: Context) { fun getCurrentQuality(): NetworkQuality { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val network = cm.activeNetwork ?: return NetworkQuality.NONE val caps = cm.getNetworkCapabilities(network) ?: return NetworkQuality.UNKNOWN return when { caps.linkDownstreamBandwidthKbps < 150 -> NetworkQuality.POOR caps.linkDownstreamBandwidthKbps < 2000 -> NetworkQuality.MODERATE else -> NetworkQuality.GOOD } } } enum class NetworkQuality { NONE, POOR, MODERATE, GOOD, UNKNOWN }2. 差异化请求策略
根据网络质量调整参数:
fun buildRequest(url: String, quality: NetworkQuality): Request { val timeoutMultiplier = when (quality) { NetworkQuality.POOR -> 3.0 // 弱网下超时延长3倍 NetworkQuality.MODERATE -> 1.5 else -> 1.0 } // 同时可以调整压缩、图片质量等 return Request.Builder() .url(url) .header("Accept-Encoding", if (quality == NetworkQuality.POOR) "gzip" else "identity") .build() }3. 离线优先架构
核心思路:先展示缓存,再发起网络请求。
用户操作 → 读取本地缓存 → 显示UI → 异步刷新 → 更新缓存对于列表页,推荐使用 Room + Paging 3 实现:
@Dao interface ArticleDao { @Query("SELECT * FROM articles ORDER BY updated_at DESC") fun getArticles(): PagingSource<Int, Article> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(articles: List<Article>) } // Repository 层 class ArticleRepository(private val api: Api, private val dao: ArticleDao) { fun getArticles(): Flow<PagingData<Article>> { return Pager(PagingConfig(pageSize = 20)) { ArticlePagingSource(api, dao) }.flow } }4. 请求合并与去抖
高频场景下(如搜索输入、滚动加载),使用 debounce 合并请求:
searchQueryFlow .debounce(300) // 300ms内无新输入才发送 .distinctUntilChanged() .flatMapLatest { query -> api.search(query) } .catch { /* 处理错误 */ } .collect { results -> updateUI(results) }5. 图片加载优化
图片往往是弱网下的最大瓶颈:
// Glide 配置示例 Glide.with(context) .load(url) .override(400, 400) // 限制分辨率 .format(DecodeFormat.PREFER_RGB_565) // 减少内存占用 .diskCacheStrategy(DiskCacheStrategy.DATA) // 缓存原始数据 .onlyRetrieveFromCache(true) // 弱网下只读缓存 .into(imageView) // 结合网络质量动态调整 if (networkQuality == NetworkQuality.POOR) { imageUrl += "?quality=30&width=200" }6. 请求优先级队列
为不同请求分配优先级,确保关键请求优先执行:
enum class Priority { HIGH, MEDIUM, LOW } class PriorityQueueDispatcher { private val queues = mapOf( Priority.HIGH to PriorityBlockingQueue<Request>(), Priority.MEDIUM to PriorityBlockingQueue<Request>(), Priority.LOW to PriorityBlockingQueue<Request>() ) fun enqueue(request: Request, priority: Priority) { queues[priority]?.put(request) processNext() } private fun processNext() { for (p in Priority.values()) { queues[p]?.poll()?.let { execute(it); return } } } }四、监控与告警:让问题可见
1. 埋点关键指标
DNS 解析耗时
TCP 连接耗时
TLS 握手耗时
首字节时间(TTFB)
总传输时间
重试次数
失败原因分布
2. 日志上报策略
{ "event": "network_request", "properties": { "url": "/api/v1/articles", "method": "GET", "status_code": 200, "duration_ms": 2340, "retry_count": 2, "network_type": "WIFI", "signal_strength": -65, "error_code": null } }3. 自动化测试
使用 Charles / Wireshark 模拟弱网:
# macOS 上使用 Network Link Conditioner # 配置:丢包率 5%,带宽 100KB/s,延迟 500msCI/CD 中加入弱网测试用例:
@Test fun testRequestUnderPoorNetwork() { // 模拟弱网环境 val mockInterceptor = MockInterceptor().apply { setDelay(2000) // 2秒延迟 setFailureRate(0.3) // 30%失败率 } val client = OkHttpClient.Builder() .addInterceptor(mockInterceptor) .build() // 验证重试逻辑 val response = client.newCall(request).execute() assertTrue(response.isSuccessful || retryCount <= 3) }总结
移动端网络优化的核心原则:
分层处理:超时、重试、缓存各司其职,互不干扰
感知环境:根据网络质量动态调整策略,而非固定配置
容错设计:假设网络会失败,优雅降级而非崩溃
数据驱动:用真实监控数据指导优化方向
最后记住:永远不要在 UI 线程做网络请求,这是所有优化的基础前提。