26、WebKit 与原生交互:JSBridge 实现原理、消息队列与回调、同步与异步调用、安全性考量
好,我们进入第二十六讲。今天聊一个非常实战的话题——JSBridge。
说白了,JSBridge 就是一座桥。桥的一头是 JavaScript,另一头是原生代码(Java/Kotlin 或 Objective-C/Swift)。没有它,H5 页面就是个孤岛,调不了摄像头、拿不到设备信息、也做不了支付。
我在项目中见过太多人把 JSBridge 当成一个简单的“发消息”工具。其实它的设计深度,直接决定了整个混合应用的稳定性和性能。今天我们就把它拆开来看。
一、JSBridge 的核心原理
先问一个问题:为什么需要 JSBridge?
因为 WebKit 的 JS 引擎和原生代码运行在两个完全隔离的世界。JS 跑在 WebCore 的 JS 引擎里,原生代码跑在 ART 或 Dalvik 里。它们之间没有直接的内存共享,只能通过消息传递来通信。
核心思路:JSBridge 本质上是一个消息路由系统。JS 端发送一个请求,原生端接收并处理,然后返回结果。
我习惯把 JSBridge 的通信模型画成下面这张图:
这张图我建议你多看两眼。它包含了 JSBridge 最核心的两个机制:消息队列和回调映射表。
二、消息队列与回调机制
JS 调用原生,不是直接调。它先把请求扔进一个队列里,然后通知原生来取。为什么这么设计?
你想想看,JS 是单线程的。如果每次调用都阻塞等待原生返回,页面直接就卡死了。所以 JSBridge 必须用异步的方式。
我的经验:早期我做过一个方案,JS 直接通过 prompt() 或 location.href 改变来触发原生调用。这种方式虽然简单,但问题很多——消息会丢失、回调无法保证顺序。后来我全部改成了基于消息队列的方案,稳定性提升了一个档次。
来看一个典型的消息队列实现:
// JS 侧:消息队列
window.JSBridge = {
// 消息队列
_queue: [],
// 回调映射表
_callbacks: {},
// 自增 ID
_callbackId: 0,
// 调用原生方法
call: function(action, params, callback) {
var callbackId = ++this._callbackId;
// 保存回调函数
if (callback) {
this._callbacks[callbackId] = callback;
}
// 构造消息
var message = {
callbackId: callbackId,
action: action,
params: params || {}
};
// 入队
this._queue.push(message);
// 通知原生取消息
this._notifyNative();
},
// 通知原生(通过注入的 JS 接口)
_notifyNative: function() {
// 方式1:通过注入对象
if (window.WebViewJavascriptBridge) {
window.WebViewJavascriptBridge._fetchMessage();
}
// 方式2:通过 URL 拦截
// location.href = 'jsbridge://_fetch_message';
},
// 原生处理完后,回调 JS
_handleCallback: function(callbackId, data) {
var callback = this._callbacks[callbackId];
if (callback) {
callback(data);
delete this._callbacks[callbackId];
}
}
};
原生侧拿到消息后,解析 action,执行对应的原生方法,然后把结果通过回调 ID 返回给 JS。
// Android 原生侧:接收消息并处理
public class JSBridgeInterface {
private WebView mWebView;
private Handler mHandler = new Handler(Looper.getMainLooper());
// JS 调用此方法来取消息
@JavascriptInterface
public void _fetchMessage() {
// 从 JS 的消息队列中取消息
mWebView.evaluateJavascript(
"JSON.stringify(window.JSBridge._queue.shift())",
new ValueCallback<String>() {
@Override
public void onReceiveValue(String json) {
if (json == null || json.equals("null")) return;
// 解析消息
JSBridgeMessage msg = parseMessage(json);
// 处理消息(异步执行,不阻塞 UI 线程)
mHandler.post(() -> {
Object result = handleAction(msg.action, msg.params);
// 回调 JS
String callbackJs = String.format(
"window.JSBridge._handleCallback('%s', %s)",
msg.callbackId, new Gson().toJson(result)
);
mWebView.evaluateJavascript(callbackJs, null);
});
}
}
);
}
}
注意:@JavascriptInterface 注解在 Android 4.2 以上是必须的。没有这个注解,JS 无法调用原生方法。我曾经遇到过线上 bug,就是因为某个低版本手机没有加这个注解,导致整个 JSBridge 失效。
三、同步调用 vs 异步调用
JSBridge 的调用方式,分为同步和异步两种。很多人搞混了,我帮你理清楚。
| 特性 | 同步调用 | 异步调用 |
|---|---|---|
| 调用方式 | JS 阻塞等待原生返回 | JS 不阻塞,通过回调获取结果 |
| 实现原理 | 通过 prompt() 或 XMLHttpRequest 同步请求 | 消息队列 + 回调映射表 |
| 性能 | 差,会卡住 UI 线程 | 好,不阻塞页面渲染 |
| 适用场景 | 获取简单值(如设备 ID、token) | 复杂操作(如拍照、支付、网络请求) |
| 风险 | 超过 5 秒可能被系统 ANR | 回调地狱,需要 Promise 封装 |
我个人习惯:能用异步就别用同步。同步调用虽然写起来简单,但代价太大了。你想想看,一个同步调用如果原生处理慢了,整个 WebView 都卡住,用户直接骂娘。
不过有一种情况我会用同步——获取一些轻量级的、必须立即返回的值。比如获取设备唯一标识:
// 同步获取设备 ID(通过 prompt 实现)
function getDeviceIdSync() {
// prompt 会阻塞 JS 线程,直到原生返回
var result = prompt('jsbridge://getDeviceId');
return result;
}
// 异步获取设备 ID(推荐方式)
function getDeviceIdAsync() {
return new Promise(function(resolve) {
window.JSBridge.call('getDeviceId', {}, function(data) {
resolve(data);
});
});
}
避坑指南:我曾经在一个金融 App 里用了同步调用获取用户信息,结果因为原生侧做了网络请求,导致页面卡死 3 秒多。后来改成异步 + 加载动画,体验好了很多。记住:任何可能耗时的操作,都不要用同步。
四、安全性考量
JSBridge 的安全问题,是很多开发者容易忽略的。我见过太多 App 因为 JSBridge 被攻击,导致用户数据泄露。
主要的安全风险有这几个:
- 接口暴露过多:JS 可以调用所有 @JavascriptInterface 方法,如果暴露了敏感接口,恶意网页就能直接调用
- 参数注入:JS 传入的参数可能包含恶意代码,原生侧没有做校验就直接执行
- URL 欺骗:通过修改 location.href 来伪造 JSBridge 调用
- 回调劫持:恶意 JS 可以监听或篡改回调数据
我总结了一套安全实践,分享给你:
// 安全实践示例
// 1. 白名单机制:只允许特定域名调用
public class SafeJSBridge {
private static final List<String> ALLOWED_ORIGINS = Arrays.asList(
"https://app.example.com",
"https://m.example.com"
);
@JavascriptInterface
public void sensitiveOperation(String param) {
// 检查来源 URL
String url = mWebView.getUrl();
if (!isAllowedOrigin(url)) {
Log.w("JSBridge", "Blocked call from: " + url);
return;
}
// 2. 参数校验:不要信任任何输入
if (!isValidParam(param)) {
sendError("Invalid parameter");
return;
}
// 3. 执行操作
doSensitiveOperation(param);
}
private boolean isAllowedOrigin(String url) {
if (url == null) return false;
for (String origin : ALLOWED_ORIGINS) {
if (url.startsWith(origin)) return true;
}
return false;
}
}
// 4. 回调数据加密(敏感数据)
function secureCallback(callbackId, data) {
// 对敏感数据进行签名
var signature = hmacSHA256(data, SECRET_KEY);
window.JSBridge._handleCallback(callbackId, {
data: data,
signature: signature
});
}
我曾经踩过的坑:有一个版本,我把文件读写接口暴露给了 JS。结果某个合作方的 H5 页面被注入了恶意脚本,直接通过 JSBridge 读取了用户的相册。从那以后,我所有的 JSBridge 接口都加了域名白名单和参数校验。这个教训,花了我两个通宵去修复。
另外,还有几个安全建议:
- 最小权限原则:只暴露必要的接口,不要图方便把所有原生能力都开放
- 参数类型校验:JS 传入的参数,原生侧一定要做类型和范围校验
- 回调超时处理:设置回调超时时间,防止回调永远不触发导致内存泄漏
- 日志审计:记录所有 JSBridge 调用日志,方便排查问题
好了,这一讲的内容就到这里。JSBridge 看起来简单,但要做好、做稳、做安全,需要花不少心思。希望今天的内容对你有帮助。
核心要点回顾:
- JSBridge 是 JS 和原生之间的消息路由系统
- 消息队列 + 回调映射表是异步调用的核心
- 同步调用慎用,只适合轻量级、必须立即返回的场景
- 安全是第一位的:白名单、参数校验、最小权限
公众号:蓝海资料掘金营,微信deep3321