Android WebView 混合开发完整指南

一、JavaScript 调用 Android 的所有方式

1.1 addJavascriptInterface 方法(最常用)

原理:将 Java 对象暴露给 WebView 中的 JavaScript,JavaScript 可以直接调用该对象的方法。

版本要求

  • Android 4.2+ 必须使用 @JavascriptInterface 注解
  • Android 4.2 之前存在安全漏洞

特点

  • ✅ 最简单直接的方式
  • ✅ 支持双向通信
  • ✅ 可以传递复杂参数
  • ⚠️ Android 4.2 之前有安全风险
  • ⚠️ 需要确保方法有 @JavascriptInterface 注解

Android 端代码

java 复制代码
// 创建接口类
public class JSInterface {
    private Context context;
    
    public JSInterface(Context context) {
        this.context = context;
    }
    
    @JavascriptInterface
    public void showToast(String message) {
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
    }
    
    @JavascriptInterface
    public String getDeviceInfo() {
        return Build.MODEL;
    }
}

// 绑定到 WebView
WebView webView = findViewById(R.id.webview);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JSInterface(this), "Android");

JavaScript 端代码

javascript 复制代码
// 调用方法
Android.showToast('Hello from JavaScript');
var deviceInfo = Android.getDeviceInfo();

1.2 shouldOverrideUrlLoading 拦截(URL Scheme)

原理 :拦截 WebView 中的 URL 请求,通过自定义协议(如 myapp://)实现通信。

版本要求:所有 Android 版本

特点

  • ✅ 安全性较高
  • ✅ 兼容性好
  • ❌ 只能单向通信(JS → Android)
  • ❌ 无法直接获取返回值(需要通过回调)

Android 端代码

java 复制代码
webView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (url.startsWith("myapp://")) {
            Uri uri = Uri.parse(url);
            String action = uri.getHost();
            String param = uri.getQueryParameter("param");
            
            if ("showToast".equals(action)) {
                Toast.makeText(context, param, Toast.LENGTH_SHORT).show();
            }
            return true;
        }
        return false;
    }
    
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        String url = request.getUrl().toString();
        if (url.startsWith("myapp://")) {
            // 处理逻辑同上
            return true;
        }
        return false;
    }
});

JavaScript 端代码

javascript 复制代码
// 使用 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);
}

callAndroid("showToast", "Hello");

1.3 onJsPrompt 方法(推荐,可返回值)

原理 :拦截 JavaScript 的 prompt() 调用,可以实现双向通信并获取返回值。

版本要求:所有 Android 版本

特点

  • ✅ 可以获取返回值
  • ✅ 安全性较高
  • ✅ 兼容性好
  • ⚠️ 需要约定通信协议格式

Android 端代码

java 复制代码
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;
    }
});

JavaScript 端代码

javascript 复制代码
function callNative(action, params) {
    var url = "jsbridge://" + action;
    if (params) {
        url += "?" + new URLSearchParams(params).toString();
    }
    return prompt(url);
}

// 使用
var device = callNative("getDeviceInfo");
console.log("Device: " + device);

1.4 onJsAlert 方法

原理 :拦截 JavaScript 的 alert() 调用。

版本要求:所有 Android 版本

特点

  • ✅ 简单易用
  • ❌ 只能单向通信(JS → Android)
  • ❌ 无返回值
  • ⚠️ 会显示弹窗,用户体验可能不佳

Android 端代码

java 复制代码
webView.setWebChromeClient(new WebChromeClient() {
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
        if (message.startsWith("jsbridge://")) {
            handleBridgeMessage(message);
            result.confirm();
            return true;
        }
        new AlertDialog.Builder(view.getContext())
            .setMessage(message)
            .setPositiveButton("确定", (d, w) -> result.confirm())
            .show();
        return true;
    }
});

JavaScript 端代码

javascript 复制代码
function callNative(action, params) {
    var msg = "jsbridge://" + action + (params ? "?" + JSON.stringify(params) : "");
    alert(msg);
}

1.5 onJsConfirm 方法

原理 :拦截 JavaScript 的 confirm() 调用,可以获取用户的选择结果。

版本要求:所有 Android 版本

特点

  • ✅ 可以获取用户选择
  • ❌ 只能单向通信(JS → Android)
  • ⚠️ 会显示确认对话框

Android 端代码

java 复制代码
webView.setWebChromeClient(new WebChromeClient() {
    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
        if (message.startsWith("jsbridge://")) {
            handleBridgeMessage(message);
            result.confirm();
            return true;
        }
        new AlertDialog.Builder(view.getContext())
            .setMessage(message)
            .setPositiveButton("确定", (d, w) -> result.confirm())
            .setNegativeButton("取消", (d, w) -> result.cancel())
            .show();
        return true;
    }
});

JavaScript 端代码

javascript 复制代码
function callNative(action, params) {
    var msg = "jsbridge://" + action + (params ? "?" + JSON.stringify(params) : "");
    return confirm(msg);
}

1.6 onConsoleMessage 方法

原理:捕获 JavaScript 控制台日志,可以用于传递数据。

版本要求:所有 Android 版本

特点

  • ✅ 主要用于调试
  • ✅ 可以传递数据
  • ❌ 不适合生产环境使用
  • ⚠️ 主要用于日志收集

Android 端代码

java 复制代码
webView.setWebChromeClient(new WebChromeClient() {
    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
        String msg = consoleMessage.message();
        if (msg.startsWith("jsbridge://")) {
            handleBridgeMessage(msg);
        } else {
            Log.d("WebView", msg);
        }
        return true;
    }
});

JavaScript 端代码

javascript 复制代码
function callNative(action, params) {
    var msg = "jsbridge://" + action + (params ? "?" + JSON.stringify(params) : "");
    console.log(msg);
}

1.7 postMessage 方法(Android 6.0+)

原理 :使用 WebMessagePort 进行消息传递,这是官方推荐的安全通信方式。

版本要求:Android 6.0 (API 23)+

特点

  • ✅ 官方推荐的安全方式
  • ✅ 支持双向通信
  • ✅ 安全性高
  • ❌ 需要 Android 6.0+
  • ⚠️ 实现相对复杂

Android 端代码

java 复制代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE)) {
        WebMessagePort[] ports = webView.createWebMessageChannel();
        WebMessagePort port1 = ports[0];
        WebMessagePort port2 = ports[1];
        
        // 将 port2 传递给 JavaScript
        webView.postWebMessage(
            new WebMessage("init", new WebMessagePort[]{port2}),
            Uri.parse("https://example.com")
        );
        
        // 监听来自 JavaScript 的消息
        port1.setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
            @Override
            public void onMessage(WebMessagePort port, WebMessage message) {
                Log.d("WebView", "Received: " + message.getData());
                port.postMessage(new WebMessage("Response from Android"));
            }
        });
    }
}

JavaScript 端代码

javascript 复制代码
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("Received: " + e.data);
        };
        port.postMessage("Hello from JavaScript");
        window.androidPort = port;
    }
});

// 后续使用
window.androidPort?.postMessage("Another message");

1.8 通过 JSBridge 库调用

详见 [三、JSBridge 开源库实现方式](#三、JSBridge 开源库实现方式 "#%E4%B8%89jsbridge-%E5%BC%80%E6%BA%90%E5%BA%93%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F")


二、Android 调用 JavaScript 的所有方式

2.1 evaluateJavascript 方法(推荐,Android 4.4+)

原理:在 WebView 中执行 JavaScript 代码并获取返回值。

版本要求:Android 4.4 (API 19)+

特点

  • ✅ 支持异步回调
  • ✅ 可以获取返回值
  • ✅ 性能较好
  • ✅ 官方推荐方式
  • ❌ 需要 Android 4.4+

Android 端代码

java 复制代码
// 调用无返回值方法
webView.evaluateJavascript("javascript:showMessage('Hello')", null);

// 调用有返回值方法
webView.evaluateJavascript("javascript:getData()", new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) {
        if (value != null && !value.equals("null")) {
            try {
                JSONObject json = new JSONObject(value);
                Log.d("WebView", "Result: " + json.toString());
            } catch (JSONException e) {
                Log.d("WebView", "Result: " + value);
            }
        }
    }
});

// 传递参数
String params = new JSONObject().put("name", "John").put("age", 30).toString();
webView.evaluateJavascript("javascript:handleData(" + params + ")", null);

JavaScript 端代码

javascript 复制代码
function showMessage(message) {
    alert(message);
}

function getData() {
    return { status: 'success', data: { name: 'John', age: 30 } };
}

function handleData(params) {
    console.log('Received:', params);
    return { success: true };
}

返回值处理

java 复制代码
webView.evaluateJavascript("javascript:getData()", new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) {
        if (value == null || value.equals("null")) return;
        
        // 去除首尾引号和转义字符
        String result = value;
        if (result.startsWith("\"") && result.endsWith("\"")) {
            result = result.substring(1, result.length() - 1);
        }
        result = result.replace("\\\"", "\"").replace("\\\\", "\\");
        
        // 解析 JSON
        try {
            JSONObject json = new JSONObject(result);
            Log.d("WebView", "Result: " + json.toString());
        } catch (JSONException e) {
            Log.d("WebView", "Result: " + result);
        }
    }
});

2.2 loadUrl 方法(兼容低版本)

原理 :通过加载 javascript: 协议的 URL 来执行 JavaScript 代码。

版本要求:所有 Android 版本

特点

  • ✅ 兼容所有 Android 版本
  • ✅ 简单易用
  • ❌ 无法获取返回值
  • ❌ 性能较低
  • ⚠️ 主要用于兼容 Android 4.4 以下版本

Android 端代码

java 复制代码
// 调用 JavaScript 方法
webView.loadUrl("javascript:showMessage('Hello')");

// 传递参数
String params = new JSONObject().put("name", "John").put("age", 30).toString();
webView.loadUrl("javascript:handleData(" + params + ")");

JavaScript 端代码

javascript 复制代码
function showMessage(message) {
    alert(message);
}

function handleData(params) {
    console.log('Received:', params);
}

