文章

WebView 与 H5 交互深度解析——跨平台开发者的原生与 Web 通信全攻略

面试高频:WebView 的 @JavascriptInterface 安全风险、JSBridge 双向通信、Cookie 同步、混合内容策略与内存泄漏治理。

WebView 与 H5 交互深度解析——跨平台开发者的原生与 Web 通信全攻略

一句话概括

WebView 是原生里内嵌 H5 的官方容器,本质是套在应用里的 Chromium 内核。它远不止”应用里开个网页”——JS 调原生、原生调 JS、Cookie 同步、URL 拦截、混合内容策略,全是面试和线上事故的高发区。

核心结论:JSBridge 只有两条路——@JavascriptInterface(仅限完全可信的 H5)或 URL 拦截/WebMessageListener(更安全);HTTPS 页里的 HTTP 资源默认被拦;addJavascriptInterface 暴露的方法等于把 App 交给网页;WebView 不销毁就是内存泄漏重灾区。 所有 Flutter/RN 的 WebView 插件,底层都绕不开这些。

核心知识点

1. 基础配置:权限与 WebSettings

1
<uses-permission android:name="android.permission.INTERNET" />
1
2
3
4
5
6
7
8
val settings = webView.settings
settings.javaScriptEnabled = true          // H5 要交互必须开
settings.domStorageEnabled = true          // 开启 LocalStorage
settings.databaseEnabled = false            // WebSQL 已废弃,不必开
settings.loadWithOverviewMode = true
settings.useWideViewPort = true
settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
// ❌ 生产别用 MIXED_CONTENT_ALWAYS_ALLOW(HTTPS 页里放行任意 HTTP)
1
2
3
4
5
6
7
8
9
10
11
// 页面加载/错误由 WebViewClient 接管
webView.webViewClient = object : WebViewClient() {
    override fun onPageFinished(view: WebView?, url: String?) { /* 关进度条 */ }
    override fun onReceivedError(view: WebView?, req: WebResourceRequest?, e: WebResourceError?) {
        if (req?.isForMainFrame == true) showErrorPage()
    }
}
// 弹窗/进度/标题由 WebChromeClient 接管
webView.webChromeClient = object : WebChromeClient() {
    override fun onProgressChanged(view: WebView?, newProgress: Int) { updateProgress(newProgress) }
}

2. @JavascriptInterface:方便但危险

API 17 起只有带 @JavascriptInterface 注解的方法才暴露给 JS,但只要暴露,网页就能以你的 App 权限调用它——加载了不可信 H5(含被注入的第三方广告)就等于开门揖盗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class AppBridge(private val ctx: Context) {
    @JavascriptInterface
    fun showToast(msg: String) {
        // 在主线程执行 UI 操作
        Toast.makeText(ctx, msg, Toast.LENGTH_SHORT).show()
    }

    @JavascriptInterface
    fun saveData(key: String, value: String) {
        // 必须校验,否则路径穿越/注入
        if (key.contains("/") || key.contains("..")) return
        prefs.edit { putString(key, value) }
    }
}
webView.addJavascriptInterface(AppBridge(this), "Native")
1
2
// H5 侧调用
Native.showToast("hello from web");

// ❌ 在会加载外链/广告的 WebView 上加 addJavascriptInterface,且方法不做校验 // ✅ 仅在你完全掌控的 H5 上用它;跨信任边界优先用 URL 拦截或 WebMessageListener

3. JSBridge 双向通信

JS → 原生两条安全路线;原生 → JS 用 evaluateJavascript。

1
2
3
4
// 原生调 JS(API 19+ 推荐,可拿返回值)
webView.evaluateJavascript("window.onUserInfo('${json}')") { result ->
    Log.d("JS", "returned: $result")
}
1
2
3
4
5
6
7
8
9
10
11
// 经典安全 JSBridge:H5 通过自定义 scheme 触发,原生在 shouldOverrideUrlLoading 拦截
webView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(view: WebView, req: WebResourceRequest): Boolean {
        val uri = req.url
        if (uri.scheme == "myapp") {
            handleAction(uri.host, uri.query)   // 解析 action 与参数,做白名单校验
            return true                         // true = 自己消化,不交给 WebView 加载
        }
        return false                            // false = 正常加载
    }
}
1
2
// H5 侧发起调用(不暴露原生方法,只跳自定义协议)
location.href = "myapp://share?text=hello";

