Android WebView 混合开发完整指南

16 阅读27分钟

一、JavaScript 调用 Android 的所有方式

1.1 addJavascriptInterface 方法(最常用)

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

版本要求

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

特点

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

Android 端代码

// 创建接口类
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 端代码

// 调用方法
Android.showToast('Hello from JavaScript');
var deviceInfo = Android.getDeviceInfo();

1.2 shouldOverrideUrlLoading 拦截(URL Scheme)

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

版本要求:所有 Android 版本

特点

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

Android 端代码

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 端代码

// 使用 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 端代码

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 端代码

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 端代码

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 端代码

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

1.5 onJsConfirm 方法

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

版本要求:所有 Android 版本

特点

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

Android 端代码

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 端代码

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

1.6 onConsoleMessage 方法

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

版本要求:所有 Android 版本

特点

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

Android 端代码

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 端代码

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 端代码

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 端代码

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 开源库实现方式


二、Android 调用 JavaScript 的所有方式

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

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

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

特点

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

Android 端代码

// 调用无返回值方法
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 端代码

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 };
}

返回值处理

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 端代码

// 调用 JavaScript 方法
webView.loadUrl("javascript:showMessage('Hello')");

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

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 端代码

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 端代码

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

2.4 通过 JSBridge 库调用

详见 三、JSBridge 开源库实现方式


2.5 通过注入 JavaScript 代码

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

版本要求:所有 Android 版本

特点

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

Android 端代码

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 端代码

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 启发
  • 支持双向通信
  • 支持持久回调
  • 提供默认处理器

依赖

implementation 'com.github.lzyzsd:jsbridge:1.0.4'

Android 端代码

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 端代码

// 连接 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 命名空间
  • 支持调试模式

依赖

implementation 'com.github.wendux:DSBridge-Android:3.0.0'

Android 端代码

// 定义 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 端代码

// 同步调用
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 端代码

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 安全配置

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 参数验证

@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 权限控制

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 版本兼容性处理

// 检查 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 实现

@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 统一错误处理框架

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

依赖说明

import android.util.Log
import org.json.JSONObject

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 实现

// 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 不一致

解决方案

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 会失败。

解决方案

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 工具类解析返回值

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

注意

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

5.4 特殊字符转义问题

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

解决方案

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 方法会导致崩溃。

解决方案

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 缓存导致页面不更新。

解决方案

// 使用统一的配置扩展函数
// 开发环境:不使用缓存
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 中已有同名对象,会导致冲突。

解决方案

// 使用唯一的前缀
webView.addJavascriptInterface(JSInterface(), "MyAppAndroid")

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

6.2 异步回调时序问题

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

解决方案

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 可能被其他应用拦截。

解决方案

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(),自动处理版本兼容性。

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

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

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

解决方案

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.116addJavascriptInterface 存在安全漏洞必须使用 URL Scheme 或其他方式
Android 4.217@JavascriptInterface 注解必需必须添加注解才能使用
Android 4.419evaluateJavascript 可用推荐使用,支持返回值
Android 5.021shouldOverrideUrlLoading 方法变更需要重写新方法
Android 6.023postMessage 支持官方推荐的安全通信方式
Android 7.024文件访问限制更严格注意文件访问权限
Android 8.026WebView 多进程支持注意进程间通信

八、性能优化建议

8.1 资源预加载策略

WebView 池(预加载优化基础)

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 实现

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 实现

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 实现

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. 缓存判断流程

用户请求资源(如 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 不会直接使用,而是发送条件请求验证:

缓存已过期
    ↓
发送请求,携带条件头:
  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(默认模式,推荐)

工作原理

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

适用场景

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

2. LOAD_CACHE_ELSE_NETWORK(缓存优先)

工作原理

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

适用场景

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

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

3. LOAD_NO_CACHE(不使用缓存)

工作原理

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

适用场景

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

4. LOAD_CACHE_ONLY(仅使用缓存)

工作原理

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

适用场景

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

四、HTTP 缓存头详解

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

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(内容指纹验证)

工作原理

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

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

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

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

工作原理

第一次请求:
  服务器返回: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 秒内多次修改,可能无法检测到变化。

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

┌─────────────────────────────────────────────────────────┐
│ 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 渲染

六、缓存存储的物理结构

/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 实现

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 的所有网络请求:

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

关键点

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

三、缓存存储架构

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

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

索引文件结构

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

优点

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

缺点

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

2. 数据库存储

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

优点

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

缺点

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

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

数据库(元数据)
  └── cache_metadata
       ├── url
       ├── file_path          # 文件路径
       ├── timestamp
       └── ttl

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

优点

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

缺点

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

四、缓存失效策略详解

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

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

// 保存时设置过期时间
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)最近最少使用

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

// 访问时更新访问时间
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. 版本控制

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

// 保存时记录版本
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. 大小限制

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

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. 后台更新(推荐)

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

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. 网络优先

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

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. 缓存优先

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

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 实现

/**
 * 简化的离线缓存管理器
 * 核心功能:保存、加载、检查、清除缓存
 */
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 实现

/**
 * 支持离线缓存的 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 会导致性能问题,通过批量合并调用可以减少通信次数,提升性能。

依赖说明

import android.webkit.WebView
import android.webkit.ValueCallback
import android.os.Build
import android.os.Handler
import android.os.Looper

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 页面展示

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

依赖说明

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 实现

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 的场景(如多标签页、多页面栈)

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

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()
    }
}

三、模块化架构

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

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

9.2 多 WebView 场景管理

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

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 性能指标,帮助优化应用性能。

依赖说明

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 实现

/**
 * 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 实现

/**
 * 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

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

Android 调用 JavaScript

方法版本要求特点推荐度
evaluateJavascriptAndroid 4.4+支持返回值⭐⭐⭐⭐⭐
loadUrl所有版本兼容性好⭐⭐⭐