iOS & Android & H5 三端 JS Bridge 的编解码逻辑与源码分析
上一篇 全链路 HTTP 编解码:从客户端到服务端的数据流转 讲解 HTTP 请求中的编解码。然而在客户端开发的日常里,除了 HTTP,还有一个更高频的跨端传输场景:与 H5 的数据交换,而 JS Bridge 正是其中不可或缺的桥梁。
本篇文章会沿着交互流程这条主线,只关注“编码 → 传输 → 解码”相关的细节。iOS、Android 与 H5 分别在什么时候序列化、什么时候做字符串转义、什么时候反序列化,以及这些差异会带来哪些坑。
下面以两个经典的开源实现为例,分析底层逻辑与代码实现:
1. 交互流程中的编解码骨架
无论是哪端实现,消息本质上都是 JSON。先把消息结构说清楚:
消息结构
消息分为请求和响应两种格式:
// 请求消息:JS → Native 或 Native → JS
{
handlerName: "getUserInfo", // 要调用的处理器名称
data: { userId: "123" }, // 业务参数
callbackId: "cb_1_1703404800" // 回调标识,用于匹配响应
}
// 响应消息:处理完成后返回
{
responseId: "cb_1_1703404800", // 对应请求的 callbackId
responseData: { name: "张三" } // 处理结果
}
接收方通过是否存在 responseId 来区分请求与响应。callbackId / responseId 虽然是流程字段,但它们在编码阶段就已经写入消息体,任何一端漏写或命名不一致,都会导致解码正常却无法匹配回调。
在实现里,iOS 用 NSDictionary 填充这些字段(WebViewJavascriptBridgeBase.m#L45-L62),Android 则用 WVJBMessage 类定义字段(WVJBWebView.java#L114-L120)。
交互流程中的编解码节点
- JS 侧构造对象 → JSON.stringify(编码)。
- Native 侧拿到字符串 → NSJSONSerialization / JSONObject(解码)。
- Native 侧构造响应 → JSON 序列化,iOS 额外做 JS 字符串转义(编码)。
- JS 侧 JSON.parse → 得到对象(解码)。
下面这张序列图展示了 JS Bridge 的双向通信流程。读图时只需要把注意力放在两次 JSON 编码与两次 JSON 解码上,其余机制只是把消息送达。
2. JS → Native 的编解码
这条链路里,H5 负责编码,Native 负责解码。
H5 侧编码
iOS 与 Android 的注入脚本虽然不同,但编码动作是一致的:先把消息序列化成 JSON 字符串(iOS 见 WebViewJavascriptBridge_JS.m#L72-L76,Android 见 WebViewJavascriptBridge.js#L12-L24)。
// iOS 端注入的 JS:把消息队列序列化成 JSON 字符串
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
// Android 端注入的 JS:直接序列化单条消息
function _doSend(message, responseCallback) {
// ...
var msg = JSON.stringify(message || {});
if (window.WVJBInterface) {
WVJBInterface.notice(msg);
} else {
prompt("_wvjbxx", msg);
}
}
JSON.stringify 有几个容易踩坑的点:
undefined和函数不会被序列化,字段会直接消失。- 大整数在 JS 里是
Number,可能丢精度,跨端时建议用字符串传输。 NaN、Infinity会被序列化成null。
iOS 端解码
iOS 端通过 evaluateJavaScript 调用 _fetchQueue() 拉取队列(WKWebViewJavascriptBridge.m#L94-L101,命令字符串由 WebViewJavascriptBridgeBase.m#L155-L157 生成)。拿到 JSON 字符串后再用 NSJSONSerialization 反序列化(WebViewJavascriptBridgeBase.m#L205-L207):
// 获取 JS 端的消息队列
NSString *messageQueueString = [self _evaluateJavascript:@"WebViewJavascriptBridge._fetchQueue();"];
// 反序列化
- (NSArray *)_deserializeMessageJSON:(NSString *)messageJSON {
return [NSJSONSerialization JSONObjectWithData:
[messageJSON dataUsingEncoding:NSUTF8StringEncoding]
options:0 error:nil];
}
Android 端解码
Android 端通过 JavascriptInterface 或 prompt 拿到字符串消息(WVJBWebView.java#L371-L379,WVJBWebView.java#L626-L680)。这两条通道只是传输差异,拿到的都是 JSON 字符串,后续再交给 JSONObject 解析并分发(WVJBWebView.java#L241-L275):
// 方式一:通过 JavascriptInterface
@JavascriptInterface
public void notice(String message) {
handleMessage(message);
}
// 方式二:通过拦截 prompt
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
if (message.equals("_wvjbxx")) {
handleMessage(defaultValue);
result.confirm("");
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
3. Native → JS 的编解码
这条链路里,Native 负责编码,H5 负责解码。关键在于 Native 侧如何把对象安全地塞进 JS 代码中。
iOS 端编码
发送消息时,通过 NSJSONSerialization 将字典序列化为 JSON 字符串。序列化方法见 WebViewJavascriptBridgeBase.m#L201-L203,派发方法见 WebViewJavascriptBridgeBase.m#L178-L198:
// 序列化方法(简化)
- (NSString *)_serializeMessage:(id)message {
return [[NSString alloc] initWithData:
[NSJSONSerialization dataWithJSONObject:message options:0 error:nil]
encoding:NSUTF8StringEncoding];
}
// 派发消息到 JS
- (void)_dispatchMessage:(NSDictionary *)message {
NSString *messageJSON = [self _serializeMessage:message];
// 转义特殊字符,防止 JS 执行出错
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
NSString *js = [NSString stringWithFormat:
@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
[self _evaluateJavascript:js];
}
这里的转义非常关键,因为 JSON 字符串被包在 JavaScript 的单引号字符串里,必须避免破坏字符串边界:
| 原始字符 | 转义后 | 说明 |
|---|---|---|
\ |
\\ |
反斜杠本身需要转义 |
" |
\" |
双引号,防止破坏字符串边界 |
' |
\' |
单引号,因为外层用单引号包裹 |
\n |
\\n |
换行符 |
\r |
\\r |
回车符 |
\f |
\\f |
换页符 |
\u2028 |
\\u2028 |
行分隔符(JS 中会被解释为换行) |
\u2029 |
\\u2029 |
段落分隔符(同上) |
特别注意 \u2028 和 \u2029。它们在 JSON 中合法,但在 JS 字符串字面量中会被当作换行处理,导致语法错误。
Android 端编码
Android 端使用 JSONObject 进行序列化(序列化见 WVJBWebView.java#L278-L300,派发见 WVJBWebView.java#L235-L238):
private JSONObject message2JSONObject(WVJBMessage message) {
JSONObject jo = new JSONObject();
if (message.callbackId != null) {
jo.put("callbackId", message.callbackId);
}
if (message.data != null) {
jo.put("data", message.data);
}
// ... 其他字段
return jo;
}
private void dispatchMessage(WVJBMessage message) {
String messageJSON = message2JSONObject(message).toString();
// 执行 JS
evaluateJavascript("WebViewJavascriptBridge._handleMessageFromJava(" + messageJSON + ")");
}
由于 JSON 直接作为对象字面量拼进 JS 调用中,不需要像 iOS 一样把 JSON 放进单引号字符串,因此少了一层字符串转义。
H5 侧解码
iOS 端传进来的是字符串,因此 JS 侧需要 JSON.parse(WebViewJavascriptBridge_JS.m#L78-L113):
function _dispatchMessageFromNative(messageJSON) {
var message = JSON.parse(messageJSON);
// 处理消息...
}
Android 端如果直接传对象,JS 侧可以跳过 JSON.parse,这取决于 Native 侧是否以字符串形式传参(WVJBWebView.java#L235-L238,WebViewJavascriptBridge.js#L44-L84)。
4. 编解码语义差异与坑点
字段一致性
三端约定的字段名必须一致,包括 handlerName、data、callbackId、responseId、responseData。前缀差异只影响调试,不影响解析,但字段名一旦变了,就会出现“解码正常却找不到回调”的现象。
JSON 语义差异
| 场景 | JavaScript | iOS(NSJSONSerialization) | Android(JSONObject) |
|---|---|---|---|
| null | null |
NSNull |
JSONObject.NULL |
| undefined | undefined(不会被序列化) |
- | - |
| 布尔值 | true / false |
@YES / @NO |
true / false |
| 大整数 | 可能丢失精度 | NSNumber |
long |
特别注意 undefined。JavaScript 中对象的 undefined 值在 JSON.stringify 时会被忽略,这可能导致字段丢失。
解析兜底
任何一端的 JSON.parse 失败都会中断后续流程,建议加一层 try-catch:
function _dispatchMessageFromNative(messageJSON) {
try {
var message = JSON.parse(messageJSON);
// ...
} catch (e) {
console.error("Bridge message parse error:", e);
}
}
5. 编解码排查清单
遇到“回调不执行”“数据丢失”“中文乱码”时,按下面顺序排查最省时:
- 编码阶段:检查
JSON.stringify/NSJSONSerialization/JSONObject是否成功。 - 转义阶段:确认 iOS 端的特殊字符转义是否完整,尤其是
\u2028/\u2029。 - 解码阶段:检查
JSON.parse是否抛异常,Native 端反序列化是否失败。 - 字段阶段:核对
callbackId/responseId是否一致。
6. 总结
JS Bridge 的编码链路并不神秘,本质就是 JSON 的序列化与反序列化,差别在于传输通道与字符串转义。只要把每一次编码与解码的位置标清楚,问题通常都能快速定位。