现代更推荐 WebView.addWebMessageListener(API 23+),它带 sourceOrigin 可做来源校验,比 scheme 拦截更严谨;postMessage 双向通信。

1
2
3
4
5
6
val cm = CookieManager.getInstance()
cm.setAcceptCookie(true)
cm.setAcceptThirdPartyCookies(webView, true)   // 跨域(如 H5 与 API 不同域)必须开
cm.setCookie("https://example.com", "token=abc; Domain=.example.com; Path=/")
// Android 5.0+ 写完必须 flush,否则不一定即时生效
CookieManager.getInstance().flush()

// ❌ 只在 Java 网络层(OkHttp)存 token,WebView 里 H5 反复跳登录页 // ✅ 登录后把 token 写进 CookieManager 并 flush,H5 请求自动带登录态

5. 混合内容与 FileProvider

Android 5.0 起默认阻止 HTTPS 页里的 HTTP 子资源(MIXED_CONTENT_NEVER_ALLOW)。要么把资源全改 HTTPS,要么退一步用 COMPATIBILITY_MODE,千万别 ALWAYS_ALLOW。

应用间共享文件要用 FileProvider 生成 content://,而不是裸 file://:

1
2
3
4
5
6
7
8
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

6. 内存泄漏与进程崩溃

WebView 持有 Activity 引用,销毁不当必泄漏;低端机上渲染进程还可能被杀。

1
2
3
4
5
6
7
8
9
10
11
12
override fun onDestroy() {
    webView.removeAllViews()
    webView.destroy()          // 必须显式销毁
    webView = null
    super.onDestroy()
}

// 渲染进程崩溃(API 26+):重建而非崩溃
override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {
    if (detail.didCrash()) { recreateWebView(); return true }
    return false
}

其实你每天都在用

  • 协议/隐私页:用 WebView 加载 H5,注意 javaScriptEnabled 按需开
  • webview_flutter 键盘错位:本质是 PlatformView 的已知问题,升新版或换 flutter_inappwebview
  • 分享到微信后回 App:H5 里 location.href="myapp://..." 被 shouldOverrideUrlLoading 拦下
  • 登录后 H5 免登:CookieManager 写入 token 并 flush
  • H5 调原生拍照/定位:@JavascriptInterface 或 scheme 拦截,结果用 evaluateJavascript 回传
  • 白屏:十有八九是混合内容被拦或 JS 异常,开 onReceivedError 看主帧错误
  • 页面卡死被系统 Kill:onDestroy 没 destroy() WebView,内存只涨不跌

常见误解(FAQ)

❌ 误区一:”addJavascriptInterface 很方便,所有 H5 都用它通信”

它把 App 的原生能力直接交给网页。只要 WebView 加载了不可信内容(含第三方广告/被 XSS 注入),恶意 JS 就能调你暴露的方法做文件读写、敏感信息窃取。结论:只在完全自有的 H5 上用,且方法必须做参数校验;跨信任边界改用 URL 拦截或 WebMessageListener。

❌ 误区二:”混合内容设置 ALWAYS_ALLOW 最省事”

这等于允许 HTTPS 页面里加载任意 HTTP 资源,中间人可篡改注入脚本,直接击穿安全模型。正确做法:资源全部上 HTTPS;实在兼容不了才用 COMPATIBILITY_MODE,且限定可信域名。

❌ 误区三:”WebView 不手动销毁也没事,GC 会收”

WebView 内部多线程 + 持有 Activity 引用,Activity 销毁后它常还活着,导致 Activity 无法回收、内存持续上涨。必须在 onDestroy 里 removeAllViews() + destroy();渲染进程崩溃要 onRenderProcessGone 兜底。

❌ 误区四:”Flutter/RN 的 WebView 插件不用管原生细节”

webview_flutter 走 PlatformView、react-native-webview 直接包原生 View,键盘、滚动冲突、Cookie、JSBridge 最终都回到 Android 原生 WebView 的行为。插件出问题时,先查原生 WebView 配置。

一句话总结

WebView 是”看起来最简单、踩坑最隐蔽”的组件:把 @JavascriptInterface 只留给可信 H5、用 URL 拦截/WebMessageListener 做跨边界通信、Cookie 记得 flush、混合内容别放开、销毁要彻底——守住这五点,H5 与原生的桥才既通又稳。

本文由作者按照 CC BY 4.0 进行授权