注意事项

  • 无法获取 JavaScript 的返回值
  • 参数中的特殊字符需要转义
  • 性能比 evaluateJavascript
  • 建议在 Android 4.4+ 使用 evaluateJavascript

2.3 通过 JavaScript 回调机制

原理:Android 调用 JavaScript 时传入回调函数名,JavaScript 执行完成后通过回调通知 Android。

版本要求 :所有 Android 版本(配合 evaluateJavascriptloadUrl

特点

  • ✅ 可以实现异步通信
  • ✅ 可以获取返回值
  • ✅ 兼容性好

Android 端代码

java 复制代码
public class WebViewCallback {
    private WebView webView;
    private Map<String, ValueCallback<String>> callbacks = new HashMap<>();
    private int callbackId = 0;
    
    public WebViewCallback(WebView webView) {
        this.webView = webView;
    }
    
    public void callJS(String method, String params, ValueCallback<String> callback) {
        String callbackName = "callback_" + (callbackId++);
        callbacks.put(callbackName, callback);
        
        String jsCode = String.format(
            "javascript:%s(%s, function(result) { Android.onJSCallback('%s', result); });",
            method, params, callbackName
        );
        webView.evaluateJavascript(jsCode, null);
    }
    
    @JavascriptInterface
    public void onJSCallback(String callbackName, String result) {
        ValueCallback<String> callback = callbacks.remove(callbackName);
        if (callback != null) {
            callback.onReceiveValue(result);
        }
    }
}

// 使用
WebViewCallback callback = new WebViewCallback(webView);
webView.addJavascriptInterface(callback, "Android");
callback.callJS("getData", "null", result -> Log.d("WebView", "Result: " + result));

JavaScript 端代码

javascript 复制代码
function getData(param, callback) {
    setTimeout(() => {
        callback(JSON.stringify({ result: 'success', data: param }));
    }, 1000);
}

2.4 通过 JSBridge 库调用

详见 [三、JSBridge 开源库实现方式](#三、JSBridge 开源库实现方式 "#%E4%B8%89jsbridge-%E5%BC%80%E6%BA%90%E5%BA%93%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F")


2.5 通过注入 JavaScript 代码

原理:在页面加载时注入 JavaScript 代码,建立通信桥梁。

版本要求:所有 Android 版本

特点

  • ✅ 可以在页面加载前建立通信
  • ✅ 灵活性高
  • ⚠️ 需要处理时机问题

Android 端代码

java 复制代码
webView.setWebViewClient(new WebViewClient() {
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        String jsCode = "javascript:(function() {" +
            "window.AndroidBridge = {" +
            "  onNativeCall: function(callback) { window._nativeCallback = callback; }" +
            "};" +
            "})();";
        view.evaluateJavascript(jsCode, null);
    }
});

// 调用注入的方法
public void callJSMethod() {
    webView.evaluateJavascript("javascript:window._nativeCallback && window._nativeCallback('Hello')", null);
}

JavaScript 端代码

javascript 复制代码
window.addEventListener('load', function() {
    if (window.AndroidBridge) {
        window.AndroidBridge.onNativeCall(function(message) {
            console.log('Received: ' + message);
        });
    }
});

三、JSBridge 开源库实现方式

3.1 JsBridge(lzyzsd)

GitHubgithub.com/lzyzsd/JsBr...

特点

  • 受微信 WebView JsBridge 启发
  • 支持双向通信
  • 支持持久回调
  • 提供默认处理器

依赖

gradle 复制代码
implementation 'com.github.lzyzsd:jsbridge:1.0.4'

Android 端代码

java 复制代码
BridgeWebView webView = (BridgeWebView) findViewById(R.id.webview);

// 注册处理器(供 JS 调用)
webView.registerHandler("submitFromWeb", (data, function) -> 
    function.onCallBack("Response from Android")
);

// 调用 JavaScript 方法
webView.callHandler("functionInJs", "data", result -> 
    Log.d("Bridge", "Result: " + result)
);

JavaScript 端代码

javascript 复制代码
// 连接 Bridge
function connectBridge(callback) {
    if (window.WebViewJavascriptBridge) {
        callback(WebViewJavascriptBridge);
    } else {
        document.addEventListener('WebViewJavascriptBridgeReady', () => 
            callback(WebViewJavascriptBridge)
        );
    }
}

connectBridge(bridge => {
    // 注册处理器(供 Android 调用)
    bridge.registerHandler("functionInJs", (data, callback) => 
        callback("Response from JS")
    );
    
    // 调用 Android 方法
    bridge.callHandler('submitFromWeb', {param: 'value'}, response => 
        console.log('Response: ' + response)
    );
});

3.2 DSBridge

GitHubgithub.com/wendux/DSBr...

特点

  • 跨平台支持(iOS + Android)
  • 支持同步和异步调用
  • 支持进度回调(一次调用,多次返回)
  • 支持腾讯 X5 内核
  • 支持 API 命名空间
  • 支持调试模式

依赖

gradle 复制代码
implementation 'com.github.wendux:DSBridge-Android:3.0.0'

Android 端代码

java 复制代码
// 定义 API 类
public class JsApi {
    @JavascriptInterface
    public String testSyn(Object msg) {
        return msg + " [syn]";
    }
    
    @JavascriptInterface
    public void testAsyn(Object msg, CompletionHandler<String> handler) {
        handler.complete(msg + " [asyn]");
    }
}

// 添加到 WebView
DWebView dWebView = (DWebView) webView;
dWebView.addJavascriptObject(new JsApi(), "nativeApi");

// 调用 JavaScript 方法
dWebView.callHandler("test.method", new Object[]{"hello"}, retValue -> 
    Log.d("DSBridge", "Result: " + retValue)
);

JavaScript 端代码

javascript 复制代码
// 同步调用
var result = dsBridge.call("nativeApi.testSyn", "test");

// 异步调用
dsBridge.call("nativeApi.testAsyn", "test", val => console.log(val));

// 注册方法供 Android 调用
dsBridge.register("test.method", (arg, callback) => callback("result from js"));

3.3 SafeWebViewBridge

特点:专注于安全性的 WebView Bridge

使用场景:对安全性要求较高的应用


3.4 WebViewJavascriptBridge(iOS 风格)

特点:模仿 iOS 的 WebViewJavascriptBridge 实现

使用场景:需要 iOS/Android 统一接口的项目


3.5 X5 WebView Bridge

特点:基于腾讯 X5 内核的 Bridge 实现

使用场景:使用腾讯 X5 内核的项目

Android 端代码

java 复制代码
com.tencent.smtt.sdk.WebView x5WebView = new com.tencent.smtt.sdk.WebView(context);
x5WebView.addJavascriptInterface(new JSInterface(), "Android");

四、安全性与最佳实践

4.1 安全风险

4.1.1 addJavascriptInterface 安全风险

  • 问题:Android 4.2 之前存在漏洞,恶意 JavaScript 可能执行任意代码
  • 解决方案
    • 使用 @JavascriptInterface 注解(Android 4.2+)
    • 验证所有来自 JavaScript 的参数
    • 限制暴露的方法和权限

4.1.2 URL Scheme 安全风险

  • 问题:可能被恶意应用拦截
  • 解决方案
    • 验证 URL 来源
    • 使用 HTTPS
    • 添加签名验证

4.1.3 XSS 攻击风险

  • 问题:注入恶意 JavaScript
  • 解决方案
    • 内容安全策略(CSP)
    • 输入验证和转义
    • 白名单机制

4.2 最佳实践

4.2.1 WebView 安全配置

java 复制代码
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);

// 安全配置
settings.setAllowFileAccess(false);                    // 禁止文件访问
settings.setAllowContentAccess(false);                 // 禁止内容访问
settings.setAllowFileAccessFromFileURLs(false);        // 禁止从文件 URL 访问
settings.setAllowUniversalAccessFromFileURLs(false);   // 禁止通用文件访问
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALWAYS); // 禁止混合内容

// 使用 HTTPS
webView.setWebViewClient(new WebViewClient() {
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        // 生产环境应该验证证书
        handler.proceed(); // 仅用于开发环境
    }
});

4.2.2 参数验证

java 复制代码
@JavascriptInterface
public void showToast(String message) {
    // 验证参数
    if (message == null || message.length() > 100) {
        return;
    }
    
    // 防止 XSS
    message = message.replace("<", "&lt;")
                     .replace(">", "&gt;")
                     .replace("\"", "&quot;")
                     .replace("'", "&#x27;");
    
    Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}

4.2.3 权限控制

java 复制代码
public class JSInterface {
    private static final Set<String> ALLOWED_METHODS = new HashSet<String>() {{
        add("showToast");
        add("getDeviceInfo");
    }};
    
    @JavascriptInterface
    public void showToast(String message) {
        // 检查权限
        if (!hasPermission("showToast")) {
            return;
        }
        // 执行操作
    }
    
    private boolean hasPermission(String method) {
        return ALLOWED_METHODS.contains(method);
    }
}

4.2.4 使用成熟的 Bridge 库

  • 避免自己实现,使用经过验证的库
  • 定期更新依赖
  • 关注安全公告

4.2.5 版本兼容性处理

java 复制代码
// 检查 Android 版本
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    // 使用 evaluateJavascript
    webView.evaluateJavascript(jsCode, callback);
} else {
    // 使用 loadUrl
    webView.loadUrl(jsCode);
}

// 检查 @JavascriptInterface 支持
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    // 使用 @JavascriptInterface
} else {
    // 使用其他方式
}

4.2.6 错误处理

Kotlin 实现

kotlin 复制代码
@JavascriptInterface
fun handleRequest(data: String) {
    try {
        val json = JSONObject(data)
        // 处理请求
    } catch (e: JSONException) {
        Log.e(TAG, "Invalid JSON", e)
        // 返回错误信息
    } catch (e: Exception) {
        Log.e(TAG, "Error handling request", e)
        // 错误处理
    }
}

4.2.7 统一错误处理框架

原理:统一的错误处理可以提升代码可维护性,便于错误追踪和上报。

依赖说明

