Android WebView 混合开发完整指南

259 阅读40分钟

一、概述与选型

1.0 Hybrid 框架原理

Hybrid(混合开发)指在 Native App 里用 WebView 加载 H5 页面,通过 JSBridge 在 Native 与 H5 之间双向调用的开发方式。原理可概括为:

  1. Native 壳:Activity/Fragment 提供容器,内嵌 WebView;负责生命周期、权限、系统能力(相机、定位、文件等)。
  2. H5 页面:在 WebView 中运行,负责部分 UI 与业务逻辑;可远程加载(URL)或本地打包(assets)。
  3. JSBridge:连接两端。H5 通过约定方式(如 addJavascriptInterface、URL Scheme、prompt 等)调用 Native;Native 通过 evaluateJavascript / loadUrl("javascript:...") 或注入回调调用 H5。
  4. 约定与安全:双方约定全局对象/方法名、参数与返回格式(如 JSON)、调用时机;Native 侧做白名单、参数校验与脱敏,防止恶意页面滥用能力。

整体上:Native 提供“壳 + 能力”,H5 提供“页面 + 逻辑”,Bridge 负责“互通 + 约束”。下文所述均为该框架下 Android 与 JS 的通信实现方式。

1.1 方式对比(JS → Android)

方式返回值兼容性安全性实现难度参数/数据典型场景推荐度
addJavascriptInterface✅ 有返回值;方法 return 的值 JS 同步拿到Android 4.2+ 且方法需加 @JavascriptInterface4.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+、需要双向长连接通信⭐⭐⭐⭐

补充说明:

  • 返回值
    有返回值的(如 addJavascriptInterfaceonJsPrompt)在 JS 里同步拿到;URL Scheme、alert/console 等需 Native 再调 evaluateJavascript 或注入回调才能把结果回给 JS。

  • 安全性
    4.2 前 addJavascriptInterface 会暴露未注解方法,存在风险;URL Scheme 需校验协议与来源,避免被其他应用抢调。

  • 选型建议
    优先 addJavascriptInterface(4.2+)或 onJsPrompt(兼容老版本且需返回值);强安全且可接受 6.0+ 时用 postMessage;URL Scheme 适合简单、无返回值或兼容老机。

JS 调 Android 选型示意:

  1. 需同步返回值?
    • 是 → 最低支持 4.2?是则用 addJavascriptInterface,否则用 onJsPrompt
    • 否 → 进入下一步。
  2. 强调安全且最低 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 接口约定页面加载前就建立好桥、或需统一入口时⭐⭐⭐

补充说明:

  • 返回值
    evaluateJavascriptValueCallback 收到的是 JS 执行结果的字符串形式(JSON 会多一层引号和转义,需在 Native 侧解析);loadUrl 无法拿到返回值。

  • 线程
    两种方式都必须在主线程调用;在子线程需先 runOnUiThreadHandler(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)

