一、概述与选型
1.0 Hybrid 框架原理
Hybrid(混合开发)指在 Native App 里用 WebView 加载 H5 页面,通过 JSBridge 在 Native 与 H5 之间双向调用的开发方式。原理可概括为:
- Native 壳:Activity/Fragment 提供容器,内嵌 WebView;负责生命周期、权限、系统能力(相机、定位、文件等)。
- H5 页面:在 WebView 中运行,负责部分 UI 与业务逻辑;可远程加载(URL)或本地打包(assets)。
- JSBridge:连接两端。H5 通过约定方式(如
addJavascriptInterface、URL Scheme、prompt等)调用 Native;Native 通过evaluateJavascript/loadUrl("javascript:...")或注入回调调用 H5。 - 约定与安全:双方约定全局对象/方法名、参数与返回格式(如 JSON)、调用时机;Native 侧做白名单、参数校验与脱敏,防止恶意页面滥用能力。
整体上:Native 提供“壳 + 能力”,H5 提供“页面 + 逻辑”,Bridge 负责“互通 + 约束”。下文所述均为该框架下 Android 与 JS 的通信实现方式。
1.1 方式对比(JS → Android)
| 方式 | 返回值 | 兼容性 | 安全性 | 实现难度 | 参数/数据 | 典型场景 | 推荐度 |
|---|---|---|---|---|---|---|---|
| addJavascriptInterface | ✅ 有返回值;方法 return 的值 JS 同步拿到 | Android 4.2+ 且方法需加 @JavascriptInterface | 4.2 前存在反射注入漏洞;4.2+ 仅暴露注解方法较安全 | 低,绑定对象即可 | 支持多参数、复杂类型(会序列化);可传 JSON 字符串 | 常规 Native 能力调用(Toast、设备信息、跳转等) | ⭐⭐⭐⭐⭐ |
| shouldOverrideUrlLoading(URL Scheme) | ❌ 无;需通过回调或再调 JS 回传 | 全版本 | 不暴露对象,仅解析 URL,相对安全;需防 Scheme 被劫持 | 中,需解析 URL、约定协议 | 仅 URL 内 query 参数,长度和特殊字符受限 | 简单指令、无需返回值的场景;兼容老设备 | ⭐⭐⭐⭐ |
| onJsPrompt | ✅ 通过 JsPromptResult.confirm(value) 回传 | 全版本 | 不暴露对象,仅拦截 prompt;需约定协议避免与真实 prompt 冲突 | 中,需在 WebChromeClient 中解析协议 | message 为字符串,可拼 JSON;defaultValue 可作辅助 | 需要同步返回值的桥接(如获取设备信息、配置) | ⭐⭐⭐⭐⭐ |
| onJsAlert | ❌ 无 | 全版本 | 同上,仅拦截 alert | 低 | 单条 message 字符串 | 不推荐做主通道;会弹窗,体验差 | ⭐⭐ |
| onJsConfirm | ✅ 仅用户点击确定/取消,通过 result.confirm()/cancel() | 全版本 | 同上 | 低 | 单条 message | 需用户确认的操作;有弹窗 | ⭐⭐ |
| onConsoleMessage | ❌ 无 | 全版本 | 控制台可被篡改,不可信 | 低 | 仅 console 输出内容 | 调试、日志上报,不作为业务桥接 | ⭐ |
| postMessage(WebMessagePort) | ✅ 双向,port 可反复收发 | Android 6.0 (API 23)+ | 官方推荐,无反射、无 URL 劫持;需校验 message 来源 | 高,需建 Channel、传 port、维护回调 | 任意可序列化数据,支持 MessagePort 传递 | 新项目、目标 6.0+、需要双向长连接通信 | ⭐⭐⭐⭐ |
补充说明:
-
返回值
有返回值的(如addJavascriptInterface、onJsPrompt)在 JS 里同步拿到;URL Scheme、alert/console 等需 Native 再调evaluateJavascript或注入回调才能把结果回给 JS。 -
安全性
4.2 前addJavascriptInterface会暴露未注解方法,存在风险;URL Scheme 需校验协议与来源,避免被其他应用抢调。 -
选型建议
优先addJavascriptInterface(4.2+)或onJsPrompt(兼容老版本且需返回值);强安全且可接受 6.0+ 时用postMessage;URL Scheme 适合简单、无返回值或兼容老机。
JS 调 Android 选型示意:
- 需同步返回值?
- 是 → 最低支持 4.2?是则用 addJavascriptInterface,否则用 onJsPrompt。
- 否 → 进入下一步。
- 强调安全且最低 6.0+?
- 是 → postMessage。
- 否 → 简单指令/兼容老机?是则 URL Scheme,否则用 addJavascriptInterface。
1.2 方式对比(Android → JS)
| 方式 | 返回值 | 兼容性 | 实现难度 | 参数/数据 | 典型场景 | 推荐度 |
|---|---|---|---|---|---|---|
| evaluateJavascript | ✅ 通过 ValueCallback<String> 异步回传 | Android 4.4 (API 19)+ | 低,直接执行 JS 字符串 | 任意可拼进 JS 的字符串;建议用 JSON 传参避免转义 | 调页面方法、取数据、触发前端逻辑并拿结果 | ⭐⭐⭐⭐⭐ |
| loadUrl("javascript:...") | ❌ 无 | 全版本 | 低 | 同上,需注意引号与特殊字符转义 | 4.4 以下兼容;仅需“触发”不关心返回时 | ⭐⭐⭐ |
| 回调机制(传回调名 + JS 里调 Native) | ✅ 通过 JS 调 addJavascriptInterface 暴露的方法回传 | 全版本(配合上述二者之一) | 中,需维护 callbackId 与 ValueCallback 映射 | 由 JS 决定回传内容(常为 JSON 字符串) | 4.4 以下需要“返回值”时;异步结果回传 | ⭐⭐⭐⭐ |
注入 JS 后由 Native 调(如注入 window._nativeCb) | ✅ 若注入的是可被 Native 调用的函数名则等价于回调 | 全版本 | 中,需在 onPageFinished 等时机注入并约定接口 | 由注入的 JS 接口约定 | 页面加载前就建立好桥、或需统一入口时 | ⭐⭐⭐ |
补充说明:
-
返回值
evaluateJavascript的ValueCallback收到的是 JS 执行结果的字符串形式(JSON 会多一层引号和转义,需在 Native 侧解析);loadUrl无法拿到返回值。 -
线程
两种方式都必须在主线程调用;在子线程需先runOnUiThread或Handler(Looper.getMainLooper())。 -
选型建议
4.4+ 优先用evaluateJavascript;4.4 以下用loadUrl,若需返回值再配合「回调机制」(JS 里调用 Native 暴露的onJSCallback(callbackId, result))。 -
回调机制 vs 注入后 Native 调(详见 3.3、3.4)
-
回调机制(传回调名 + JS 里调 Native)
- 方向:Native → 要结果。
- 流程:Native 先调 JS(如
loadUrl("javascript: getData('cb_1')"))并传入 callbackId → 页面getData执行完后,在 JS 里调Android.onJSCallback('cb_1', result)回传 → Native 用Map<callbackId, ValueCallback>绑定,在 onJSCallback 里回调。
-
注入 JS 后由 Native 调(如 window._nativeCb)
- 方向:Native → 主动推数据给页面。
- 流程:Native 在 onPageFinished 等时机注入
window._nativeCb = function(id, data) { ... }→ 需要推数据时evaluateJavascript("window._nativeCb('id','data')")调该函数。 - 区别:前者是 Native 调 JS 业务方法,等 JS 再调 onJSCallback 回传;后者是 Native 直接调事先挂好的函数把数据推给 JS,无需 JS 再调 Native。
-
版本与 API 的详细对照见第九章。
二、JS 调用 Android
2.1 addJavascriptInterface(最常用)
要点:将 Java/Kotlin 对象暴露给 WebView,JS 直接调对象上的方法。Android 4.2+ 必须对暴露方法加 @JavascriptInterface。
调用流程:
sequenceDiagram
participant H5 as H5 页面
participant WebView as WebView
participant Native as Native/Android
Note over Native: addJavascriptInterface(obj, "Android")
H5->>WebView: Android.showToast('msg')
WebView->>Native: 反射调用 obj.showToast("msg")
Native->>Native: 执行方法(如 Toast)
Note over H5,WebView: 若有返回值,直接回到 JS
H5->>WebView: var x = Android.getDeviceInfo()
WebView->>Native: 调用 getDeviceInfo()
Native-->>WebView: 返回 "Pixel 6"
WebView-->>H5: x = "Pixel 6"
Android:
public class JSInterface {
private Context context;
private WebView webView; // 用于异步回传时调 JS
public JSInterface(Context context, WebView webView) {
this.context = context;
this.webView = webView;
}
// 同步:直接 return,JS 立即拿到返回值
@JavascriptInterface
public void showToast(String message) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}
@JavascriptInterface
public String getDeviceInfo() {
return Build.MODEL;
}
// 异步方法无“直接返回”:方法立即 return void,结果稍后才出来,只能再用 evaluateJavascript 把结果推回 JS
@JavascriptInterface
public void getDeviceInfoAsync(final String callbackId) {
new Thread(() -> {
String result = Build.MODEL; // 模拟耗时
String script = "window.__nativeCallback && window.__nativeCallback('"
+ callbackId + "','" + result.replace("'", "\\'") + "');";
webView.post(() -> webView.evaluateJavascript(script, null));
// 若需拿到这次调 JS 的返回值,可传 ValueCallback:evaluateJavascript(script, value -> { ... });
}).start();
}
}
// 绑定
WebView webView = findViewById(R.id.webview);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JSInterface(this, webView), "Android");
JS:
// 同步调用:直接拿返回值
Android.showToast('Hello from JavaScript');
var deviceInfo = Android.getDeviceInfo();
// 异步回调:传 callbackId,在 __nativeCallback 里收结果
window.__nativeCallback = function(callbackId, result) {
if (callbackId === 'cb_device') console.log('Device:', result);
};
Android.getDeviceInfoAsync('cb_device');
说明:
- 同步:方法直接
return,JS 侧var x = Android.getDeviceInfo()立即得到结果。 - 可回调 / 异步回传:JS 传
callbackId,Native 在子线程等做完后,用evaluateJavascript调页面里的window.__nativeCallback(callbackId, result),把结果传回 JS。 - “直接返回”和“用 evaluateJavascript 回调”不冲突:
直接返回只适用于同步方法:方法里做完事情后return value,JS 用var x = Android.getDeviceInfo()立刻拿到。
getDeviceInfoAsync 是异步方法:一进来就new Thread(...).start()然后方法马上return(void),此时结果还没有,不可能用return把“稍后的结果”交给 JS。所以必须等异步完成后,用evaluateJavascript再调一次 JS,把结果当参数传进去。总结:同步 = 直接 return 有返回值;异步 = 没有“过一会儿再 return”的机制,只能 Native 再调 JS 把结果传回去。 - ValueCallback:是
evaluateJavascript(script, value -> { ... })的第二个参数,用于在 Native 调 JS 时接收这次执行 JS 的返回值(字符串)。上面异步回传若不需要拿“回调函数执行后的返回值”,可传null;若要拿,就传ValueCallback<String>在回调里解析。
2.2 URL Scheme(shouldOverrideUrlLoading)
要点:通过自定义协议(如 myapp://)拦截请求,实现 JS→Android;无直接返回值,需通过回调或后续 evaluateJavascript 回传。
URL Scheme 调用流程:
sequenceDiagram
participant H5 as H5
participant WebView as WebView
participant Native as Native
H5->>H5: 创建隐藏 iframe,src=myapp://action?param=xx
H5->>WebView: 请求 myapp://...
WebView->>Native: shouldOverrideUrlLoading 拦截
Native->>Native: 解析 host、query,执行业务逻辑
Native-->>WebView: return true(不继续加载该 URL)
Note over H5: 无返回值;需结果时 Native 再 evaluateJavascript 回调
Android 端:在 WebViewClient 中拦截 myapp://,解析 host 与 query 并处理;必须同时重写 shouldOverrideUrlLoading(WebView, String url) 与 shouldOverrideUrlLoading(WebView, WebResourceRequest)(5.0+ 会走后者),否则部分跳转拦截不到。示例:
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return handleScheme(url);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return handleScheme(request.getUrl().toString());
}
return false;
}
private boolean handleScheme(String url) {
if (url != null && url.startsWith("myapp://")) {
Uri uri = Uri.parse(url);
String action = uri.getHost();
String param = uri.getQueryParameter("param");
// 根据 action 执行业务,如 showToast(param)
return true; // 表示已处理,不继续加载该 URL
}
return false;
}
});
JS: 用隐藏 iframe 触发,避免当前页跳转:
function callAndroid(action, param) {
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = "myapp://" + action + "?param=" + encodeURIComponent(param);
document.body.appendChild(iframe);
setTimeout(() => document.body.removeChild(iframe), 100);
}
2.3 通过 WebChromeClient 拦截(prompt / alert / confirm / console)
onJsPrompt、onJsAlert、onJsConfirm、onConsoleMessage 均为 WebChromeClient 的回调方法,分别用于拦截页面内 JS 的 prompt()、alert()、confirm() 以及控制台输出。用约定前缀(如 jsbridge://)区分为桥接后由 Native 处理。实现方式:在对应回调里判断 message 是否带约定前缀,若是则处理并 return true,并按需调用 result.confirm(value) 等,避免系统再弹默认框。
四种方式对比:
| 方式 | 拦截的 JS API | 返回值 | 是否弹窗 | 适用场景 | 推荐作桥接 |
|---|---|---|---|---|---|
| onJsPrompt | prompt(msg) | ✅ 通过 result.confirm(value) 同步回传 | 拦截后可不弹 | 需同步拿 Native 结果的桥接(如 getDeviceInfo、配置) | ⭐ 推荐 |
| onJsAlert | alert(msg) | ❌ 无 | 会弹框,除非拦截后 return true | 仅把消息传给 Native,不做主通道 | ❌ |
| onJsConfirm | confirm(msg) | ✅ 用户点确定/取消,通过 result.confirm()/cancel() | 会弹确认框 | 需用户二次确认的操作(如删除前确认) | ❌ |
| onConsoleMessage | console.log/error 等 | ❌ 无 | 无弹窗 | 调试、日志上报、错误采集(控制台不可信) | ❌ |
2.3.1 onJsPrompt(推荐作桥接,下面为示例与流程)
调用流程:
sequenceDiagram
participant H5 as H5
participant WebView as WebView
participant Native as Native
H5->>WebView: prompt("jsbridge://getDeviceInfo")
WebView->>Native: onJsPrompt(message, result)
Native->>Native: 解析 message,若为 jsbridge:// 则处理
Native->>Native: 执行业务,得到返回值
Native->>WebView: result.confirm(返回值)
WebView-->>H5: prompt 返回该值,JS 可同步拿到
Android:
webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsPrompt(WebView view, String url, String message,
String defaultValue, JsPromptResult result) {
if (message != null && message.startsWith("jsbridge://")) {
Uri uri = Uri.parse(message);
String action = uri.getHost();
if ("getDeviceInfo".equals(action)) {
result.confirm(Build.MODEL);
} else if ("getVersion".equals(action)) {
result.confirm(Build.VERSION.RELEASE);
} else {
result.confirm("unknown");
}
return true;
}
return false;
}
});
JS:
function callNative(action, params) {
var url = "jsbridge://" + action;
if (params) url += "?" + new URLSearchParams(params).toString();
return prompt(url);
}
var device = callNative("getDeviceInfo");
2.3.2 onJsAlert / onJsConfirm / onConsoleMessage
三者用法见上表;在 WebChromeClient 中分别实现 onJsAlert、onJsConfirm、onConsoleMessage 回调,若作桥接则同样判断 message 是否带约定前缀,若是则处理并 return true 并 result.confirm() 等。一般不作为主桥接通道。
2.4 postMessage(Android 6.0+,官方推荐)
要点:使用系统提供的 WebMessagePort 做双向信道,不依赖 addJavascriptInterface 反射,也不走 URL,安全性较好;需 Android 6.0 (API 23)+。
流程步骤(通俗理解):
可以理解为:Native 和 H5 共用一对“传话筒”(Channel),一头在 Native(port1),一头交给 H5(port2),两边对着传话筒喊话就能互通。
- Native 先准备好传话筒:
createWebMessageChannel()得到两个 port,一个自己拿着(port1),一个准备交给页面(port2)。 - Native 在自己这头挂好“接听”:在 port1 上
setWebMessageCallback,以后 H5 从 port2 发来的消息都会进这个回调;要回复就调port1.postMessage(...)。 - Native 把另一头交给 H5:
postWebMessage("init", [port2], targetOrigin)把 port2 和一条 "init" 消息发给页面,并限制只发给指定域名(targetOrigin,如你的 H5 域名)。 - H5 收传话筒:页面里
window.addEventListener("message", ...),收到event.data === "init"且event.ports[0]就是 port2;可顺带校验event.origin防篡改。 - H5 用 port2 收发:给 port2 设
onmessage收 Native 发来的内容,要发给 Native 就port2.postMessage(...)。 - 之后双向随便发:H5 发 → Native 的 callback 收到;Native 发 → H5 的
onmessage收到。同一对 port 一直用,不用重复建。
简单示例:
Android 端(API 23+;需先加载完页面再 postMessage,否则页面收不到;回调若更新 UI 需 post 到主线程):
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
WebMessagePort[] ports = webView.createWebMessageChannel();
WebMessagePort portToJs = ports[1];
ports[0].setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
@Override
public void onMessage(WebMessagePort port, WebMessage message) {
String data = message.getData();
Log.d("Bridge", "收到 H5: " + data);
port.postMessage(new WebMessage("Native 已收到: " + data));
}
});
webView.postWebMessage(
new WebMessage("init", new WebMessagePort[]{ portToJs }),
Uri.parse("https://yourdomain.com")
);
}
JS 端(页面内):
window.addEventListener("message", function(event) {
if (event.data === "init" && event.ports && event.ports.length > 0) {
var port = event.ports[0];
port.onmessage = function(e) {
console.log("收到 Native:", e.data);
};
port.postMessage("Hello from H5");
}
});
实现相对复杂,适合新项目且最低支持 6.0 时使用。
三、Android 调用 JS
共性注意:所有“Android 调 JS”都必须在主线程执行;且应在 onPageFinished 之后再调,否则页面未就绪可能无效或报错。4.4 以下用 loadUrl("javascript:..."),无法拿返回值,需配合回调机制(见 3.3)。
Android 调 JS 总体流程:
┌─────────────────┐
│ Native 要调 JS │
└────────┬────────┘
▼
┌─────────────────┐
│ 页面已加载完? │
└────────┬────────┘
否 │ 是
┌─────┘ └─────┐
▼ ▼
┌───────────────┐ ┌─────────────────┐
│ 等待 │ │ API 版本 ≥ 4.4? │
│ onPageFinished│ └────────┬────────┘
│ 后再调 │ 是 │ 否
└───────────────┘ ┌────┘ └────┐
▼ ▼
┌───────────────┐ ┌──────────────┐
│evaluateJavascript│ │ loadUrl │
│"javascript:fn()"│ │"javascript:.."│
└───────┬───────┘ └──────┬───────┘
▼ ▼
┌───────────────┐ ┌──────────────┐
│ 需要返回值? │ │ 无返回值; │
└───────┬───────┘ │ 需则用 3.3 回调│
是 │ 否 └──────────────┘
┌─────┘ └─────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ ValueCallback│ │ 传 null 即可 │
│ 异步拿字符串 │ └─────────────┘
└─────────────┘
3.1 evaluateJavascript(推荐,4.4+)
要点:在主线程调用,页面加载完成后执行;可通过 ValueCallback<String> 异步拿到返回值(返回的是 JSON 字符串,可能带引号与转义,需在 Native 侧解析)。
// 无返回值
webView.evaluateJavascript("javascript:showMessage('Hello')", null);
// 有返回值
webView.evaluateJavascript("javascript:getData()", value -> {
if (value != null && !"null".equals(value)) {
// value 可能是 "\"json string\"" 或 原始 JSON,需去引号/转义后解析
Log.d("WebView", "Result: " + value);
}
});
// 传参建议用 JSON,避免拼接字符串与转义问题
String params = new JSONObject().put("name", "John").put("age", 30).toString();
webView.evaluateJavascript("javascript:handleData(" + params + ")", null);
返回值解析要点:若为字符串,会多一层引号和 \ 转义,需先 substring 去首尾引号并 replace("\\\"", "\"") 再解析。
JS 端需提供对应方法,例如:
function showMessage(msg) { console.log(msg); }
function getData() { return JSON.stringify({ name: 'John', age: 30 }); }
function handleData(params) { console.log('Received:', params); return true; }
3.2 loadUrl(兼容 4.4 以下)
要点:通过加载 javascript: 协议 URL 执行 JS,全版本可用;无法获取返回值。传参时注意引号与特殊字符转义,建议用 JSON 序列化后拼进字符串,或让 JS 从全局变量/约定好的对象上读参数。
webView.loadUrl("javascript:showMessage('Hello')");
// 传参示例(注意转义)
String params = new JSONObject().put("key", "value").toString();
webView.loadUrl("javascript:handleData(" + params + ")");
仅需“触发”不关心返回时可用;若需返回值,需配合 3.3 回调机制。
3.3 通过回调机制拿“返回值”
思路:**Native 要调 JS 并拿到“返回值”**时,loadUrl 无返回值;4.4 以下也没有 evaluateJavascript,只能用“回调”兜底:
- Native 调 JS 时把 callbackId 传进去(如
getData(null, function(r){ Android.onJSCallback('cb_1', r); })); - 页面里的方法执行完后,在 JS 里调 Native 暴露的
Android.onJSCallback(callbackId, result),把结果回传; - Native 用
Map<callbackId, ValueCallback>把这次“调 JS”和对应的 ValueCallback 绑在一起,在onJSCallback里根据 callbackId 找到并回调。
这样在 4.4 以下用loadUrl也能间接拿到“返回值”。
Android 端示例(需将包含 onJSCallback 的对象通过 addJavascriptInterface 绑定到 WebView,例如命名为 Android):
private int callbackId = 0;
private final Map<String, ValueCallback<String>> callbacks = new HashMap<>();
public void callJS(String method, String params, ValueCallback<String> callback) {
String id = "cb_" + (callbackId++);
callbacks.put(id, callback);
String js = "javascript:" + method + "(" + params + ", function(r){ Android.onJSCallback('" + id + "', r); })";
webView.loadUrl(js); // 或 evaluateJavascript(js, null)
}
@JavascriptInterface
public void onJSCallback(String callbackId, String result) {
ValueCallback<String> cb = callbacks.remove(callbackId);
if (cb != null) cb.onReceiveValue(result);
}
JS 端示例(Native 拼进 getData(params, callback) 时,params 是直接嵌入的 JS 表达式:传 "null" 则首参为 null,传 "{}" 则首参为对象 {},传 "\"str\"" 则为字符串;若 Native 传的是 JSON 字符串且未嵌入为表达式,则 JS 端需 JSON.parse(param)):
function getData(param, callback) {
var data = (param === null || param === undefined) ? null : (typeof param === 'string' ? JSON.parse(param) : param);
setTimeout(function() {
callback(JSON.stringify({ result: 'ok', data: data }));
}, 500);
}
3.4 JS 注入原理与使用
一、定义与本质
- 注入:由 Native 在运行时向当前 WebView 页面放入一段 JavaScript,这段代码不是页面自带的,而是 Native“塞进去”的,在页面所在的 JS 环境中执行。
- 本质:注入的 JS 里会定义方法、对象或内容(如
window.Android、window.onerror);之后通过调用这些方法就能控制或配合 H5 页面。H5 的用法必须和注入里约定的接口对应(方法名、参数格式、调用时机一致),否则会报错或不起作用。
二、原理
-
同一执行环境
WebView 当前页面对应一个 JS 引擎、一个window、一份 DOM。Native 通过evaluateJavascript("javascript:...")或loadUrl("javascript:...")把字符串交给这个引擎执行,因此注入的代码与页面里的<script>共享同一全局环境,可读写window、改 DOM、挂对象或覆盖全局函数,后续页面逻辑都会受影响。 -
注入 vs 不注入(含“直接调页面已有方法”)
- 不注入:不往页面里执行“额外的新代码”。
- JS 调 Native:用
addJavascriptInterface绑定对象后,页面里直接写Android.xxx()即可,页面本来就有能力调 Native,不需要再塞一段脚本。 - Native 调 JS:页面里已经定义了
getData等函数,Native 直接evaluateJavascript("getData()", callback)调用已有方法,没有在页面里新增任何代码。
- JS 调 Native:用
- 注入:Native 主动往页面里执行一段“新”的 JS 字符串,这段代码不是页面自带的,执行后会在
window上新增或改写内容(如window.Android、window.onerror、window._nativeCb)。- 用途:页面原本没有这些逻辑,通过注入“塞进去”,从而在不改 H5 源码的前提下扩展或改变页面行为(如提前挂 Bridge、错误上报、注入配置)。
- 一句话:不注入 = 只“用”页面里已有的(对象/方法);注入 = 先“塞”一段新代码进页面,再“用”这段代码产生的结果。
- 不注入:不往页面里执行“额外的新代码”。
-
执行时机
- 加载顺序:
onPageStarted→ 拉取并解析 HTML、执行页面<script>→onPageFinished。 - onPageFinished 之后注入:在页面脚本和 DOM 都就绪后执行,可安全访问 DOM、覆盖或增强全局(如
onerror)。 - onPageStarted 之后、页面 script 未执行前注入:注入代码早于页面内
<script>,可提前挂window.Android等,但 DOM 可能未就绪,仅适合挂全局对象。
- 加载顺序:
JS 注入与页面加载时机关系:
- 页面加载顺序:
onPageStarted→ 拉取 HTML → 解析 HTML → 执行页面 script →onPageFinished。 - 注入时机 1(早于页面 script):在
onPageStarted之后,Native 执行loadUrl("javascript:..."),注入代码先执行(如挂window.Android等),早于页面内<script>。 - 注入时机 2(晚于页面):在
onPageFinished之后,Native 执行evaluateJavascript,注入代码可访问 DOM、覆盖onerror等。
页面加载与注入时机流程图:
时间线 ───────────────────────────────────────────────────────────────►
onPageStarted 拉取/解析 HTML 执行页面 script onPageFinished
│ │ │ │
▼ │ │ ▼
┌─────────────┐ │ │ ┌─────────────┐
│ 注入时机 1 │ │ │ │ 注入时机 2 │
│ loadUrl │ │ │ │evaluateJS │
│ (javascript:..) │ │ │ 可访问 DOM、 │
│ 挂 window. │ │ │ │ 覆盖 onerror │
│ Android 等 │─────────┼──────────────────┼────────►│ │
└─────────────┘ │ │ └─────────────┘
▼ ▼
┌──────────┐ ┌──────────┐
│ 解析 HTML │ ────► │ 页面 │
└──────────┘ │ script │
└──────────┘
三、注入与“不注入”的关系
- 不注入也可以完成互相调用,注入只是可选手段。
- JS 调 Android:Native 用
addJavascriptInterface绑定对象后,页面自带的 JS 即可Android.xxx(),无需注入。注入多用于“提前挂 Bridge”或给不能改源码的 H5 打补丁。 - Android 调 JS:页面里已有全局函数时,Native 直接
evaluateJavascript("getData()", callback)即可,无需先注入。注入多用于“提前挂对象”或往页面里加本来没有的逻辑(如错误上报、配置)。
是否需要注入流程图:
┌─ JS 调 Android:页面要调 Native
│ └─ addJavascriptInterface 已绑定? ─ 是 → 直接 Android.xxx(),不需注入
│ └─ 需“早于页面 script”挂 Bridge / 不能改 H5 源码? ─ 是 → 用注入
│
└─ Android 调 JS:Native 要调页面
└─ 页面已有全局函数? ─ 是 → evaluateJavascript("fn()") 即可,不需注入
└─ 要加“页面没有”的逻辑(错误上报、配置、polyfill)? ─ 是 → 用注入
四、实现方式(Native 如何注入)
| 方式 | 时机 | 特点 |
|---|---|---|
evaluateJavascript("javascript:...") | 多在 onPageFinished 之后 | 直接执行字符串,可拿返回值;4.4+ 推荐 |
loadUrl("javascript:...") | 同上,或更早 | 全版本可用,无法拿返回值 |
在 HTML 中插入 <script> | Native 能改写 HTML 再加载时(如 loadDataWithBaseURL) | 脚本随页面解析,可控制执行顺序;需在加载前拿到 HTML |
注入方式选择流程图:
┌─────────────────────┐
│ 需要注入 JS 到页面 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ 能先拿到 HTML 再加载?│
└──────────┬──────────┘
是 │ 否
┌─────┘ └─────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 在 HTML 中插入 │ │ 是否需早于 │
│ <script> 再加载 │ │ 页面 script? │
│ (loadDataWithBaseURL 等) │ └────────┬────────┘
└─────────────────┘ 是 │ 否
┌─────┘ └─────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ loadUrl │ │evaluateJavascript│
│(javascript:..)│ │ (4.4+ 推荐) │
│ 全版本、更早 │ └──────────────┘
└──────────────┘
五、典型用途
- Bridge 初始化:提前挂
window.Android或 Bridge 脚本,保证 H5 一加载就能调 Native。 - 注入配置 / token:如
window.__CONFIG__ = { ... },供页面 JS 读取。 - 错误上报:注入
window.onerror或 Promise 未捕获处理,在回调里调 Native 上报。 - 补丁 / Polyfill:为老旧页面注入兼容或安全脚本。
六、H5 与 Native 两端配合
- H5 侧不需要“注入”。注入是 Native 往 WebView 里塞 JS;H5 做的是在页面里写好业务逻辑(调
Android.xxx()、在window上挂供 Native 调用的函数),与 Native 约定好接口和时机 即可。
配合方式概览:
| 方式 | Native | H5 | 说明 |
|---|---|---|---|
| 只靠 addJavascriptInterface | 绑定对象(如 Android),不注入 | 直接写 Android.xxx(),挂 window.getData 等供 Native 调 | 约定对象名、方法名、参数格式即可 |
| Native 再注入 Bridge | 除绑定外,在 onPageFinished(或更早)注入脚本,如挂 __BRIDGE_READY__ | 若执行早于 Native 挂对象,则轮询或监听自定义事件(如 AndroidReady)后再调 | 适合 H5 script 很早执行、或需统一 Bridge 封装的场景 |
| 用 JSBridge 库 | 按库文档注册 handler、调 H5 | 等 WebViewJavascriptBridgeReady 等就绪后 register/call | 协议由库统一约定 |
“注入 window._nativeCb 后由 Native 调”的典型用法:Native 在 onPageFinished 等时机注入 window._nativeCb = function(id, data) { ... };之后需要把数据推给页面时,就 evaluateJavascript("window._nativeCb('id','...')")。这是 Native 主动调页面里已挂好的函数,和 3.3 的“Native 调 JS → JS 再调 onJSCallback 回传”是反过来的方向。
约定要点:
- ① 全局对象/方法名:如
window.Android、window.getData。 - ② 参数与返回格式:如约定用 JSON 传参、返回。
- ③ 时机:H5 何时调、Native 何时挂对象;若 H5 先执行须“等”再调。
- ④ 降级:非 App 内无
Android时的处理(如判断再调、提示在 App 内打开)。
七、注意事项
- 时机:依赖 DOM 的注入须在 onPageFinished 之后;“先于页面脚本”可用
loadUrl("javascript:...")在 onPageStarted 之后(仅适合挂全局对象)。 - 编码与转义:注入内容中的
\、'、"、换行等需正确转义;复杂数据建议 JSON 序列化后注入。 - 安全:只注入可信内容,勿把用户输入或不可信数据拼进脚本,防止 XSS。
本章补充说明(Android → JS,适用于 3.1~3.4 所有方式)
- 调用时机:务必在
onPageFinished之后调用,否则页面可能尚未就绪;若 H5 有异步脚本,可再postDelayed几百毫秒。 - 线程:所有 WebView 相关调用必须在主线程;子线程中需先
runOnUiThread或Handler(Looper.getMainLooper()).post(...)再执行。 - 返回值:
evaluateJavascript的ValueCallback收到的是 JS 执行结果的字符串形式,若 JS 返回 JSON 会多一层引号与转义,需在 Native 侧去引号、反转义后再解析;loadUrl无返回值,只能靠 3.3 回调机制间接拿结果。 - 4.4 以下:用
loadUrl执行 JS,若需“返回值”须配合 3.3 的回调(JS 里调 Native 暴露的onJSCallback);传参时注意转义与 JSON 序列化。 - 特殊字符:尽量用 JSON 传参,避免手拼字符串导致语法错误(详见第六章 6.4)。
四、JSBridge 开源方案
不想手写 Bridge、或希望与 iOS 共用一套 H5 桥接协议时,可选用现成库。本节对比各库的维护情况、优缺点与适用场景,便于选型。
4.1 总览对比
| 库 | 维护情况 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| JsBridge (lzyzsd) | 原仓库更新较少,有社区 fork 在维护 | 接入简单、API 少、双向+持久回调、文档多 | 原库久未更新、需自行修 bug;仅 Android、无命名空间 | 快速接入、不追求新特性、可接受 fork |
| DSBridge | 活跃度较高,有 iOS/Android/Web 多端 | 同步/异步、进度回调、命名空间、支持 X5 分支、跨端接口统一 | 依赖稍重、API 略多、需替换为 DWebView | 新项目、多端统一、需同步调用或进度回调 |
| SafeWebViewBridge / safe-java-js-webview-bridge | 不同作者、项目规模不一,需按具体仓库查 | 针对安全设计、减少反射暴露、可限制调用方 | 生态小于上述两者、文档与示例相对少 | 对安全要求高、需弱化 addJavascriptInterface 暴露 |
| WebViewJavascriptBridge 风格 | 多为仿 iOS 实现,各项目独立 | 与 iOS 端接口一致、H5 一套代码双端复用 | 多为自研或小众库、无统一标准实现 | iOS/Android 共用同一套 H5 桥接协议时 |
| X5 WebView Bridge | 随腾讯 X5 内核更新 | 与系统 WebView API 类似、兼容 X5 能力 | 依赖 X5、包体积与合规需考虑 | 已使用腾讯 X5 内核的项目 |
4.2 JsBridge (lzyzsd)
- 依赖:
com.github.lzyzsd:jsbridge:1.0.4(JitPack) - 维护:原仓库 lzyzsd/JsBridge 近年更新较少,社区有 fork(如 hjhrq1991/JsBridge、happydog-intj/JsBridge)在做优化与修复,选型时可考虑用维护中的 fork 或自建分支。
- 优点:接入快、API 少(
registerHandler/callHandler);双向通信、回调可多次使用;网上教程多、与微信 JSBridge 思路接近。 - 缺点:原库长期未发版,新系统/新 API 兼容需自己跟进;仅 Android 端,无官方 iOS 配套;无命名空间,多模块时需自己约定 handler 命名。
- 示例:
Android:
// 使用 BridgeWebView 替代系统 WebView
webView.registerHandler("submitFromWeb", (data, function) -> function.onCallBack("..."));
// Native 调 JS
webView.callHandler("functionInJs", "data", result -> { /* 处理 result */ });
JS:
// 监听桥接就绪后使用
document.addEventListener("WebViewJavascriptBridgeReady", function() {
bridge.registerHandler("xxx", function(data, responseCallback) { ... });
bridge.callHandler("submitFromWeb", data, function(response) { ... });
});
4.3 DSBridge (wendux)
- 依赖:
com.github.wendux:DSBridge-Android:3.0.0;X5 内核用x5-3.0分支。 - 维护:仓库 wendux/DSBridge-Android 星数较多,有 iOS/Web 端配套,近年仍有更新,社区反馈相对多。
- 优点:支持同步与异步调用;支持进度回调(一次调用多次返回);命名空间(如
nativeApi.xxx)便于多模块管理;提供 X5 分支;三端(Android/iOS/Web)接口风格统一,便于 H5 一套代码。 - 缺点:需使用
DWebView替代系统WebView;API 比 JsBridge 多,上手略慢;依赖体积相对大。 - 示例:
Android:
dWebView.addJavascriptObject(new JsApi(), "nativeApi"); // 异步方法用 CompletionHandler<T>
// Native 调 JS
dWebView.callHandler("test.method", new Object[]{"hello"}, retValue -> { /* 处理 retValue */ });
JS:
// 同步调 Native
dsBridge.call("nativeApi.testSyn", "test");
// 异步调 Native
dsBridge.call("nativeApi.testAsyn", "test", function(val) { ... });
// 注册供 Native 调用的方法
dsBridge.register("test.method", function(arg, callback) { callback("..."); });
4.4 SafeWebViewBridge / 安全向 Bridge
- 代表实现:如 mayu0924/SafeWebViewBridge、pedant/safe-java-js-webview-bridge 等,侧重减少
addJavascriptInterface的暴露面或改用更可控的通信方式。 - 维护:多为个人或小团队项目,需到具体仓库看 Issues/Commits 判断是否仍在维护。
- 优点:从设计上考虑安全(如限制可调方法、校验来源、减少反射);部分支持“把 JS 函数传到 Java 再回调”,避免全局暴露过多。
- 缺点:生态和文档不如 JsBridge/DSBridge;不同库实现差异大,需按仓库文档单独集成和评估。
- 适用:对 WebView 桥接安全有明确要求、愿意为安全做额外集成的项目。
4.5 WebViewJavascriptBridge 风格(对齐 iOS)
- 含义:接口设计对齐 iOS 的 WebViewJavascriptBridge,便于 H5 写一套
bridge.callHandler/registerHandler双端复用。 - 维护:Android 侧多为“仿写”或独立实现,没有唯一官方库,是否维护取决于具体选用的项目。
- 优点:与 iOS 端协议一致,前端只需一套桥接代码;概念清晰(注册 handler、按 name 调用)。
- 缺点:Android 实现分散,质量与维护情况不一;需自行核对与 iOS 的协议兼容性。
- 适用:已有 iOS WebViewJavascriptBridge、希望 Android 行为与接口一致的项目。
4.6 X5 WebView Bridge
- 含义:在腾讯 X5 内核的
WebView上做桥接,API 与系统addJavascriptInterface等类似,但运行在 X5 环境。 - 维护:随腾讯 X5 SDK 更新,需关注 X5 官方发布与兼容性说明。
- 优点:与 X5 内核能力(内核版本、兼容性、调试等)一致;若已用 X5,接入成本低。
- 缺点:强依赖 X5,包体与合规需单独评估;部分设备需拉取内核,有失败与降级策略问题。
- 适用:已确定使用腾讯 X5 内核的 App。
4.7 选型小结
- 只做 Android、求简单:JsBridge 或其一维护中的 fork。
- 新项目、要同步调用/进度回调/多端统一:优先考虑 DSBridge。
- 安全优先:在 SafeWebViewBridge、safe-java-js-webview-bridge 等中按仓库活跃度与文档选一个。
- 与 iOS 共用一套 H5 桥接:WebViewJavascriptBridge 风格或 DSBridge(DSBridge 本身也支持多端)。
- 已用 X5:在 X5 上做 Bridge 或采用 DSBridge 的 X5 分支。
JSBridge 库选型流程图:
┌─────────────────────┐
│ 要选 JSBridge 库 │
└──────────┬──────────┘
▼
已用 X5? ─ 是 → X5 Bridge 或 DSBridge X5 分支
│
否 → 要与 iOS 共用一套 H5 桥接? ─ 是 → WebViewJavascriptBridge 风格或 DSBridge
│
否 → 只做 Android、求简单? ─ 是 → JsBridge 或维护中的 fork
│
否 → 新项目、要同步/进度回调/多端统一? ─ 是 → DSBridge
│
否 → 安全优先? ─ 是 → SafeWebViewBridge / safe-java-js-webview-bridge 等
五、安全与最佳实践
上线前、或接入不可信 H5 时,建议通读本节并落实配置与校验;使用 addJavascriptInterface、URL Scheme、JS 注入时尤其要注意参数与来源校验。
5.1 安全风险与对策
| 风险 | 成因简述 | 对策 |
|---|---|---|
| addJavascriptInterface 被滥用 | 4.2 前未注解方法可被 JS 反射调用;恶意页面可执行任意暴露方法 | 4.2+ 仅暴露带 @JavascriptInterface 的方法;校验参数类型与长度;不暴露敏感操作 |
| URL Scheme 被劫持 | 自定义协议可被其他 App 注册,拦截后伪造参数 | 校验 URL 来源(referrer/白名单域名);Scheme 使用唯一前缀;关键操作加签名或 token |
| XSS / 脚本注入 | 页面或参数未转义,被注入脚本窃取数据、调 Native | 设置 CSP;对来自前端的展示内容做 HTML 转义;Native 只调白名单内的 JS 方法名 |
| 中间人 / 篡改 | HTTP 或错误处理 SSL 时被劫持 | 全站 HTTPS;生产环境不随意 handler.proceed(),应校验证书或提示用户 |
| 敏感数据泄露 | 通过 Bridge 或控制台暴露 token、用户信息 | Bridge 不返回敏感原始数据;日志脱敏;Release 关闭调试接口 |
5.2 WebView 安全配置示例
WebSettings s = webView.getSettings();
s.setJavaScriptEnabled(true);
s.setAllowFileAccess(false);
s.setAllowContentAccess(false);
s.setAllowFileAccessFromFileURLs(false);
s.setAllowUniversalAccessFromFileURLs(false);
s.setJavaScriptCanOpenWindowsAutomatically(false); // 禁止 JS 自动弹窗打开新窗口,按需开启
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
s.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); // HTTPS 页不加载 HTTP 资源
}
// 生产环境:SSL 错误应校验证书或提示,勿一律 proceed(BuildConfig.DEBUG 为编译时常量,无则改为根据是否为调试构建判断)
webView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
if (BuildConfig.DEBUG) handler.proceed(); else handler.cancel();
}
});
5.3 接口层建议
- 参数校验:对 JS 传入的长度、类型、必填项做校验;用于页面展示的内容做 HTML 转义(
<→<,"→"等),避免 XSS。 - 权限控制:用白名单限制可调用的方法名;敏感操作(如支付、跳转)可要求先登录或二次校验。
- 错误与异常:Bridge 内统一 try-catch,打日志并上报类型/方法名/参数摘要,不把裸异常或栈信息回传给 JS。
- 版本兼容:4.4+ 用
evaluateJavascript,以下用loadUrl;4.2+ 必须给暴露方法加@JavascriptInterface。
5.4 参数校验与转义示例
以下适用于 H5 统一调用一个入口(如 Android.callNative(method, paramsJson))、由 Native 按 method 分发的场景:
private static final Set<String> ALLOWED = new HashSet<>(Arrays.asList("showToast", "getDeviceInfo"));
@JavascriptInterface
public void callNative(String method, String paramsJson) {
if (!ALLOWED.contains(method) || (paramsJson != null && paramsJson.length() > 10240)) return;
try {
JSONObject json = paramsJson != null ? new JSONObject(paramsJson) : new JSONObject();
// 业务处理;对要展示的字符串做 escapeHtml 后再用
} catch (JSONException e) { Log.w(TAG, "Invalid params", e); }
}
static String escapeHtml(String raw) {
if (raw == null) return "";
return raw.replace("&", "&").replace("<", "<")
.replace(">", ">").replace("\"", """).replace("'", "'");
}
5.5 安全实践小结
- 仅加载可信域名,必要时用
shouldOverrideUrlLoading做 URL 白名单。 - 不向 WebView 注入未校验的 HTML/JS;若必须注入,对内容做转义或严格 CSP。
- 定期更新 WebView 系统组件与依赖库,关注安全公告。
- 发布前关闭调试、移除测试用 Bridge 方法;若业务允许可关闭不必要的
setJavaScriptEnabled。
六、常见问题与坑
6.1 WebView 内存泄漏
现象:Activity 退出后仍无法被 GC 回收,反复进入含 WebView 的页面易 OOM;LeakCanary 等工具会报 WebView 或 Activity 泄漏。
原因:WebView 会持有创建时传入的 Context(多为 Activity),内部还有 Chromium 渲染进程、内核线程等与 Context 绑定。若不在页面销毁时主动解绑并释放,这些引用会一直存在,导致 Activity 无法回收。
处理:
- 在合适的生命周期里释放:在 Activity 的
onDestroy(或 Fragment 的onDestroyView)中按顺序执行以下步骤,最后将 WebView 引用置为null:
释放步骤流程图:
onDestroy / onDestroyView
│
▼
loadDataWithBaseURL(清空页) → clearHistory → clearCache(true) → onPause
│
▼
getParent() 为 ViewGroup? ─ 是 → removeView(webView)
│ 否 → 跳过 removeView
└────────────────────────────┘
▼
destroy() → webView = null
(若 getParent() 为 null 或非 ViewGroup,则跳过 removeView,直接执行 destroy。)
// 1. 清空页面,避免后续操作再触发 JS 或请求
webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
webView.clearHistory();
webView.clearCache(true);
webView.onPause();
// 2. 从父布局移除并销毁(若 getParent() 为 null,可只执行 destroy)
if (webView.getParent() instanceof ViewGroup) {
((ViewGroup) webView.getParent()).removeView(webView);
}
webView.destroy();
webView = null;
- 尽量用 ApplicationContext 创建 WebView:若不需要 Activity 主题、弹窗、文件选择等,用
getApplicationContext()可避免 WebView 持有 Activity。 - 必须用 Activity Context 时:在
addJavascriptInterface绑定的 Bridge 对象里对 Context 使用WeakReference,回调时先get()判空再使用,防止 Activity 已销毁仍被引用。 - 单 Activity 多 Fragment:Fragment 销毁时务必把其持有的 WebView 从父布局
removeView并destroy(),避免被其他 Fragment 或 Activity 间接持有导致无法回收。
6.2 页面未加载完就调 JS
现象:evaluateJavascript 或 loadUrl("javascript:...") 不生效、无回调、或报错。
原因:DOM 或脚本尚未就绪,全局对象或方法尚未挂载。
处理:
- 必须在
WebViewClient.onPageFinished之后再调 JS;若页面内有异步脚本,可再postDelayed300~500ms,或让 H5 在就绪后通过 Bridge 通知 Native“页面已就绪”。 - 避免在
onPageStarted或loadUrl调用刚返回时(此时页面尚未加载完)立即执行 JS。
6.3 evaluateJavascript 返回值格式
现象:ValueCallback 拿到的字符串带首尾双引号和 \" 等转义,直接 new JSONObject(value) 解析失败。
原因:JS 返回字符串时,WebView 会再包一层 JSON 字符串序列化(即多一层引号和转义)。
处理:先按“一层字符串”还原:若首尾是 ",则 substring 去掉并 replace("\\\"", "\""),再解析 JSON。建议封装成统一工具(如 parseJSResult(value)),见附录。
6.4 特殊字符与参数传递
现象:拼进 javascript:fn('...') 的字符串里含引号、换行、反斜杠时,导致 JS 语法错误或截断。
处理:优先用 JSON 传参(new JSONObject().put(...).toString() 拼进 javascript:fn(${params})),由 JS 端 JSON.parse;若必须手拼字符串,需对 \、'、"、\n、\r、\t 等做转义。
6.5 线程
现象:在子线程调用 evaluateJavascript、loadUrl、addJavascriptInterface 等导致崩溃或未定义行为。
原因:WebView 相关 API 必须在主线程调用。
处理:在子线程中通过 runOnUiThread、Handler(Looper.getMainLooper()).post(...) 或 View 的 post(...) 切回主线程再调 WebView。
6.6 缓存导致页面不更新
现象:改了 H5 或接口数据,但 WebView 里仍显示旧内容。
原因:HTTP 缓存、WebView 缓存或 LocalStorage 等未失效。
处理:开发阶段可将 cacheMode 设为 LOAD_NO_CACHE;需要时主动 clearCache(true)(可异步)、clearHistory(),并清理 Cookie(CookieManager.getInstance().removeAllCookies(null) + flush())。若仍异常,检查服务端 Cache-Control 或考虑给 URL 加版本号/时间戳。
6.7 shouldOverrideUrlLoading 需重写两个方法
现象:在 Android 5.0+ 上点击链接或 JS 触发跳转时,自定义 Scheme 或逻辑未生效。
原因:5.0 起增加了 shouldOverrideUrlLoading(WebView, WebResourceRequest),若只重写带 String url 的旧方法,新 API 不会走你的逻辑。
处理:两个重载都实现,在新方法里用 request.getUrl().toString() 判断并处理,再 return true/false。
6.8 addJavascriptInterface 名称冲突
现象:页面已有全局变量 Android 或与 Native 绑定的名称相同,导致 Native 绑定的对象被覆盖或命名冲突报错。
处理:使用唯一命名(如 MyAppAndroid、YourApp_Native),并在 H5 约定好同一名称;或先检测 typeof window.Android,再决定是否挂到备用名。
6.9 异步回调时 Activity 已销毁
现象:JS 异步操作完成后调 Native 回调,此时 Activity 已 finish,导致崩溃或无效操作。
处理:Bridge 里对 Context/Activity 使用 WeakReference,在回调中先 get() 判空再执行;或通过 Lifecycle 判断 isDestroyed(),若已销毁则不再回调。
6.10 URL Scheme 被其他应用拦截
现象:自定义 myapp:// 被其他 App 注册,点击链接会调起别的应用。
处理:校验 URL 的 host、path 和来源;关键操作加服务端签名或 token;尽量使用 HTTPS + 自定义 path 或 query,减少对自定义 Scheme 的依赖。
6.11 Fragment 中 WebView 生命周期
现象:Fragment 在 onDestroyView 后仍持有 WebView,或 Tab 切换时多个 WebView 同时存在导致卡顿/泄漏。
处理:在 onDestroyView 里将 WebView 从父布局移除并 destroy()、引用置空;后续若需调用 JS,先判断 Fragment isAdded() 且 WebView 非空再执行。
6.12 混合内容与 HTTPS
现象:HTTPS 页面加载 HTTP 图片或脚本被拦截,页面显示不全或报错。
原因:默认混合内容策略会禁止 HTTPS 页加载 HTTP 资源。
处理:服务端将资源改为 HTTPS;或临时在调试时设置 setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE)(生产不建议)。
七、性能与缓存
7.1 通信与执行性能
| 手段 | 说明 | 注意点 |
|---|---|---|
| 批量执行 JS | 短时间内多次调用先压入队列,合并成一条 evaluateJavascript("stmt1;stmt2;...") 再执行,减少主线程与内核往返次数 | 单条语句不宜过长;有顺序依赖的需保证顺序 |
| WebView 复用 | 多页/多 Tab 场景用“WebView 池”:obtain 取实例、recycle 归还前清空 URL 与历史,避免反复 new 与 destroy | 池大小不宜过大;回收时务必 loadDataWithBaseURL 清空并 clearHistory |
| 硬件加速 | webView.setLayerType(View.LAYER_TYPE_HARDWARE, null) 可提升渲染性能 | 部分机型兼容问题,按需开启;调试时可先关闭定位渲染问题 |
| 减少 Bridge 调用频率 | 前端合并多次调用为一次,或 Native 侧做节流/防抖后再调 JS | 避免在滚动、输入等高频回调里直接调 Bridge |
批量执行示例思路:维护一个 List<String> 待执行 JS 队列,定时(如 100ms)或达到一定条数后拼接为 stmt1;stmt2;... 一次 evaluateJavascript,然后清空队列。
7.2 缓存模式(CacheMode)
| 模式 | 行为 | 适用场景 |
|---|---|---|
| LOAD_DEFAULT | 按 HTTP 响应头(Cache-Control、Expires)决定是否用缓存、是否发条件请求 | 大多数页面,平衡实时与性能 |
| LOAD_CACHE_ELSE_NETWORK | 有缓存即用(含过期),无缓存再请求网络 | 离线优先、内容更新不频繁 |
| LOAD_NO_CACHE | 忽略缓存,每次走网络 | 强实时(如行情、聊天)、开发调试 |
| LOAD_CACHE_ONLY | 只用缓存,不发起网络请求 | 完全离线、预置包场景 |
- 缓存目录一般在
/data/data/包名/cache/下,由系统按响应头管理;过期后会发 If-None-Match / If-Modified-Since,304 则继续用缓存并更新元数据。 - 需要强制更新时:
clearCache(true)、clearHistory(),必要时清理 Cookie;或对请求 URL 加时间戳/版本号。
缓存模式选择(按场景):
┌─────────────────────┐
│ 选缓存模式 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ 需要强实时? │
│ (行情/聊天) │
└──────────┬──────────┘
是 │ 否
│ └──────► 完全离线/预置?
│ 是→LOAD_CACHE_ONLY
▼ 否→ 离线优先/更新不频繁?
LOAD_NO_CACHE 是→LOAD_CACHE_ELSE_NETWORK
否→LOAD_DEFAULT(按响应头)
7.2.1 离线缓存原理
一、WebView 内置 HTTP 缓存原理
- WebView 使用系统网络库的 HTTP 缓存:首次请求某 URL 时走网络,根据响应头(如
Cache-Control、Expires、ETag)决定是否将响应体写入本地缓存目录,并记录过期时间、ETag 等元数据。 - 再次请求同一 URL 时:先查本地是否有缓存;若有且未过期,直接使用缓存(不请求网络);若已过期,则带
If-None-Match/If-Modified-Since发条件请求,服务器返回 304 时继续用本地缓存并更新过期时间,返回 200 时用新内容并更新缓存。 - 因此“离线”能力依赖服务端正确设置缓存头;若服务端不设或设了
no-store,内置缓存无法持久化,需要自定义离线方案。
内置 HTTP 缓存流程:
- 请求 URL → 本地有缓存?
- 否 → 请求网络 → 根据响应头决定是否写入缓存 → 返回内容给 WebView。
- 是 → 缓存未过期?
- 是 → 直接使用缓存,不请求网络。
- 否 → 带 If-None-Match/If-Modified-Since 发条件请求 → 服务器返回 304 则继续用本地缓存并更新过期时间,返回 200 则用新内容更新缓存并返回。
二、自定义离线缓存原理
- 通过
WebViewClient.shouldInterceptRequest在每次资源请求发出前被调用,可拦截请求并返回自定义的WebResourceResponse,从而“劫持”响应来源。 - 流程:请求 URL →
shouldInterceptRequest被调用 → Native 先查本地存储(文件或 DB,以 URL 或 hash 为 key)→ 若命中且未过期,用本地数据构造WebResourceResponse(指定 MIME、编码、InputStream)并 return → WebView 直接使用该响应,不发起网络请求;若未命中或已过期,return null → WebView 按默认逻辑走网络(或先走网络再在onLoadResource等时机把响应写入本地,供下次使用)。 - 存储时机:可在“首次请求返回后”把响应体写入本地(如异步下载并落盘),并在索引中记录 URL、路径、时间戳、TTL;下次同一 URL 请求时在
shouldInterceptRequest中读本地并返回。 - 淘汰与更新:用 TTL 判断过期;用 LRU 或总大小上限做淘汰;需要“先显示缓存再更新”时,可先返回缓存,再在后台请求新内容并覆盖本地。
自定义离线缓存流程:
- WebView 发起资源请求 → shouldInterceptRequest 被调用。
- 查本地缓存:
- 命中且未过期 → 用本地数据构造 WebResourceResponse → return 给 WebView,不请求网络。
- 未命中或已过期 → return null → WebView 走默认逻辑(请求网络);可选:请求成功后异步写入本地,供下次使用。
7.3 缓存清理与 DOM 存储
- 何时只清缓存即可:开发调试时页面不更新、或用户主动点“清除缓存”时,用系统自带清理即可:
webView.clearCache(true)(可异步)、webView.clearHistory();Cookie 用CookieManager.getInstance().removeAllCookies(null)后flush()。无需自己做离线缓存时,不必实现 7.4 的自定义逻辑。 - LocalStorage / SessionStorage:由 WebView 按域名存储,与 HTTP 缓存分开。清除需在 JS 里
localStorage.clear()或通过evaluateJavascript执行;卸载应用或清除应用数据会一并清除。 - 获取缓存占用:可遍历
context.getCacheDir()下与 WebView 相关目录计算大小,用于设置页“清除缓存”的展示或提示。
7.4 自定义离线缓存实现思路
何时需要自定义:当服务端未设缓存头或设了 no-store、或需要自己控制过期时间与淘汰策略(如按 LRU、按容量上限)时,仅靠 7.2 的 CacheMode 不够,需在 7.2.1 的“自定义离线缓存原理”基础上实现。
实现时需解决存储结构(URL→文件路径或 DB 索引)与淘汰策略(TTL、LRU、总大小上限),用 WebViewClient.shouldInterceptRequest 拦截请求(原理与流程图见 7.2.1):
- 读缓存:先查本地(文件或 DB,以 URL 或 hash 为 key),若命中且未过期则构造
WebResourceResponse返回InputStream。 - 未命中:返回
null,交给 WebView 走默认网络;若需“先返缓存再更新”,可在返回缓存的同时异步拉新内容并写入缓存。 - 淘汰策略:TTL 过期删除;按大小或条数限制时可用 LRU(按访问时间排序,删最久未用)。
- 实现注意:拦截与 IO 尽量在子线程或异步,避免主线程卡顿;大内容放文件,索引放 DB 或 JSON。
7.5 预加载
原理:在用户真正打开某页面或资源之前,用隐藏的 WebView 或后台请求提前加载并缓存(内存或磁盘),等用户点击时可直接用已加载的实例或本地缓存,从而缩短首屏时间。
| 策略 | 做法 | 注意 |
|---|---|---|
| 预加载下一页 | 当前页 onPageFinished 后,用隐藏 WebView 或池中实例对“下一页”URL 执行 loadUrl | 避免同时预加载过多,占用内存与网络 |
| 空闲时预加载 | 应用启动或回到前台后延迟几秒,对常用 URL 做预加载 | 不阻塞首屏;可结合 WebView 池 |
| 预加载静态资源 | 对关键 JS/CSS 提前请求并落盘,在自定义 shouldInterceptRequest 中优先读本地 | 需维护 URL→本地路径映射与过期策略 |
预加载可缩短用户点击后的首屏时间,但会增加内存与流量,需按业务权衡。
预加载整体流程:
- 触发时机:当前页 onPageFinished / 应用启动或回到前台延迟数秒 / 用户行为预测或常用 URL 列表。
- 预加载动作:从 WebView 池 obtain 或创建隐藏 WebView → 对目标 URL 执行 loadUrl → 页面/资源加载完成,留在池中或落盘。
- 用户使用:用户点击进入该页 → 直接复用已加载实例或从缓存读,首屏更快。
预加载流程图:
触发时机 预加载动作 用户使用
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 当前页 onPageFin │ │ 池 obtain 或 │ │ 用户点击进入该页 │
│ 启动/回前台延迟 │──────►│ 创建隐藏 WebView│──────►│ 复用已加载实例 │
│ 常用 URL 列表 │ │ loadUrl(目标URL) │ │ 或从缓存读 │
└─────────────────┘ │ 加载完成→池/落盘 │ │ 首屏更快 │
└─────────────────┘ └─────────────────┘
预加载页面(如“下一页”)时序:
sequenceDiagram
participant User as 用户
participant Page1 as 当前页 WebView
participant Native as Native
participant Pool as WebView 池
participant Page2 as 预加载页 WebView
User->>Page1: 浏览当前页
Page1->>Native: onPageFinished
Native->>Pool: obtain() 取实例
Pool-->>Native: WebView 实例
Native->>Page2: loadUrl(下一页 URL)
Note over Page2: 后台加载,用户无感知
Page2->>Native: onPageFinished(可选:放入池或保持)
User->>Native: 点击“下一页”
Native->>Page2: 直接展示已加载的 WebView(或从池取出)
Native->>User: 首屏几乎无等待
八、架构与监控
8.1 分层与结构
第一步:按场景选结构
- 单页:Activity/Fragment 内一个 WebView,配好 WebViewClient、WebChromeClient、一个 JSInterface(或 Bridge 封装);配置与加载逻辑放在 Activity/Fragment 或 ViewModel。
- 多页/多 Tab:用 WebView 池或统一 WebViewManager,负责 obtain/recycle、生命周期(onPause/onResume/destroy);Bridge 层独立,多 WebView 共用同一套 Native 能力与白名单。
- 大型应用:拆成模块——WebView 管理(创建/复用/销毁)、Bridge(注册 handler、参数校验、回调)、缓存/预加载(可选)、错误与性能上报;用接口与配置注入,便于单测与替换。
按场景选结构流程图:
┌─────────────────────┐
│ 按场景选结构 │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ 只有单页、单 WebView?│
└──────────┬──────────┘
是 │ 否
│ └──────► 多页/多 Tab?
▼ 是→WebView 池 + WebViewManager
单页结构: + Bridge 独立
WebView + Client 否→大型应用:拆模块
+ JSInterface (管理/Bridge/缓存/监控)
+ ViewModel
第二步:定依赖方向
UI 层依赖 WebViewManager 与 Bridge;Bridge 不直接依赖具体 Activity,通过回调或接口与业务层通信。
8.2 多 WebView 与 Tab 管理
- 多 Tab:每个 Tab 一个 WebView(或从池取);切换时对当前 WebView 调
onPause(),对即将显示的调onResume(),避免后台 WebView 继续耗资源。 - 栈内多页:每页一个 WebView 时,在页面销毁时及时
destroy()并置空引用,避免栈中残留导致内存与状态错乱。 - 复用:从池取出的 WebView 使用前重新设置 WebViewClient、WebChromeClient、JSInterface,并清空历史与缓存,避免串页或旧回调。
8.3 错误监控与上报
- Native 侧:Bridge 内统一 try-catch,记录异常类型、方法名、参数长度或摘要(勿记敏感内容)、栈信息,上报到自有日志/监控平台。
- JS 侧:提供
onJSError(message, stack)等 Bridge 方法,供前端在window.onerror或 Promise 未捕获时上报;Native 只做转发与聚合。 - 脱敏:上报前去掉 URL 中的 token、用户 ID、密码等;栈信息可截断或哈希后再上报。
8.4 性能监控
- 首屏/页面加载耗时:在
onPageStarted记开始时间,onPageFinished记结束时间,二者之差即粗略首屏时间,用于评估加载体验、发现慢页。 - Bridge 调用耗时:在每次调用 Native 前后打点,或封装统一入口统计,用于发现慢接口、控制调用频率。
- WebView 内存:用
Debug.getNativeHeapSize()或系统 API 粗略观察,或配合 LeakCanary 检测泄漏,用于评估是否需池化或降级。 - 上报策略:对上述指标做采样(如 1% 请求)、按页面或版本聚合,避免数据量过大。
九、版本与 API 速查
选型或做兼容时,可结合第一章的对比表与本节的版本对照,确认当前最低支持版本下能用哪些方式。
| Android 版本 | API | 关键点 |
|---|---|---|
| 4.1 | 16 | addJavascriptInterface 有漏洞,慎用 |
| 4.2 | 17 | 必须用 @JavascriptInterface |
| 4.4 | 19 | evaluateJavascript 可用,建议作为主方案 |
| 5.0 | 21 | shouldOverrideUrlLoading(WebResourceRequest) 需一并重写 |
| 6.0 | 23 | WebMessagePort / postMessage 可用 |
| 7.0+ | 24+ | 文件与安全策略更严,注意权限与路径;API 24+ 可用 Lambda 等 Java 8 语法(或脱糖) |
API 速查
- JS → Android:addJavascriptInterface(4.2+)/ onJsPrompt(全版本)/ URL Scheme / postMessage(6.0+)。
- Android → JS:evaluateJavascript(4.4+,有返回值)/ loadUrl("javascript:...")(全版本,无返回值)。
附录:关键代码片段速查
以下为最常用片段;完整示例与说明见第二、三章。
JS 调 Android(addJavascriptInterface):
Android.showToast('msg');
var info = Android.getDeviceInfo();
Android 调 JS(evaluateJavascript):
// 需在主线程、onPageFinished 之后调用;Lambda 需 API 24+ 或 Java 8 脱糖
webView.evaluateJavascript("javascript:fn('arg')", value -> { /* 解析 value,见 6.3 */ });
返回值简单解析(Kotlin,适用于 JS 返回字符串或 JSON 字符串):
fun parseJSResult(value: String?): String? {
if (value == null || value == "null") return null
var s = value.trim()
if (s.startsWith("\"") && s.endsWith("\""))
s = s.drop(1).dropLast(1).replace("\\\"", "\"")
return s
}
安全清理 WebView(onDestroy,与 6.1 一致):
webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
webView.clearHistory()
webView.clearCache(true)
webView.onPause()
(webView.parent as? ViewGroup)?.removeView(webView) // 从父布局移除,无父布局则跳过
webView.destroy()
webView = null