kotlin 复制代码
import android.util.Log
import org.json.JSONObject

Kotlin 实现

kotlin 复制代码
/**
 * 统一错误处理框架
 */
object ErrorHandler {
    private var errorReporter: ((ErrorInfo) -> Unit)? = null
    
    data class ErrorInfo(
        val type: ErrorType,
        val message: String,
        val stackTrace: String? = null,
        val context: Map<String, Any>? = null
    )
    
    enum class ErrorType {
        JS_ERROR,      // JavaScript 错误
        BRIDGE_ERROR,  // Bridge 调用错误
        NETWORK_ERROR, // 网络错误
        CACHE_ERROR,   // 缓存错误
        UNKNOWN_ERROR  // 未知错误
    }
    
    /**
     * 设置错误上报器
     */
    fun setErrorReporter(reporter: (ErrorInfo) -> Unit) {
        errorReporter = reporter
    }
    
    /**
     * 处理错误
     */
    fun handleError(
        type: ErrorType,
        message: String,
        throwable: Throwable? = null,
        context: Map<String, Any>? = null
    ) {
        val errorInfo = ErrorInfo(
            type = type,
            message = message,
            stackTrace = throwable?.stackTraceToString(),
            context = context
        )
        
        // 记录日志
        Log.e("ErrorHandler", "[${type.name}] $message", throwable)
        
        // 上报错误
        errorReporter?.invoke(errorInfo)
    }
    
    /**
     * 处理 JavaScript 错误
     */
    fun handleJSError(message: String, stackTrace: String? = null) {
        handleError(
            ErrorType.JS_ERROR,
            message,
            context = mapOf("stackTrace" to (stackTrace ?: ""))
        )
    }
    
    /**
     * 处理 Bridge 调用错误
     */
    fun handleBridgeError(method: String, error: Throwable) {
        handleError(
            ErrorType.BRIDGE_ERROR,
            "Bridge call failed: $method",
            error,
            mapOf("method" to method)
        )
    }
}

// 在 JSInterface 中使用
class JSInterface(private val context: Context) {
    @JavascriptInterface
    fun onJSError(errorInfo: String) {
        try {
            val json = JSONObject(errorInfo)
            ErrorHandler.handleJSError(
                json.optString("message", ""),
                json.optString("stack", null)
            )
        } catch (e: Exception) {
            ErrorHandler.handleError(ErrorType.JS_ERROR, "Failed to parse JS error", e)
        }
    }
    
    @JavascriptInterface
    fun callNative(method: String, params: String) {
        try {
            // 处理调用
            handleNativeCall(method, params)
        } catch (e: Exception) {
            ErrorHandler.handleBridgeError(method, e)
        }
    }
    
    private fun handleNativeCall(method: String, params: String) {
        // 实现调用逻辑
    }
}

// 配置错误上报
ErrorHandler.setErrorReporter { errorInfo ->
    // 上报到服务器
    // uploadErrorToServer(errorInfo)
}

4.2.8 性能优化

Kotlin 实现

kotlin 复制代码
// 1. WebView 复用
private val webViewPool = WebViewPool(context)

// 2. 硬件加速
webView.setLayerType(View.LAYER_TYPE_HARDWARE, null)

// 3. 减少通信频率(批量处理)
private val batchExecutor = BatchJSExecutor(webView)

// 使用批量执行器
batchExecutor.addCall("console.log('1')")
batchExecutor.addCall("console.log('2')")
// 100ms 后自动批量执行

五、常见问题与解决方案

5.1 WebView 内存泄漏问题

问题:WebView 可能导致 Activity 内存泄漏。

原因

  • WebView 持有 Activity 的 Context 引用
  • WebView 在后台线程中执行,生命周期与 Activity 不一致

解决方案

kotlin 复制代码
class WebViewActivity : AppCompatActivity() {
    private var webView: WebView? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 使用 ApplicationContext 创建 WebView(如果可能)
        webView = WebView(applicationContext)
        setContentView(webView)
    }
    
    override fun onDestroy() {
        // 重要:在 onDestroy 中清理 WebView
        webView?.let {
            try {
                it.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
                it.clearHistory()
                it.clearCache(true)
                it.onPause()
                it.removeAllViews()
                it.destroy()
                webView = null
            } catch (e: Exception) {
                // 使用统一的错误处理框架(见 4.2.7)
                ErrorHandler.handleError(
                    ErrorHandler.ErrorType.UNKNOWN_ERROR,
                    "Failed to destroy WebView",
                    e
                )
            }
        }
        super.onDestroy()
    }
    
    override fun onPause() {
        super.onPause()
        webView?.onPause()
    }
    
    override fun onResume() {
        super.onResume()
        webView?.onResume()
    }
}

5.2 WebView 未加载完成就调用 JavaScript

问题:在页面加载完成前调用 JavaScript 会失败。

解决方案

kotlin 复制代码
var isPageFinished = false

webView.webViewClient = object : WebViewClient() {
    override fun onPageFinished(view: WebView?, url: String?) {
        super.onPageFinished(view, url)
        isPageFinished = true
        // 页面加载完成后可以安全调用 JavaScript
        callJavaScriptSafely()
    }
    
    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
        super.onPageStarted(view, url, favicon)
        isPageFinished = false
    }
}

// 安全调用 JavaScript 的方法
private fun callJavaScriptSafely() {
    webView?.let {
        // 方式1:延迟调用,确保页面完全加载
        it.postDelayed({
            // 使用统一的扩展函数
            it.evaluateJavaScriptSafe("javascript:init()")
        }, 500)
        
        // 方式2:在 onPageFinished 中直接调用
    }
}

5.3 evaluateJavascript 返回值格式问题

问题:返回值包含引号和转义字符,解析困难。

解决方案 :使用 JSResultParser 工具类解析返回值

kotlin 复制代码
// 使用统一的工具类解析返回值
webView.evaluateJavaScriptSafe("javascript:getData()") { value ->
    // 使用 JSResultParser 解析
    val cleaned = JSResultParser.parseResult(value)
    val json = JSResultParser.parseAsJson(value)
    // 使用解析后的数据
}

注意

  • 推荐使用 evaluateJavaScriptSafe() 扩展函数替代直接调用 evaluateJavascript()
  • 返回值解析统一使用 JSResultParser,避免重复实现解析逻辑

5.4 特殊字符转义问题

问题:传递包含特殊字符的参数时,JavaScript 执行失败。

解决方案

kotlin 复制代码
object JSEscapeUtils {
    /**
     * 转义 JavaScript 字符串中的特殊字符
     */
    fun escapeJS(input: String?): String {
        if (input == null) return "null"
        
        return input.replace("\\", "\\\\")
                   .replace("'", "\\'")
                   .replace("\"", "\\\"")
                   .replace("\n", "\\n")
                   .replace("\r", "\\r")
                   .replace("\t", "\\t")
                   .replace("/", "\\/")
    }
    
    /**
     * 安全地调用 JavaScript(推荐使用 JSON 传递参数)
     */
    fun callJSSafely(webView: WebView, functionName: String, params: Any? = null) {
        val jsCode = if (params == null) {
            "javascript:$functionName()"
        } else {
            // 使用 JSON 传递参数,避免转义问题
            val jsonParams = JSONObject().put("data", params).toString()
            "javascript:$functionName($jsonParams)"
        }
        
        // 使用统一的扩展函数
        webView.evaluateJavaScriptSafe(jsCode)
        // 注意:低版本无法获取返回值,如需返回值请使用 WebViewBridgeKt
    }
}

5.5 线程安全问题

问题:在非主线程中调用 WebView 方法会导致崩溃。

解决方案

kotlin 复制代码
object WebViewUtils {
    /**
     * 在主线程中安全调用 WebView 方法
     */
    fun callOnMainThread(webView: WebView, runnable: Runnable) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            // 已经在主线程
            runnable.run()
        } else {
            // 切换到主线程
            Handler(Looper.getMainLooper()).post(runnable)
        }
    }
    
    /**
     * 安全调用 JavaScript
     * 注意:推荐使用扩展函数 evaluateJavaScriptSafe()
     */
    fun evaluateJavaScript(
        webView: WebView, 
        jsCode: String, 
        callback: ((String?) -> Unit)? = null
    ) {
        callOnMainThread(webView, Runnable {
            // 使用统一的扩展函数
            webView.evaluateJavaScriptSafe(jsCode, callback)
        })
    }
}

5.6 WebView 缓存问题

问题:WebView 缓存导致页面不更新。

解决方案

kotlin 复制代码
// 使用统一的配置扩展函数
// 开发环境:不使用缓存
if (BuildConfig.DEBUG) {
    webView.setupDefaultSettings(enableCache = false)
} else {
    // 生产环境:使用缓存
    webView.setupDefaultSettings(enableCache = true)
}

// 清除缓存(需要时调用)
fun clearWebViewCache() {
    try {
        webView.clearCache(true)
        webView.clearHistory()
        CookieManager.getInstance().apply {
            removeAllCookies(null)
            flush()
        }
    } catch (e: Exception) {
        // 使用统一的错误处理框架(见 4.2.7)
        ErrorHandler.handleError(
            ErrorHandler.ErrorType.CACHE_ERROR,
            "Failed to clear WebView cache",
            e
        )
    }
}

六、实际开发中的坑

6.1 addJavascriptInterface 方法名冲突

:如果 JavaScript 中已有同名对象,会导致冲突。

解决方案

kotlin 复制代码
// 使用唯一的前缀
webView.addJavascriptInterface(JSInterface(), "MyAppAndroid")

// 或者检查 JavaScript 中是否已存在
val checkCode = """
    if (typeof Android !== 'undefined') {
        window.MyAppAndroid = Android;
    }
""".trimIndent()
// 使用统一的扩展函数
webView.evaluateJavaScriptSafe(checkCode)

6.2 异步回调时序问题

:JavaScript 异步操作完成时,Android 端可能已经销毁。

解决方案

kotlin 复制代码
class JSInterface(context: Context) {
    private val contextRef = WeakReference(context)
    
