Skip to content

桥接调用超时分层与就绪竞态 ​

回到总览:混合开发与桥接相关模块:WebView H5 JSBridge 交互与安全防护 · 双 WebView 引擎运输层抽象

一句话定义 ​

桥接调用的「超时」不是一个数——它至少包含四件不同的事:运输层就绪等待、单次 RPC 超时、用户操作 UI 超时、HTTP 读超时。必须分层建模;其中就绪等待超时仍要 resolve(绝不卡死页面),而依赖用户操作的通道(支付、选图)必须关闭 Web 侧计时,否则会把「用户正在操作」误判成失败。

代码索引 ​

对应 Lab:bridge-timeout-layering-simulation · BridgeTimeoutLayeringSimulation.java

为什么需要 ​

  • 为什么不能给桥接调用一个全局 15s 超时就完事?
    • 一句话答:这会误杀两类正常调用——用户操作类(支付/选图耗时由人决定)和弱网慢操作(多图上传);同时又会卡死另一类——桥未就绪时整个页面等待超时。四件事驱动不同行为,必须分开配。
  • 为什么 waitForBridge 超时必须仍然 resolve,而不是 reject 报错?
    • 一句话答:桥未就绪往往只是注入延迟,不代表失败;若超时 reject,H5 首屏会因「桥缺失」整体卡死。超时仍 resolve,让页面继续跑、稍后重试。
  • 为什么支付 / 选图这类通道要「关闭 Web 侧计时」?
    • 一句话答:它们的结束时机是「用户在原生界面操作完 / 关闭界面」,可能远超 15s;Web 侧强行计时到点会误判失败,正确做法是由原生界面关闭事件来结束,Web 不计时。

底层机制 ​

1. 四层超时语义分离 ​

四类超时数值上不可合并,各自驱动不同行为;其中最容易被画错的点,是「就绪等待结束」并不等于「调用失败」,以及「用户操作类调用」在拉起原生界面后,Web 侧计时器应该停表而不是继续跑:

对应 Lab:bridge-timeout-layering-simulation · BridgeTimeoutLayeringSimulation.java

java
// 四层超时语义(数值不可合并)
static final long TRANSPORT_READY_MS = 3000;        // waitForBridge:超时仍 resolve,防卡死
static final long SINGLE_RPC_MS = 15000;            // 单次 RPC 默认
static final long USER_OP_DISABLE_WEB_TIMER = -1;   // 依赖用户操作:Web 侧不计时
static final long HTTP_READ_MS = 120000;            // 上传等多图弱网慢操作

// 每通道独立超时表
static final Map<String, Long> channelTimeout = Map.of(
    "getDeviceInfo", SINGLE_RPC_MS,
    "requestPayment", USER_OP_DISABLE_WEB_TIMER,  // 等用户操作,Native 关页才结束
    "openGallery", USER_OP_DISABLE_WEB_TIMER,
    "appUploadOss", HTTP_READ_MS);

2. 就绪竞态:waitForBridge 超时仍 resolve ​

桥未就绪时,等待窗口到达即返回 resolved=true,绝不抛异常;后续调用在 transport 就绪后自然成功。

java
// 模拟 waitForBridge:到达窗口即返回 true(即使桥仍未就绪),绝不卡死页面
static boolean waitForBridge(boolean ready, long windowMs) throws InterruptedException {
    long waited = Math.min(windowMs, ready ? 0 : windowMs);
    Thread.sleep(Math.min(waited, 50)); // 演示用,压缩真实等待
    return true; // 关键:超时也返回 true(resolved),不抛、不卡
}

Android / Flutter / Web / Backend 对照 ​

维度Android / 原生Flutter / DartH5 / Web后端类比
就绪等待WebView 注入完成runJavaScript 注入完成waitForBridge连接池预热等待
单次 RPC 超时future.timeout / 协程 withTimeoutFuture.timeoutPromise 超时HTTP client 读超时
用户操作 UI原生界面关闭事件原生界面关闭事件关闭 Web 侧计时—(无此类)
重试退让指数退避指数退避阶梯 backoff重试 + jitter

常见场景 ​

1. 多图上传通道给长 HTTP 读超时 ​

appUploadOss 这类「压缩 + 上传」在弱网下可能几十秒;若套用单次 RPC 的 15s,会频繁误超时。走独立的 HTTP_READ_MS 长超时。

对应 Lab:bridge-timeout-layering-simulation · 四层超时分离

2. 支付通道关闭 Web 侧计时 ​

requestPayment 的结束取决于用户是否完成支付、何时关闭支付界面;Web 侧不计时,由原生界面关闭事件结束调用。

常见误配、事故后果与排障 ​

  • 误配:全局统一 15s 超时。
    • 后果:支付/选图被误判失败(用户正在操作)、多图上传超时。
    • 排障:按通道类型拆超时表,用户操作类关闭 Web 计时。
  • 误配:waitForBridge 超时 reject。
    • 后果:桥注入稍慢即整页卡死。
    • 排障:超时仍 resolve,配合重试。
  • 误配:固定间隔重试。
    • 后果:弱网雪崩。
    • 排障:阶梯 backoff(1s/2s/4s)+ 去重。

与相近概念对比 ​

  • 桥接超时分层 vs 单一 RPC 超时:普通网络库只关心「这次请求多久」,桥接还要关心「桥本身就绪了吗」「这是不是等人操作的调用」。
  • 就绪超时 resolve vs reject:resolve 是「容错优先」(页面不卡死),reject 是「严格优先」,桥接场景选 resolve。

对应实验 ​

Lab说明源码
bridge-timeout-layering-simulation四层超时分离仿真:就绪 resolve / 用户操作关计时 / 阶梯 backoffBridgeTimeoutLayeringSimulation.java

复习检查题 ​

  1. 为什么「桥接调用超时」必须拆成运输层就绪、单次 RPC、用户操作 UI、HTTP 读超时四层,而不能用一个全局超时?

    答:四者驱动的行为完全不同——就绪超时是为了不卡死页面(超时仍 resolve),用户操作超时是因为结束时机取决于人(必须关 Web 计时),HTTP 读超时是因为弱网慢操作需要长窗口。合并成一个全局值,要么误杀正常操作(支付/上传),要么卡死页面(桥未就绪)。

  2. 为什么 waitForBridge 超时应该 resolve 而不是 reject?

    答:桥未就绪通常只是注入延迟,不等于调用失败;若超时 reject,H5 首屏会因「桥缺失」整体卡死。超时仍 resolve,让页面继续渲染,后续调用在 transport 就绪后自然成功,配合重试即可恢复。

速记 ​

  • 超时不是一个数:运输层就绪 / 单次 RPC / 用户操作 UI / HTTP 读超时 四件事。
  • waitForBridge 超时仍 resolve,绝不卡死页面。
  • 用户操作通道(支付/选图)关闭 Web 侧计时,由原生界面关闭事件结束。
  • 重试用阶梯 backoff,别固定间隔雪崩。

站点构建时间:2026/8/24 23:43:17