onJsPromptonJsAlertonJsConfirmonConsoleMessage 均为 WebChromeClient 的回调方法,分别用于拦截页面内 JS 的 prompt()alert()confirm() 以及控制台输出。用约定前缀(如 jsbridge://)区分为桥接后由 Native 处理。实现方式:在对应回调里判断 message 是否带约定前缀,若是则处理并 return true,并按需调用 result.confirm(value) 等,避免系统再弹默认框。

四种方式对比:

方式拦截的 JS API返回值是否弹窗适用场景推荐作桥接
onJsPromptprompt(msg)✅ 通过 result.confirm(value) 同步回传拦截后可不弹需同步拿 Native 结果的桥接(如 getDeviceInfo、配置)⭐ 推荐
onJsAlertalert(msg)❌ 无会弹框,除非拦截后 return true仅把消息传给 Native,不做主通道
onJsConfirmconfirm(msg)✅ 用户点确定/取消,通过 result.confirm()/cancel()会弹确认框需用户二次确认的操作(如删除前确认)
onConsoleMessageconsole.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 中分别实现 onJsAlertonJsConfirmonConsoleMessage 回调,若作桥接则同样判断 message 是否带约定前缀,若是则处理并 return trueresult.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 把另一头交给 H5postWebMessage("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,只能用“回调”兜底:

  1. Native 调 JS 时把 callbackId 传进去(如 getData(null, function(r){ Android.onJSCallback('cb_1', r); }));
  2. 页面里的方法执行完后,在 JS 里调 Native 暴露的 Android.onJSCallback(callbackId, result),把结果回传;
  3. 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.Androidwindow.onerror);之后通过调用这些方法就能控制或配合 H5 页面。H5 的用法必须和注入里约定的接口对应(方法名、参数格式、调用时机一致),否则会报错或不起作用。

二、原理

  1. 同一执行环境
    WebView 当前页面对应一个 JS 引擎、一个 window、一份 DOM。Native 通过 evaluateJavascript("javascript:...")loadUrl("javascript:...")字符串交给这个引擎执行,因此注入的代码与页面里的 <script> 共享同一全局环境,可读写 window、改 DOM、挂对象或覆盖全局函数,后续页面逻辑都会受影响。

  2. 注入 vs 不注入(含“直接调页面已有方法”)

    • 不注入:不往页面里执行“额外的新代码”。
      • JS 调 Native:用 addJavascriptInterface 绑定对象后,页面里直接写 Android.xxx() 即可,页面本来就有能力调 Native,不需要再塞一段脚本。
      • Native 调 JS:页面里已经定义了 getData 等函数,Native 直接 evaluateJavascript("getData()", callback) 调用已有方法,没有在页面里新增任何代码。
    • 注入:Native 主动往页面里执行一段“新”的 JS 字符串,这段代码不是页面自带的,执行后会在 window新增或改写内容(如 window.Androidwindow.onerrorwindow._nativeCb)。
      • 用途:页面原本没有这些逻辑,通过注入“塞进去”,从而在不改 H5 源码的前提下扩展或改变页面行为(如提前挂 Bridge、错误上报、注入配置)。
    • 一句话:不注入 = 只“用”页面里已有的(对象/方法);注入 = 先“塞”一段新代码进页面,再“用”这段代码产生的结果。
  3. 执行时机

    • 加载顺序: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 约定好接口和时机 即可。

配合方式概览:

方式NativeH5说明
只靠 addJavascriptInterface绑定对象(如 Android),不注入直接写 Android.xxx(),挂 window.getData 等供 Native 调约定对象名、方法名、参数格式即可
Native 再注入 Bridge除绑定外,在 onPageFinished(或更早)注入脚本,如挂 __BRIDGE_READY__若执行早于 Native 挂对象,则轮询或监听自定义事件(如 AndroidReady)后再调适合 H5 script 很早执行、或需统一 Bridge 封装的场景
用 JSBridge 库按库文档注册 handler、调 H5WebViewJavascriptBridgeReady 等就绪后 register/call协议由库统一约定

“注入 window._nativeCb 后由 Native 调”的典型用法:Native 在 onPageFinished 等时机注入 window._nativeCb = function(id, data) { ... };之后需要把数据推给页面时,就 evaluateJavascript("window._nativeCb('id','...')")。这是 Native 主动调页面里已挂好的函数,和 3.3 的“Native 调 JS → JS 再调 onJSCallback 回传”是反过来的方向。

约定要点

  • 全局对象/方法名:如 window.Androidwindow.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 相关调用必须在主线程;子线程中需先 runOnUiThreadHandler(Looper.getMainLooper()).post(...) 再执行。
  • 返回值evaluateJavascriptValueCallback 收到的是 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/JsBridgehappydog-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/SafeWebViewBridgepedant/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 转义(<&lt;"&quot; 等),避免 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("&", "&amp;").replace("<", "&lt;")
              .replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;");
}

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 无法回收。

处理

  1. 在合适的生命周期里释放:在 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;
  1. 尽量用 ApplicationContext 创建 WebView:若不需要 Activity 主题、弹窗、文件选择等,用 getApplicationContext() 可避免 WebView 持有 Activity。
  2. 必须用 Activity Context 时:在 addJavascriptInterface 绑定的 Bridge 对象里对 Context 使用 WeakReference,回调时先 get() 判空再使用,防止 Activity 已销毁仍被引用。
  3. 单 Activity 多 Fragment:Fragment 销毁时务必把其持有的 WebView 从父布局 removeViewdestroy(),避免被其他 Fragment 或 Activity 间接持有导致无法回收。

6.2 页面未加载完就调 JS

现象evaluateJavascriptloadUrl("javascript:...") 不生效、无回调、或报错。

原因:DOM 或脚本尚未就绪,全局对象或方法尚未挂载。

处理

  • 必须在 WebViewClient.onPageFinished 之后再调 JS;若页面内有异步脚本,可再 postDelayed 300~500ms,或让 H5 在就绪后通过 Bridge 通知 Native“页面已就绪”。
  • 避免在 onPageStartedloadUrl 调用刚返回时(此时页面尚未加载完)立即执行 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 线程

现象:在子线程调用 evaluateJavascriptloadUrladdJavascriptInterface 等导致崩溃或未定义行为。

原因:WebView 相关 API 必须在主线程调用。

处理:在子线程中通过 runOnUiThreadHandler(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 绑定的对象被覆盖或命名冲突报错。

处理:使用唯一命名(如 MyAppAndroidYourApp_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-ControlExpiresETag)决定是否将响应体写入本地缓存目录,并记录过期时间、ETag 等元数据。
  • 再次请求同一 URL 时:先查本地是否有缓存;若有且未过期,直接使用缓存(不请求网络);若已过期,则带 If-None-Match / If-Modified-Since 发条件请求,服务器返回 304 时继续用本地缓存并更新过期时间,返回 200 时用新内容并更新缓存。
  • 因此“离线”能力依赖服务端正确设置缓存头;若服务端不设或设了 no-store,内置缓存无法持久化,需要自定义离线方案。

内置 HTTP 缓存流程:

  1. 请求 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 或总大小上限做淘汰;需要“先显示缓存再更新”时,可先返回缓存,再在后台请求新内容并覆盖本地。

自定义离线缓存流程:

  1. WebView 发起资源请求 → shouldInterceptRequest 被调用。
  2. 查本地缓存
    • 命中且未过期 → 用本地数据构造 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 管理

  1. 多 Tab:每个 Tab 一个 WebView(或从池取);切换时对当前 WebView 调 onPause(),对即将显示的调 onResume(),避免后台 WebView 继续耗资源。
  2. 栈内多页:每页一个 WebView 时,在页面销毁时及时 destroy() 并置空引用,避免栈中残留导致内存与状态错乱。
  3. 复用:从池取出的 WebView 使用前重新设置 WebViewClient、WebChromeClient、JSInterface,并清空历史与缓存,避免串页或旧回调。

8.3 错误监控与上报

  1. Native 侧:Bridge 内统一 try-catch,记录异常类型、方法名、参数长度或摘要(勿记敏感内容)、栈信息,上报到自有日志/监控平台。
  2. JS 侧:提供 onJSError(message, stack) 等 Bridge 方法,供前端在 window.onerror 或 Promise 未捕获时上报;Native 只做转发与聚合。
  3. 脱敏:上报前去掉 URL 中的 token、用户 ID、密码等;栈信息可截断或哈希后再上报。

8.4 性能监控

  1. 首屏/页面加载耗时:在 onPageStarted 记开始时间,onPageFinished 记结束时间,二者之差即粗略首屏时间,用于评估加载体验、发现慢页。
  2. Bridge 调用耗时:在每次调用 Native 前后打点,或封装统一入口统计,用于发现慢接口、控制调用频率。
  3. WebView 内存:用 Debug.getNativeHeapSize() 或系统 API 粗略观察,或配合 LeakCanary 检测泄漏,用于评估是否需池化或降级。
  4. 上报策略:对上述指标做采样(如 1% 请求)、按页面或版本聚合,避免数据量过大。

九、版本与 API 速查

选型或做兼容时,可结合第一章的对比表与本节的版本对照,确认当前最低支持版本下能用哪些方式。

Android 版本API关键点
4.116addJavascriptInterface 有漏洞,慎用
4.217必须用 @JavascriptInterface
4.419evaluateJavascript 可用,建议作为主方案
5.021shouldOverrideUrlLoading(WebResourceRequest) 需一并重写
6.023WebMessagePort / 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