    @JavascriptInterface
    fun handleAsyncResult(result: String) {
        val context = contextRef.get()
        if (context == null) {
            // Context 已被回收,忽略回调
            return
        }
        
        // 使用弱引用避免内存泄漏
        // 处理结果
    }
}

6.3 URL Scheme 被其他应用拦截

:自定义 URL Scheme 可能被其他应用拦截。

解决方案

kotlin 复制代码
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
    if (url?.startsWith("myapp://") == true) {
        // 验证 URL 来源
        if (isValidUrl(url)) {
            handleCustomUrl(url)
            return true
        }
    }
    return false
}

private fun isValidUrl(url: String): Boolean {
    // 添加签名验证
    val uri = Uri.parse(url)
    val signature = uri.getQueryParameter("sig")
    // 验证签名
    return verifySignature(url, signature)
}

6.4 evaluateJavascript 在低版本返回 null

:Android 4.4 以下版本不支持 evaluateJavascript,需要使用 loadUrl。

解决方案 :使用统一的扩展函数 evaluateJavaScriptSafe(),自动处理版本兼容性。

kotlin 复制代码
// 使用统一的扩展函数
fun callJS(webView: WebView, jsCode: String, callback: ((String?) -> Unit)? = null) {
    webView.evaluateJavaScriptSafe(jsCode, callback)
    // 注意:低版本无法获取返回值,callback 不会执行
    // 如需返回值,请使用 WebViewBridgeKt
}

6.5 WebView 在 Fragment 中的生命周期问题

:Fragment 生命周期与 WebView 不一致,可能导致崩溃。

解决方案

kotlin 复制代码
class WebViewFragment : Fragment() {
    private var webView: WebView? = null
    private var isWebViewAvailable = false
    
    override fun onCreateView(
        inflater: LayoutInflater, 
        container: ViewGroup?, 
        savedInstanceState: Bundle?
    ): View? {
        webView = WebView(requireContext())
        isWebViewAvailable = true
        return webView
    }
    
    override fun onDestroyView() {
        isWebViewAvailable = false
        webView?.destroy()
        webView = null
        super.onDestroyView()
    }
    
    fun callJavaScript(code: String) {
        if (isWebViewAvailable && webView != null) {
            // 使用统一的扩展函数
            webView?.evaluateJavaScriptSafe(code)
        }
    }
}

七、版本兼容性对照表

Android 版本 API Level 关键特性 注意事项
Android 4.1 16 addJavascriptInterface 存在安全漏洞 必须使用 URL Scheme 或其他方式
Android 4.2 17 @JavascriptInterface 注解必需 必须添加注解才能使用
Android 4.4 19 evaluateJavascript 可用 推荐使用,支持返回值
Android 5.0 21 shouldOverrideUrlLoading 方法变更 需要重写新方法
Android 6.0 23 postMessage 支持 官方推荐的安全通信方式
Android 7.0 24 文件访问限制更严格 注意文件访问权限
Android 8.0 26 WebView 多进程支持 注意进程间通信

八、性能优化建议

8.1 资源预加载策略

WebView 池(预加载优化基础)

kotlin 复制代码
class WebViewPool(private val context: Context) {
    private val pool = mutableListOf<WebView>()
    private val maxSize = 3
    
    fun obtain(): WebView {
        return if (pool.isNotEmpty()) {
            pool.removeAt(0)
        } else {
            WebView(context.applicationContext).apply {
                setupDefaultSettings()
            }
        }
    }
    
    fun recycle(webView: WebView) {
        if (pool.size < maxSize) {
            webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
            pool.add(webView)
        } else {
            webView.destroy()
        }
    }
}

8.1.1 页面预加载

原理:在用户访问前提前加载常用页面,提升用户体验。使用 WebView 池中的实例进行预加载,避免重复创建。

Kotlin 实现

kotlin 复制代码
class WebViewPreloader(
    private val webViewPool: WebViewPool
) {
    private val preloadWebView: WebView by lazy { webViewPool.obtain() }
    
    fun preloadPage(url: String) = preloadWebView.loadUrl(url)
    fun preloadPages(urls: List<String>) = urls.forEach { preloadPage(it) }
    fun cleanup() = webViewPool.recycle(preloadWebView)
}

8.1.2 静态资源预加载

原理:提前加载 CSS、JavaScript、图片等静态资源。使用 WebView 池中的实例,避免为预加载创建额外的 WebView。

Kotlin 实现

kotlin 复制代码
class ResourcePreloader(
    private val webViewPool: WebViewPool
) {
    private val preloadWebView: WebView by lazy { webViewPool.obtain() }
    
    fun preloadResource(url: String) {
        val html = when {
            url.endsWith(".js") -> "<html><head><script src=\"$url\"></script></head><body></body></html>"
            url.endsWith(".css") -> "<html><head><link rel=\"stylesheet\" href=\"$url\"></head><body></body></html>"
            url.matches(Regex(".*\\.(jpg|jpeg|png|gif|webp)$", RegexOption.IGNORE_CASE)) -> 
                "<html><body><img src=\"$url\" style=\"display:none;\"></body></html>"
            else -> return
        }
        preloadWebView.loadDataWithBaseURL(null, html, "text/html", "utf-8", null)
    }
    
    fun cleanup() = webViewPool.recycle(preloadWebView)
}

8.1.3 预加载时机控制

原理:选择合适的时机进行预加载,避免影响应用启动速度和用户体验。

Kotlin 实现

kotlin 复制代码
class PreloadManager(private val context: Context) {
    private val webViewPool = WebViewPool(context)
    private val preloader = WebViewPreloader(webViewPool)
    private val handler = Handler(Looper.getMainLooper())
    
    fun preloadOnAppStart() {
        handler.postDelayed({
            preloader.preloadPages(listOf("https://example.com/home", "https://example.com/about"))
        }, 3000)
    }
    
    fun preloadAfterPageLoad(currentUrl: String) {
        handler.postDelayed({
            val nextUrls = when {
                currentUrl.contains("/home") -> listOf("https://example.com/products")
                currentUrl.contains("/products") -> listOf("https://example.com/product/detail")
                else -> emptyList()
            }
            preloader.preloadPages(nextUrls)
        }, 1000)
    }
    
    fun cleanup() = preloader.cleanup()
}

8.2 离线缓存策略

8.2.1 WebView 缓存工作原理

一、WebView 缓存的核心机制

WebView 的缓存基于 HTTP 缓存协议,这是 Web 标准缓存机制,由 WebView 内核自动管理。

缓存存储位置

  • Android 路径:/data/data/包名/cache/webviewCache/
  • 存储内容:HTML、CSS、JavaScript、图片等所有网络资源
  • 存储方式:文件系统,按 URL 和缓存策略组织

二、HTTP 缓存的工作原理

1. 缓存判断流程

