一、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 版本(配合 evaluateJavascript 或 loadUrl)
特点:
- ✅ 可以实现异步通信
- ✅ 可以获取返回值
- ✅ 兼容性好
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)
GitHub :github.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
GitHub :github.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("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
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 缓存有以下限制:
- 依赖服务器响应头:如果服务器没有设置缓存头,无法缓存
- 无法跨域缓存:某些跨域资源无法缓存
- 缓存策略固定:无法灵活控制缓存逻辑
- 无法加密存储:缓存数据以明文存储
- 无法自定义过期时间:只能依赖服务器的 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 |
所有版本 | 兼容性好 | ⭐⭐⭐ |