sql 复制代码
用户请求资源(如 https://example.com/page.html)
    ↓
WebView 检查本地缓存目录
    ↓
缓存是否存在?
    ├─ 不存在 → 直接请求网络 → 保存响应到缓存 → 返回给 WebView
    └─ 存在 → 检查缓存是否过期
         ├─ 未过期 → 直接返回缓存(不请求网络,200 from cache)
         └─ 已过期 → 发送条件请求(If-None-Match / If-Modified-Since)
              ├─ 服务器返回 304 → 使用缓存(更新过期时间)
              └─ 服务器返回 200 → 更新缓存(新内容)

2. 缓存过期判断

WebView 根据 HTTP 响应头判断缓存是否过期:

  • Cache-Control: max-age=3600

    • 缓存有效期 3600 秒(1小时)
    • 计算:当前时间 - 缓存时间 < max-age → 未过期
  • Expires: Wed, 21 Oct 2024 08:00:00 GMT

    • 绝对过期时间
    • 计算:当前时间 < Expires 时间 → 未过期

3. 条件请求机制(验证缓存有效性)

当缓存过期时,WebView 不会直接使用,而是发送条件请求验证:

less 复制代码
缓存已过期
    ↓
发送请求,携带条件头:
  If-None-Match: "abc123"        // ETag 值
  If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
    ↓
服务器检查资源是否变化
    ├─ 未变化 → 返回 304 Not Modified → WebView 使用缓存(更新过期时间)
    └─ 已变化 → 返回 200 + 新内容 → WebView 更新缓存

优点:节省带宽,即使缓存过期,如果内容未变,仍可使用缓存。

三、WebView 缓存模式详解

WebView 提供 4 种缓存模式(通过 webView.settings.cacheMode 设置):

1. LOAD_DEFAULT(默认模式,推荐)

工作原理

markdown 复制代码
请求资源
    ↓
检查 HTTP 缓存头(Cache-Control、Expires)
    ↓
有缓存头且有效? → 是 → 使用缓存(不请求网络)
    ↓ 否
请求网络 → 保存到缓存 → 返回给 WebView

适用场景

  • 大多数 Web 应用
  • 服务器正确设置了缓存头的情况
  • 需要平衡性能和实时性的场景

2. LOAD_CACHE_ELSE_NETWORK(缓存优先)

工作原理

markdown 复制代码
请求资源
    ↓
检查本地缓存
    ↓
有缓存? → 是 → 直接使用缓存(即使过期也不请求网络)
    ↓ 否
请求网络 → 保存到缓存 → 返回给 WebView

适用场景

  • 离线优先的应用(新闻阅读、文档查看)
  • 网络不稳定时仍可查看历史内容
  • 内容更新不频繁的场景

注意:即使缓存过期也会使用,可能导致显示旧内容。

3. LOAD_NO_CACHE(不使用缓存)

工作原理

markdown 复制代码
请求资源
    ↓
忽略所有缓存
    ↓
直接请求网络 → 不保存到缓存 → 返回给 WebView

适用场景

  • 需要实时数据的页面(股票行情、实时聊天)
  • 每次都需要最新内容的场景
  • 调试阶段,确保获取最新内容

4. LOAD_CACHE_ONLY(仅使用缓存)

工作原理

markdown 复制代码
请求资源
    ↓
检查本地缓存
    ↓
有缓存? → 是 → 使用缓存
    ↓ 否
返回空白页面(不请求网络)

适用场景

  • 完全离线模式
  • 已预加载所有内容的场景
  • 不依赖网络的离线应用

四、HTTP 缓存头详解

1. Cache-Control(优先级最高)

arduino 复制代码
Cache-Control: max-age=3600          // 缓存有效期 3600 秒
Cache-Control: no-cache              // 每次都要验证缓存(发送条件请求)
Cache-Control: no-store              // 不存储缓存
Cache-Control: must-revalidate       // 缓存过期后必须验证
Cache-Control: public                // 可以被任何缓存存储
Cache-Control: private               // 只能被浏览器缓存,不能被 CDN 缓存

2. ETag / If-None-Match(内容指纹验证)

工作原理

sql 复制代码
第一次请求:
  服务器返回:ETag: "abc123"(资源的唯一标识,通常是内容哈希)
  WebView 保存 ETag 和内容

第二次请求(缓存过期):
  WebView 发送:If-None-Match: "abc123"
  服务器比较:
    - 资源未变 → 返回 304 Not Modified(不返回内容,节省带宽)
    - 资源已变 → 返回 200 + 新内容 + 新 ETag

优点:即使缓存过期,如果内容未变,仍可使用缓存,节省带宽。

3. Last-Modified / If-Modified-Since(时间戳验证)

工作原理

markdown 复制代码
第一次请求:
  服务器返回:Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
  WebView 保存时间戳和内容

第二次请求(缓存过期):
  WebView 发送:If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
  服务器比较:
    - 资源未变 → 返回 304 Not Modified
    - 资源已变 → 返回 200 + 新内容 + 新 Last-Modified

注意:Last-Modified 精度是秒级,如果资源在 1 秒内多次修改,可能无法检测到变化。

五、缓存的生命周期完整流程

python 复制代码
┌─────────────────────────────────────────────────────────┐
│ 1. 首次请求                                              │
└─────────────────────────────────────────────────────────┘
用户请求 URL
    ↓
WebView 检查缓存目录 → 不存在
    ↓
请求网络
    ↓
服务器返回:
  - 响应内容(HTML/CSS/JS/图片等)
  - 缓存头(Cache-Control、ETag、Last-Modified)
    ↓
WebView 保存到缓存目录:
  - 文件内容
  - 元数据(URL、时间戳、ETag、过期时间等)
    ↓
返回给 WebView 渲染

┌─────────────────────────────────────────────────────────┐
│ 2. 再次请求(缓存未过期)                                │
└─────────────────────────────────────────────────────────┘
用户请求相同 URL
    ↓
WebView 检查缓存目录 → 存在
    ↓
检查过期时间:
  当前时间 - 缓存时间 < max-age → 未过期
    ↓
直接返回缓存(200 from cache)
    ↓
不请求网络,立即显示

┌─────────────────────────────────────────────────────────┐
│ 3. 再次请求(缓存已过期)                                │
└─────────────────────────────────────────────────────────┘
用户请求相同 URL
    ↓
WebView 检查缓存目录 → 存在
    ↓
检查过期时间:
  当前时间 - 缓存时间 >= max-age → 已过期
    ↓
发送条件请求(携带 If-None-Match / If-Modified-Since)
    ↓
服务器验证:
  ├─ 资源未变 → 返回 304 Not Modified
  │     ↓
  │  WebView 使用缓存(更新过期时间)
  │     ↓
  │  返回给 WebView 渲染
  │
  └─ 资源已变 → 返回 200 + 新内容
        ↓
    WebView 更新缓存(覆盖旧内容)
        ↓
    返回给 WebView 渲染

六、缓存存储的物理结构

bash 复制代码
/data/data/包名/cache/webviewCache/
  ├── Cache/
  │   ├── f_000001          # 缓存的资源文件(二进制)
  │   ├── f_000002
  │   └── ...
  ├── Code Cache/           # JavaScript 代码缓存(V8 引擎优化)
  │   └── ...
  └── GPUCache/             # GPU 相关缓存
      └── ...

缓存文件命名

  • WebView 使用内部算法生成文件名(通常是 URL 的哈希值)
  • 无法直接通过文件名识别对应的 URL
  • 元数据存储在 WebView 内部数据库中

七、DOM 存储(LocalStorage / SessionStorage)

LocalStorage

  • 存储位置/data/data/包名/app_webview/Default/Local Storage/
  • 存储方式:键值对,持久化存储
  • 生命周期:除非手动清除,否则永久保存
  • 作用域:同源策略(相同协议、域名、端口)

SessionStorage

  • 存储位置:内存中
  • 生命周期:会话结束(关闭 WebView)后清除
  • 作用域:同源策略

IndexedDB

  • 存储位置/data/data/包名/app_webview/Default/IndexedDB/
  • 存储方式:NoSQL 数据库,支持复杂数据结构
  • 适用场景:存储大量结构化数据

注意:这些存储机制与 HTTP 缓存不同,主要用于存储应用数据,而非网络资源。

8.2.2 WebView 缓存配置实现

Kotlin 实现

kotlin 复制代码
class WebViewCacheManager {
    companion object {
        /**
         * 配置 WebView 缓存
         */
        fun setupCache(webView: WebView, enableCache: Boolean = true) {
            // 使用统一的配置扩展函数
            webView.setupDefaultSettings(enableCache)
            
            // 如果需要自定义缓存策略,可以单独设置
            if (enableCache) {
                webView.settings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
            }
            
            // 注意:setAppCacheEnabled 在 Android 5.0+ 已废弃,不应使用
            // 如需更灵活的缓存控制,推荐使用自定义缓存(见 8.2.3)
        }
        
        /**
         * 清除 WebView 缓存
         */
        fun clearCache(context: Context) {
            try {
                // 清除缓存
                WebView(context).clearCache(true)
                
                // 清除历史记录
                WebView(context).clearHistory()
                
                // 清除 Cookie
                CookieManager.getInstance().apply {
                    removeAllCookies(null)
                    flush()
                }
                
                // 注意:AppCache 已在 Android 5.0+ 废弃,无需清除
            } catch (e: Exception) {
                // 使用统一的错误处理框架(见 4.2.7)
                ErrorHandler.handleError(
                    ErrorHandler.ErrorType.CACHE_ERROR,
                    "Failed to clear WebView cache",
                    e
                )
            }
        }
        
        /**
         * 获取缓存大小
         */
        fun getCacheSize(context: Context): Long {
            val cacheDir = File(context.cacheDir, "webview")
            return if (cacheDir.exists()) {
                cacheDir.walkTopDown().sumOf { it.length() }
            } else {
                0L
            }
        }
    }
}

8.2.3 自定义离线缓存管理器原理

一、为什么需要自定义缓存?

WebView 内置的 HTTP 缓存有以下限制:

  1. 依赖服务器响应头:如果服务器没有设置缓存头,无法缓存
  2. 无法跨域缓存:某些跨域资源无法缓存
  3. 缓存策略固定:无法灵活控制缓存逻辑
  4. 无法加密存储:缓存数据以明文存储
  5. 无法自定义过期时间:只能依赖服务器的 Cache-Control

自定义缓存可以解决这些问题,实现完全可控的缓存策略。

二、自定义缓存的核心原理

自定义缓存通过 shouldInterceptRequest 方法拦截 WebView 的所有网络请求:

csharp 复制代码
WebView 请求资源
    ↓
shouldInterceptRequest 拦截(Android 端)
    ↓
检查自定义缓存(文件系统/数据库)
    ↓
有缓存且有效? → 是 → 构造 WebResourceResponse 返回缓存
    ↓ 否
返回 null → WebView 继续默认流程(HTTP 缓存 → 网络请求)
    ↓
onPageFinished(页面加载完成)
    ↓
获取页面 HTML 内容
    ↓
保存到自定义缓存(文件系统/数据库)

关键点

  • shouldInterceptRequest 在请求发起前拦截,可以返回缓存的响应
  • 如果返回 null,WebView 会继续使用默认的 HTTP 缓存机制
  • onPageFinished 中保存页面内容,实现缓存更新

三、缓存存储架构

1. 文件系统存储(推荐)

bash 复制代码
cacheDir/
  └── offline_cache/
       ├── index.json          # 缓存索引(URL -> 文件路径映射)
       ├── 12345.html          # 缓存的 HTML 文件(URL 哈希值作为文件名)
       ├── 67890.html
       └── ...

索引文件结构

json 复制代码
{
  "https://example.com/page1": {
    "hash": "12345",
    "timestamp": 1698123456789,
    "ttl": 604800000
  },
  "https://example.com/page2": {
    "hash": "67890",
    "timestamp": 1698123456790,
    "ttl": 604800000
  }
}

优点

  • 实现简单,直接使用文件系统
  • 存储效率高,适合大文件
  • 易于调试,可以直接查看文件

缺点

  • 需要手动管理文件
  • 查询效率相对较低(需要遍历索引)

2. 数据库存储

scss 复制代码
SQLite 数据库
  └── cache_table
       ├── url (TEXT PRIMARY KEY)
       ├── content (BLOB)        # 缓存的 HTML 内容
       ├── timestamp (INTEGER)   # 缓存时间戳
       └── ttl (INTEGER)         # 过期时间

优点

  • 查询效率高,支持索引
  • 支持复杂查询(如按时间、大小排序)
  • 事务支持,数据一致性好

缺点

  • 大文件存储效率低(BLOB 字段)
  • 实现相对复杂

3. 混合存储(最佳实践)

bash 复制代码
数据库(元数据)
  └── cache_metadata
       ├── url
       ├── file_path          # 文件路径
       ├── timestamp
       └── ttl

文件系统(内容)
  └── cache_files/
       ├── 12345.html
       └── 67890.html

优点

  • 兼顾查询效率和存储效率
  • 元数据查询快,大文件存储效率高

缺点

  • 实现复杂,需要维护两套存储

四、缓存失效策略详解

1. TTL(Time To Live)时间过期

原理:每个缓存条目设置一个过期时间,超过时间后自动失效。

kotlin 复制代码
// 保存时设置过期时间
val ttl = 7 * 24 * 60 * 60 * 1000L  // 7 天
val timestamp = System.currentTimeMillis()

// 检查时判断是否过期
val age = System.currentTimeMillis() - timestamp
if (age > ttl) {
    // 缓存已过期,删除
    clearCache(url)
}

适用场景

  • 新闻、文章等有时效性的内容
  • 需要定期更新的数据

2. LRU(Least Recently Used)最近最少使用

原理:维护每个缓存条目的访问时间,当缓存空间不足时,删除最久未访问的缓存。

kotlin 复制代码
// 访问时更新访问时间
fun loadPage(url: String): String? {
    val entry = getCacheEntry(url) ?: return null
    updateAccessTime(url)  // 更新访问时间
    return readFile(entry.filePath)
}

// 清理时按访问时间排序
fun evictCache() {
    val entries = getAllCacheEntries()
        .sortedBy { it.accessTime }  // 按访问时间升序排序
    
    // 删除最久未访问的
    entries.take(entries.size - maxSize).forEach {
        clearCache(it.url)
    }
}

适用场景

  • 缓存空间有限,需要自动清理
  • 希望保留最常用的缓存

3. 版本控制

原理:通过版本号管理缓存,版本更新时清除旧缓存。

kotlin 复制代码
// 保存时记录版本
val cacheVersion = "v1.0"
savePage(url, html, version = cacheVersion)

// 检查时比较版本
fun isCacheValid(url: String): Boolean {
    val entry = getCacheEntry(url) ?: return false
    return entry.version == currentVersion
}

适用场景

  • 应用版本更新,需要清除旧缓存
  • 数据结构变更,需要重新缓存

4. 大小限制

原理:限制缓存总大小,超过限制时删除最旧的缓存。

kotlin 复制代码
fun checkCacheSize() {
    var totalSize = getTotalCacheSize()
    if (totalSize > maxCacheSize) {
        // 按时间排序,删除最旧的
        val entries = getAllCacheEntries()
            .sortedBy { it.timestamp }
        
        for (entry in entries) {
            if (totalSize <= maxCacheSize) break
            totalSize -= entry.size
            clearCache(entry.url)
        }
    }
}

适用场景

  • 需要控制缓存占用的存储空间
  • 防止缓存无限增长

五、缓存更新策略

1. 后台更新(推荐)

原理:立即返回缓存给用户,同时在后台更新缓存。

kotlin 复制代码
override fun shouldInterceptRequest(
    view: WebView?,
    request: WebResourceRequest?
): WebResourceResponse? {
    val url = request?.url?.toString() ?: return null
    
    // 立即返回缓存
    val cached = cacheManager.loadPage(url)
    if (cached != null) {
        // 后台更新(异步)
        backgroundUpdate(url)
        return WebResourceResponse("text/html", "utf-8", cached.byteInputStream())
    }
    
    return null
}

private fun backgroundUpdate(url: String) {
    // 在后台线程更新缓存
    thread {
        // 请求最新内容
        val newContent = fetchFromNetwork(url)
        cacheManager.savePage(url, newContent)
    }
}

优点

  • 用户体验好,立即响应
  • 后台自动更新,保持数据新鲜

缺点

  • 首次可能返回旧数据
  • 需要额外的网络请求

2. 网络优先

原理:优先请求网络,失败时才使用缓存。

kotlin 复制代码
override fun shouldInterceptRequest(
    view: WebView?,
    request: WebResourceRequest?
): WebResourceResponse? {
    val url = request?.url?.toString() ?: return null
    
    // 先尝试网络请求
    try {
        val networkResponse = fetchFromNetwork(url)
        // 成功则更新缓存
        cacheManager.savePage(url, networkResponse)
        return WebResourceResponse("text/html", "utf-8", networkResponse.byteInputStream())
    } catch (e: Exception) {
        // 失败则使用缓存
        val cached = cacheManager.loadPage(url)
        return cached?.let {
            WebResourceResponse("text/html", "utf-8", it.byteInputStream())
        }
    }
}

优点

  • 数据最新
  • 离线时仍可使用缓存

缺点

  • 每次都需要网络请求,速度较慢

3. 缓存优先

原理:优先使用缓存,缓存不存在时才请求网络。

kotlin 复制代码
override fun shouldInterceptRequest(
    view: WebView?,
    request: WebResourceRequest?
): WebResourceResponse? {
    val url = request?.url?.toString() ?: return null
    
    // 先检查缓存
    val cached = cacheManager.loadPage(url)
    if (cached != null) {
        return WebResourceResponse("text/html", "utf-8", cached.byteInputStream())
    }
    
    // 缓存不存在,返回 null 让 WebView 请求网络
    return null
}

优点

  • 离线可用
  • 加载速度快

缺点

  • 可能返回过期数据
  • 需要手动触发更新

8.2.4 自定义离线缓存实现(简化版)

Kotlin 实现

kotlin 复制代码
/**
 * 简化的离线缓存管理器
 * 核心功能:保存、加载、检查、清除缓存
 */
class OfflineCacheManager(private val context: Context) {
    private val cacheDir = File(context.cacheDir, "offline_cache")
    private val indexFile = File(cacheDir, "index.json")
    private val maxCacheSize = 50 * 1024 * 1024L // 50MB
    private val defaultTTL = 7 * 24 * 60 * 60 * 1000L // 默认 7 天过期
    
    // 缓存索引:URL -> CacheEntry
    private data class CacheEntry(
        val hash: String,           // 文件哈希(用于文件名)
        val timestamp: Long,        // 缓存时间戳
        val size: Long,            // 缓存大小
        val ttl: Long = defaultTTL // 生存时间
    )
    
    init {
        if (!cacheDir.exists()) {
            cacheDir.mkdirs()
        }
        if (!indexFile.exists()) {
            indexFile.writeText("{}")
        }
    }
    
    /**
     * 保存页面到缓存
     * 
     * 原理:
     * 1. 将 URL 转换为哈希值作为文件名(避免特殊字符问题)
     * 2. 保存 HTML 内容到文件
     * 3. 保存关联资源(CSS、JS、图片等)到子目录
     * 4. 更新索引文件,记录 URL、时间戳、大小等信息
     * 5. 检查缓存大小,如果超限则删除最旧的缓存(LRU)
     */
    fun savePage(
        url: String, 
        html: String, 
        resources: Map<String, ByteArray> = emptyMap(),
        ttl: Long = defaultTTL
    ) {
        try {
            // 1. 生成 URL 哈希(使用 MD5 或简单哈希)
            val urlHash = url.hashCode().toString()
            val pageFile = File(cacheDir, "$urlHash.html")
            val resourcesDir = File(cacheDir, "${urlHash}_resources")
            
            // 2. 保存 HTML 内容
            pageFile.writeText(html, Charsets.UTF_8)
            
            // 3. 保存关联资源(CSS、JS、图片等)
            if (resources.isNotEmpty()) {
                if (!resourcesDir.exists()) {
                    resourcesDir.mkdirs()
                }
                resources.forEach { (name, data) ->
                    // 清理文件名,避免路径遍历攻击
                    val safeName = name.replace(Regex("[^a-zA-Z0-9._-]"), "_")
                    File(resourcesDir, safeName).writeBytes(data)
                }
            }
            
            // 4. 计算缓存大小
            val cacheSize = pageFile.length() + 
                           resourcesDir.walkTopDown().sumOf { it.length() }
            
            // 5. 更新缓存索引
            updateCacheIndex(url, CacheEntry(
                hash = urlHash,
                timestamp = System.currentTimeMillis(),
                size = cacheSize,
                ttl = ttl
            ))
            
            // 6. 检查并清理过期缓存
            cleanupExpiredCache()
            
            // 7. 检查缓存大小,如果超限则删除最旧的(LRU)
            checkAndEvictCache()
        } catch (e: Exception) {
            // 使用统一的错误处理框架(见 4.2.7)
            ErrorHandler.handleError(
                ErrorHandler.ErrorType.CACHE_ERROR,
                "Failed to save page to cache: $url",
                e
            )
        }
    }
    
    /**
     * 从缓存加载页面
     * 
     * 原理:
     * 1. 从索引文件查找 URL 对应的哈希值
     * 2. 检查缓存是否过期(TTL)
     * 3. 如果有效,读取 HTML 文件内容
     * 4. 更新访问时间(用于 LRU)
     */
    fun loadPage(url: String): String? {
        return try {
            val entry = getCacheEntry(url) ?: return null
            
            // 检查是否过期
            if (isExpired(entry)) {
                clearCache(url)
                return null
            }
            
            val pageFile = File(cacheDir, "${entry.hash}.html")
            if (pageFile.exists()) {
                // 更新访问时间(用于 LRU)
                updateAccessTime(url)
                pageFile.readText(Charsets.UTF_8)
            } else {
                null
            }
        } catch (e: Exception) {
            // 使用统一的错误处理框架(见 4.2.7)
            ErrorHandler.handleError(
                ErrorHandler.ErrorType.CACHE_ERROR,
                "Failed to load page from cache: $url",
                e
            )
            null
        }
    }
    
    /**
     * 检查页面是否已缓存且有效
     * 
     * 原理:
     * 1. 检查索引中是否存在该 URL
     * 2. 检查缓存是否过期
     * 3. 检查文件是否存在
     */
    fun isCached(url: String): Boolean {
        val entry = getCacheEntry(url) ?: return false
        if (isExpired(entry)) {
            clearCache(url)
            return false
        }
        val pageFile = File(cacheDir, "${entry.hash}.html")
        return pageFile.exists()
    }
    
    /**
     * 检查缓存是否过期
     */
    private fun isExpired(entry: CacheEntry): Boolean {
        val age = System.currentTimeMillis() - entry.timestamp
        return age > entry.ttl
    }
    
    /**
     * 清除指定 URL 的缓存
     */
    fun clearCache(url: String) {
        try {
            val urlHash = getUrlHash(url) ?: return
            File(cacheDir, "$urlHash.html").delete()
            File(cacheDir, "${urlHash}_resources").deleteRecursively()
            removeFromCacheIndex(url)
        } catch (e: Exception) {
            // 使用统一的错误处理框架(见 4.2.7)
            ErrorHandler.handleError(
                ErrorHandler.ErrorType.CACHE_ERROR,
                "Failed to clear cache: $url",
                e
            )
        }
    }
    
    /**
     * 清除所有缓存
     */
    fun clearAllCache() {
        try {
            cacheDir.deleteRecursively()
            cacheDir.mkdirs()
        } catch (e: Exception) {
            // 使用统一的错误处理框架(见 4.2.7)
            ErrorHandler.handleError(
                ErrorHandler.ErrorType.CACHE_ERROR,
                "Failed to clear all cache",
                e
            )
        }
    }
    
    /**
     * 获取缓存大小
     */
    fun getCacheSize(): Long {
        return if (cacheDir.exists()) {
            cacheDir.walkTopDown().sumOf { it.length() }
        } else {
            0L
        }
    }
    
    /**
     * 更新缓存索引
     * 
     * 原理:
     * 索引文件结构:
     * {
     *   "url1": {"hash": "123", "timestamp": 1234567890, "size": 1024, "ttl": 604800000},
     *   "url2": {"hash": "456", "timestamp": 1234567891, "size": 2048, "ttl": 604800000}
     * }
     */
    private fun updateCacheIndex(url: String, entry: CacheEntry) {
        try {
            val index = loadIndex()
            val entryJson = JSONObject().apply {
                put("hash", entry.hash)
                put("timestamp", entry.timestamp)
                put("size", entry.size)
                put("ttl", entry.ttl)
                put("accessTime", System.currentTimeMillis()) // 用于 LRU
            }
            index.put(url, entryJson)
            saveIndex(index)
        } catch (e: Exception) {
            // 使用统一的错误处理框架(见 4.2.7)
            ErrorHandler.handleError(
                ErrorHandler.ErrorType.CACHE_ERROR,
                "Failed to update cache index: $url",
                e
            )
        }
    }
    
    /**
     * 更新访问时间(用于 LRU 算法)
     */
    private fun updateAccessTime(url: String) {
        try {
            val index = loadIndex()
            val entryJson = index.optJSONObject(url) ?: return
            entryJson.put("accessTime", System.currentTimeMillis())
            index.put(url, entryJson)
            saveIndex(index)
        } catch (e: Exception) {
            // 使用统一的错误处理框架(见 4.2.7)
            ErrorHandler.handleError(
                ErrorHandler.ErrorType.CACHE_ERROR,
                "Failed to update access time: $url",
                e
            )
        }
    }
    
    /**
     * 获取缓存条目
     */
    private fun getCacheEntry(url: String): CacheEntry? {
        return try {
            val index = loadIndex()
            val entryJson = index.optJSONObject(url) ?: return null
            CacheEntry(
                hash = entryJson.getString("hash"),
                timestamp = entryJson.getLong("timestamp"),
                size = entryJson.getLong("size"),
                ttl = entryJson.optLong("ttl", defaultTTL)
            )
        } catch (e: Exception) {
            null
        }
    }
    
    /**
     * 加载索引文件
     */
    private fun loadIndex(): JSONObject {
        return if (indexFile.exists()) {
            try {
                JSONObject(indexFile.readText())
            } catch (e: Exception) {
                JSONObject()
            }
        } else {
            JSONObject()
        }
    }
    
    /**
     * 保存索引文件
     */
    private fun saveIndex(index: JSONObject) {
        indexFile.writeText(index.toString())
    }
    
    /**
     * 清理过期缓存
     * 
     * 原理:遍历所有缓存条目,删除已过期的
     */
    private fun cleanupExpiredCache() {
        try {
            val index = loadIndex()
            val keys = index.keys()
            val expiredUrls = mutableListOf<String>()
            
            while (keys.hasNext()) {
                val url = keys.next()
                val entry = getCacheEntry(url) ?: continue
                if (isExpired(entry)) {
                    expiredUrls.add(url)
                }
            }
            
            expiredUrls.forEach { url ->
                clearCache(url)
            }
        } catch (e: Exception) {
            // 使用统一的错误处理框架(见 4.2.7)
            ErrorHandler.handleError(
                ErrorHandler.ErrorType.CACHE_ERROR,
                "Failed to cleanup expired cache",
                e
            )
        }
    }
    
    /**
     * 检查并清理缓存(LRU 策略)
     * 
     * 原理:
     * 1. 计算当前缓存总大小
     * 2. 如果超过限制,按访问时间排序(LRU)
     * 3. 删除最久未访问的缓存,直到满足大小限制
     */
    private fun checkAndEvictCache() {
        try {
            var currentSize = getCacheSize()
            if (currentSize <= maxCacheSize) {
                return
            }
            
            // 按访问时间排序(LRU:最久未访问的优先删除)
            val index = loadIndex()
            val entries = mutableListOf<Pair<String, Long>>()
            
            index.keys().forEach { url ->
                val entryJson = index.optJSONObject(url) ?: return@forEach
                val accessTime = entryJson.optLong("accessTime", entryJson.getLong("timestamp"))
                entries.add(Pair(url, accessTime))
            }
            
            // 按访问时间升序排序(最旧的在前)
            entries.sortBy { it.second }
            
            // 删除最旧的缓存,直到满足大小限制
            for ((url, _) in entries) {
                if (currentSize <= maxCacheSize) {
                    break
                }
                val entry = getCacheEntry(url) ?: continue
                currentSize -= entry.size
                clearCache(url)
            }
        } catch (e: Exception) {
            // 使用统一的错误处理框架(见 4.2.7)
            ErrorHandler.handleError(
                ErrorHandler.ErrorType.CACHE_ERROR,
                "Failed to evict cache",
                e
            )
        }
    }
}

8.2.5 离线缓存完整使用示例

一、缓存策略选择指南

场景 推荐策略 原因
新闻阅读 LOAD_CACHE_ELSE_NETWORK + 自定义缓存(后台更新) 离线可读,后台更新
实时数据 LOAD_NO_CACHE 需要最新数据
静态文档 LOAD_CACHE_ELSE_NETWORK 内容变化少
完全离线 LOAD_CACHE_ONLY + 自定义缓存 不依赖网络
混合应用 LOAD_DEFAULT + 自定义缓存(网络优先) 平衡性能和实时性

二、完整实现示例

Kotlin 实现

kotlin 复制代码
/**
 * 支持离线缓存的 WebViewClient(简化版)
 */
class CachedWebViewClient(
    private val cacheManager: OfflineCacheManager
) : WebViewClient() {
    
    override fun shouldInterceptRequest(
        view: WebView?,
        request: WebResourceRequest?
    ): WebResourceResponse? {
        val url = request?.url?.toString() ?: return null
        val cachedHtml = cacheManager.loadPage(url)
        return if (cachedHtml != null) {
            WebResourceResponse("text/html", "utf-8", cachedHtml.byteInputStream())
        } else {
            super.shouldInterceptRequest(view, request)
        }
    }
    
    override fun onPageFinished(view: WebView?, url: String?) {
        super.onPageFinished(view, url)
        url?.let {
            view?.evaluateJavaScriptSafe("document.documentElement.outerHTML") { html ->
                JSResultParser.parseResult(html)?.let { cleaned ->
                    cacheManager.savePage(url, cleaned)
                }
            }
        }
    }
}

// 使用示例
class MainActivity : AppCompatActivity() {
    private lateinit var webView: WebView
    private lateinit var cacheManager: OfflineCacheManager
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        cacheManager = OfflineCacheManager(this)
        webView = findViewById(R.id.webview)
        webView.setupDefaultSettings(enableCache = true)
        webView.webViewClient = CachedWebViewClient(cacheManager)
        webView.loadUrl("https://example.com")
    }
}

8.3 批量通信优化

原理:频繁调用 JavaScript 会导致性能问题,通过批量合并调用可以减少通信次数,提升性能。

依赖说明

kotlin 复制代码
import android.webkit.WebView
import android.webkit.ValueCallback
import android.os.Build
import android.os.Handler
import android.os.Looper

Kotlin 实现

kotlin 复制代码
/**
 * 批量 JavaScript 执行器
 * 
 * 工作原理:
 * 1. 收集待执行的 JavaScript 代码
 * 2. 延迟执行(100ms 内合并)
 * 3. 批量执行,减少通信次数
 */
class BatchJSExecutor(private val webView: WebView) {
    private val pendingCalls = mutableListOf<String>()
    private val handler = Handler(Looper.getMainLooper())
    private var batchRunnable: Runnable? = null
    private val batchDelay = 100L // 100ms 内合并
    
    fun addCall(jsCode: String) {
        synchronized(pendingCalls) { pendingCalls.add(jsCode) }
        scheduleBatch()
    }
    
    fun flush() {
        handler.removeCallbacks(batchRunnable ?: return)
        batchRunnable?.run()
    }
    
    private fun scheduleBatch() {
        batchRunnable?.let { handler.removeCallbacks(it) }
        batchRunnable = Runnable {
            synchronized(pendingCalls) {
                if (pendingCalls.isNotEmpty()) {
                    webView.evaluateJavaScriptSafe(pendingCalls.joinToString(";"))
                    pendingCalls.clear()
                }
            }
        }
        handler.postDelayed(batchRunnable!!, batchDelay)
    }
    
    fun cleanup() {
        handler.removeCallbacks(batchRunnable ?: return)
        synchronized(pendingCalls) { pendingCalls.clear() }
    }
}

// 使用示例
val batchExecutor = BatchJSExecutor(webView)

// 添加多个调用
batchExecutor.addCall("console.log('1')")
batchExecutor.addCall("console.log('2')")
batchExecutor.addCall("console.log('3')")
// 100ms 后会自动批量执行

// 立即执行
batchExecutor.flush()

九、架构设计建议

9.1 WebView 架构设计

一、单一 WebView 架构

适用于:简单的 H5 页面展示

scss 复制代码
Activity
  └── WebView
       └── WebViewClient (处理页面加载)
       └── WebChromeClient (处理 JS 交互)
       └── JSInterface (Bridge 接口)

依赖说明

kotlin 复制代码
import android.webkit.WebView
import android.webkit.WebViewClient
import android.webkit.WebChromeClient
import android.webkit.JavascriptInterface
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

Kotlin 实现

kotlin 复制代码
class SimpleWebViewActivity : AppCompatActivity() {
    private lateinit var webView: WebView
    private lateinit var bridge: WebViewBridgeKt
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        webView = WebView(this)
        // WebViewBridgeKt 会自动配置 WebView
        bridge = WebViewBridgeKt(webView)
        
        // 注册处理器
        bridge.registerHandler("showToast") { data, callback ->
            Toast.makeText(this, data, Toast.LENGTH_SHORT).show()
            callback?.invoke("success")
        }
        
        setContentView(webView)
        webView.loadUrl("https://example.com")
    }
}

二、多 WebView 管理架构

适用于:需要管理多个 WebView 的场景(如多标签页、多页面栈)

scss 复制代码
WebViewManager (单例)
  ├── WebViewPool (WebView 池)
  ├── WebViewRegistry (WebView 注册表)
  └── LifecycleManager (生命周期管理)
       ├── WebView 1
       ├── WebView 2
       └── WebView 3

Kotlin 实现

kotlin 复制代码
/**
 * WebView 管理器(简化版)
 */
class WebViewManager(private val context: Context) {
    private val webViewPool = WebViewPool(context)
    private val activeWebViews = mutableMapOf<String, WebView>()
    
    fun getWebView(id: String): WebView {
        return activeWebViews.getOrPut(id) { webViewPool.obtain() }
    }
    
    fun destroyWebView(id: String) {
        activeWebViews.remove(id)?.let { webViewPool.recycle(it) }
    }
    
    fun clearAll() {
        activeWebViews.values.forEach { webViewPool.recycle(it) }
        activeWebViews.clear()
    }
}

三、模块化架构

适用于:大型项目,需要模块化管理

scss 复制代码
App
  └── WebViewModule
       ├── WebViewManager (WebView 管理)
       ├── BridgeManager (Bridge 管理)
       ├── CacheManager (缓存管理)
       ├── PreloadManager (预加载管理)
       └── ErrorHandler (错误处理)

9.2 多 WebView 场景管理

场景:多标签页浏览器、多页面栈、Fragment 中的 WebView

Kotlin 实现

kotlin 复制代码
/**
 * 多 WebView 场景管理器(简化版)
 */
class MultiWebViewManager(private val context: Context) {
    private val webViews = mutableMapOf<String, WebView>()
    
    fun createWebView(id: String): WebView {
        return WebView(context.applicationContext).apply {
            setupDefaultSettings()
            webViews[id] = this
        }
    }
    
    fun getWebView(id: String): WebView? = webViews[id]
    
    fun switchWebView(fromId: String, toId: String) {
        webViews[fromId]?.onPause()
        webViews[toId]?.onResume()
    }
    
    fun destroyWebView(id: String) {
        webViews.remove(id)?.destroy()
    }
    
    fun clearAll() {
        webViews.values.forEach { it.destroy() }
        webViews.clear()
    }
}

十、性能监控

10.1 性能指标收集

原理:收集 WebView 性能指标,帮助优化应用性能。

依赖说明

kotlin 复制代码
import android.webkit.WebView
import android.webkit.WebResourceRequest
import android.webkit.WebResourceError
import android.webkit.WebResourceResponse
import android.graphics.Bitmap
import android.util.Log

Kotlin 实现

kotlin 复制代码
/**
 * WebView 性能监控器
 */
class WebViewPerformanceMonitor(private val webView: WebView) {
    private val metrics = mutableListOf<PerformanceMetric>()
    
    data class PerformanceMetric(
        val type: MetricType,
        val value: Long,
        val timestamp: Long = System.currentTimeMillis(),
        val context: Map<String, Any>? = null
    )
    
    enum class MetricType {
        PAGE_LOAD_TIME,      // 页面加载时间
        JS_EXECUTION_TIME,   // JS 执行时间
        BRIDGE_CALL_TIME,    // Bridge 调用时间
        MEMORY_USAGE,        // 内存使用
        NETWORK_REQUEST_TIME // 网络请求时间
    }
    
    /**
     * 监控页面加载时间
     */
    fun monitorPageLoad() {
        var startTime = 0L
        
        webView.webViewClient = object : WebViewClient() {
            override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                super.onPageStarted(view, url, favicon)
                startTime = System.currentTimeMillis()
            }
            
            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                val loadTime = System.currentTimeMillis() - startTime
                recordMetric(MetricType.PAGE_LOAD_TIME, loadTime, mapOf("url" to (url ?: "")))
            }
        }
    }
    
    /**
     * 监控 Bridge 调用时间
     */
    fun monitorBridgeCall(handlerName: String, block: () -> Unit) {
        val startTime = System.currentTimeMillis()
        try {
            block()
        } finally {
            val duration = System.currentTimeMillis() - startTime
            recordMetric(MetricType.BRIDGE_CALL_TIME, duration, mapOf("handler" to handlerName))
        }
    }
    
    /**
     * 记录指标
     */
    private fun recordMetric(type: MetricType, value: Long, context: Map<String, Any>? = null) {
        metrics.add(PerformanceMetric(type, value, context = context))
        
        // 如果指标过多,只保留最近 1000 条
        if (metrics.size > 1000) {
            metrics.removeAt(0)
        }
    }
    
    /**
     * 获取性能报告
     */
    fun getPerformanceReport(): PerformanceReport {
        val pageLoadTimes = metrics.filter { it.type == MetricType.PAGE_LOAD_TIME }
        val bridgeCallTimes = metrics.filter { it.type == MetricType.BRIDGE_CALL_TIME }
        
        return PerformanceReport(
            avgPageLoadTime = pageLoadTimes.map { it.value }.average().toLong(),
            avgBridgeCallTime = bridgeCallTimes.map { it.value }.average().toLong(),
            totalMetrics = metrics.size
        )
    }
    
    data class PerformanceReport(
        val avgPageLoadTime: Long,
        val avgBridgeCallTime: Long,
        val totalMetrics: Int
    )
}

// ========== 使用示例 ==========
class MainActivity : AppCompatActivity() {
    private lateinit var webView: WebView
    private lateinit var performanceMonitor: WebViewPerformanceMonitor
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        webView = findViewById(R.id.webview)
        
        // 初始化性能监控
        performanceMonitor = WebViewPerformanceMonitor(webView)
        performanceMonitor.monitorPageLoad()
        
        // 监控 Bridge 调用
        val bridge = WebViewBridgeKt(webView)
        bridge.registerHandler("test") { data, callback ->
            performanceMonitor.monitorBridgeCall("test") {
                // 处理逻辑
                callback?.invoke("success")
            }
        }
        
        webView.loadUrl("https://example.com")
    }
    
    override fun onDestroy() {
        super.onDestroy()
        // 获取性能报告
        val report = performanceMonitor.getPerformanceReport()
        // 性能日志,生产环境建议上报到服务器
        if (BuildConfig.DEBUG) {
            Log.d("Performance", "平均页面加载时间: ${report.avgPageLoadTime}ms")
            Log.d("Performance", "平均 Bridge 调用时间: ${report.avgBridgeCallTime}ms")
        }
    }
}

10.2 内存监控

Kotlin 实现

kotlin 复制代码
/**
 * WebView 内存监控(简化版)
 */
object WebViewMemoryMonitor {
    fun getMemoryInfo(): MemoryInfo {
        val runtime = Runtime.getRuntime()
        val usedMemory = runtime.totalMemory() - runtime.freeMemory()
        return MemoryInfo(
            usedMemoryMB = usedMemory / 1024.0 / 1024.0,
            maxMemoryMB = runtime.maxMemory() / 1024.0 / 1024.0
        )
    }
    
    data class MemoryInfo(
        val usedMemoryMB: Double,
        val maxMemoryMB: Double
    )
    
    fun isMemorySufficient(requiredMB: Int): Boolean {
        val info = getMemoryInfo()
        return (info.maxMemoryMB - info.usedMemoryMB) >= requiredMB
    }
}

十一、快速参考

11.1 API 速查表

JavaScript 调用 Android

方法 版本要求 特点 推荐度
addJavascriptInterface Android 4.2+ 最简单直接 ⭐⭐⭐⭐⭐
shouldOverrideUrlLoading 所有版本 安全性高 ⭐⭐⭐⭐
onJsPrompt 所有版本 可返回值 ⭐⭐⭐⭐⭐
onJsAlert 所有版本 简单但功能有限 ⭐⭐
onJsConfirm 所有版本 需要用户确认 ⭐⭐
postMessage Android 6.0+ 官方推荐 ⭐⭐⭐⭐

Android 调用 JavaScript

方法 版本要求 特点 推荐度
evaluateJavascript Android 4.4+ 支持返回值 ⭐⭐⭐⭐⭐
loadUrl 所有版本 兼容性好 ⭐⭐⭐
相关推荐
GIS之路2 小时前
GDAL 实现矢量数据转换处理(全)
前端
大厂技术总监下海3 小时前
Rust的“一发逆转弹”:Dioxus 如何用一套代码横扫 Web、桌面、移动与后端?
前端·rust·开源
加洛斯3 小时前
SpringSecurity入门篇(2):替换登录页与config配置
前端·后端
用户904706683573 小时前
Nuxt详解 —— 设置seo以及元数据
前端
DarkLONGLOVE3 小时前
Vue组件使用三步走:创建、注册、使用(Vue2/Vue3双版本详解)
前端·javascript·vue.js
DarkLONGLOVE3 小时前
手把手教你玩转Vue组件:创建、注册、使用三步曲!
前端·javascript·vue.js
龙之叶3 小时前
【Android Monkey源码解析三】- 运行解析
android
李剑一3 小时前
uni-app实现leaflet地图图标旋转
前端·trae
风度前端4 小时前
npm 2026安全新规下的免登录发包策略
前端