一、基础架构
┌─────────────────────────────────┐ │ Android App Layer │ │ ┌───────────┐ ┌─────────────┐ │ │ │ Activity │ │ Fragment │ │ │ └─────┬─────┘ └──────┬──────┘ │ │ │ │ │ │ ┌─────┴───────────────┴──────┐ │ │ │ WebView │ │ │ │ ┌───────────────────────┐ │ │ │ │ │ WebViewClient │ │ │ │ │ │ WebChromeClient │ │ │ │ │ │ JavascriptInterface │ │ │ │ │ └───────────────────────┘ │ │ │ └────────────────────────────┘ │ └─────────────────────────────────┘ ↕ IPC Bridge ┌─────────────────────────────────┐ │ Web Content Layer │ │ HTML / CSS / JavaScript │ └─────────────────────────────────┘二、核心初始化配置
class WebViewActivity : AppCompatActivity() { private lateinit var webView: WebView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_webview) webView = findViewById(R.id.webView) setupWebView() webView.loadUrl("https://example.com") } private fun setupWebView() { webView.settings.apply { // 基础配置 javaScriptEnabled = true domStorageEnabled = true allowFileAccess = false allowContentAccess = false // 缓存策略 cacheMode = WebSettings.LOAD_DEFAULT // 显示优化 useWideViewPort = true loadWithOverviewMode = true setSupportZoom(false) builtInZoomControls = false displayZoomControls = false // 混合内容(HTTPS 页面加载 HTTP 资源) mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE // 字体缩放 textZoom = 100 } // 设置客户端 webView.webViewClient = AppWebViewClient() webView.webChromeClient = AppChromeClient() // 注入 JS 桥 webView.addJavascriptInterface( WebBridge(this), "NativeBridge" ) } }三、WebViewClient — 页面导航控制
class AppWebViewClient : WebViewClient() { // 拦截页面加载(核心) override fun shouldOverrideUrlLoading( view: WebView, request: WebResourceRequest ): Boolean { val url = request.url return when { // 拦截外部链接 → 用系统浏览器打开 url.host?.contains("example.com") == false -> { Intent(Intent.ACTION_VIEW, url).also { view.context.startActivity(it) } true } // 处理 deep link url.scheme == "myapp" -> { DeepLinkHandler.handle(view.context, url) true } // 内部链接 WebView 自己加载 else -> false } } // 拦截资源请求(可选缓存/替换) override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { val url = request.url.toString() // 本地资源拦截 if (url.endsWith(".js") && url.contains("analytics")) { // 返回空脚本,屏蔽埋点 return WebResourceResponse( "application/javascript", "UTF-8", "".byteInputStream() ) } return null } override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { // 显示加载指示器 } override fun onPageFinished(view: WebView, url: String?) { // 隐藏加载指示器 // 注入通用脚本 view.evaluateJavascript(bridgeScript, null) } override fun onReceivedError( view: WebView, request: WebResourceRequest, error: WebResourceError ) { if (request.isForMainFrame) { // 加载离线降级页面 view.loadUrl("file:///android_asset/error.html") } } override fun onReceivedSslError( view: WebView, handler: SslErrorHandler, error: SslError ) { // 安全:禁止忽略证书错误 handler.cancel() } }四、WebChromeClient — UI 交互
class AppChromeClient : WebChromeClient() { override fun onProgressChanged(view: WebView, newProgress: Int) { // 更新进度条 progressBar.progress = newProgress progressBar.isVisible = newProgress < 100 } override fun onReceivedTitle(view: WebView, title: String?) { // 更新标题栏 titleText.text = title } // 文件选择器 override fun onShowFileChooser( webView: WebView, filePathCallback: ValueCallback<Array<Uri>>, fileChooserParams: FileChooserParams ): Boolean { fileCallback = filePathCallback val intent = fileChooserParams.createIntent() filePickerLauncher.launch(intent) return true } // JS alert() override fun onJsAlert( view: WebView, url: String?, message: String?, result: JsResult ): Boolean { AlertDialog.Builder(view.context) .setTitle("提示") .setMessage(message) .setPositiveButton("确定") { _, _ -> result.confirm() } .show() return true // 自己处理,不走默认弹窗 } // 权限请求(定位、摄像头等) override fun onGeolocationPermissionsShowPrompt( origin: String, callback: GeolocationPermissions.Callback ) { if (ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED ) { callback.invoke(origin, true, false) } else { // 请求权限后回调 pendingGeoCallback = callback requestLocationPermission() } } }五、JS Bridge 双向通信
Native ──evaluateJavascript()──→ Web Native ←──JavascriptInterface─── Web Native ←──shouldOverrideUrlLoading (URL Scheme)─── Web5.1 Native → Web
// Native 调用 JS webView.evaluateJavascript( "window.onTokenRefresh('new_token_123')" ) { result -> Log.d("Bridge", "JS returned: $result") } // 注入通用环境变量 val script = """ window.appConfig = { platform: 'android', version: '${BuildConfig.VERSION_NAME}', statusBarHeight: ${statusBarHeight} }; """.trimIndent() webView.evaluateJavascript(script, null)5.2 Web → Native
class WebBridge(private val context: Context) { @JavascriptInterface fun share(title: String, url: String) { val intent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra(Intent.EXTRA_SUBJECT, title) putExtra(Intent.EXTRA_TEXT, url) } context.startActivity(intent) } @JavascriptInterface fun getToken(): String { return TokenManager.get() ?: "" } @JavascriptInterface fun close() { (context as? Activity)?.finish() } @JavascriptInterface fun postMessage(json: String) { // 通用消息通道 val msg = JSONObject(json) val action = msg.getString("action") MessageBus.dispatch(action, msg) } }// Web 端调用 const token = NativeBridge.getToken(); NativeBridge.share('标题', 'https://example.com'); // 通用消息 NativeBridge.postMessage(JSON.stringify({ action: 'openCamera', params: { quality: 80 } }));六、安全设计
private fun securityHardening() { webView.settings.apply { // 禁止文件访问 allowFileAccess = false allowFileAccessFromFileURLs = false allowUniversalAccessFromFileURLs = false } // URL 白名单校验 fun isUrlAllowed(url: String): Boolean { val allowedHosts = listOf("example.com", "api.example.com") val uri = Uri.parse(url) return uri.host in allowedHosts } // SSL Pinning fun setupSSL() { val certFactory = CertificateFactory.getInstance("X.509") val cert = assets.open("server_cert.cer").use { certFactory.generateCertificate(it) } val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply { load(null) setCertificateEntry("server", cert) } val trustManager = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() ).apply { init(keyStore) }.trustManagers[0] as X509TrustManager val sslContext = SSLContext.getInstance("TLS").apply { init(null, arrayOf(trustManager), null) } // 配置到 OkHttp 等网络层 } }安全清单:
| 要点 | 措施 |
|---|---|
| HTTPS Only | mixedContentMode = NEVER_ALLOW(生产环境) |
| JS 注入风险 | addJavascriptInterface加@Keep+ 混淆保护 |
| Intent Scheme | shouldOverrideUrlLoading中过滤intent:// |
| XSS 防御 | Web 端 CSP 头 + 输入校验 |
| 文件访问 | 关闭所有 file:// 权限 |
| 导航劫持 | URL 白名单 + origin 校验 |
| 证书校验 | SSL Pinning + 不忽略onReceivedSslError |
七、生命周期管理
class WebViewActivity : AppCompatActivity() { override fun onResume() { super.onResume() webView.onResume() webView.resumeTimers() } override fun onPause() { webView.onPause() webView.pauseTimers() // 暂停 JS 定时器 super.onPause() } override fun onDestroy() { webView.apply { stopLoading() loadDataWithBaseURL(null, "", "text/html", "utf-8", null) clearHistory() (parent as? ViewGroup)?.removeView(this) destroy() } super.onDestroy() } // 处理返回键 @Deprecated("Deprecated in Java") override fun onBackPressed() { if (webView.canGoBack()) { webView.goBack() } else { super.onBackPressed() } } }八、性能优化
优化维度 ┌──────────┼──────────┐ ▼ ▼ ▼ 预加载 缓存策略 资源管控 │ │ │ ▼ ▼ ▼ WebViewPool 多级缓存 拦截大图// 1. WebView 预加载池 object WebViewPool { private val pool = LinkedList<WebView>() fun init(context: Context, count: Int = 2) { repeat(count) { pool.add(WebView(context).apply { settings.javaScriptEnabled = true settings.domStorageEnabled = true }) } } fun obtain(context: Context): WebView { return if (pool.isNotEmpty()) pool.pop() else WebView(context) } fun release(webView: WebView) { webView.loadUrl("about:blank") pool.push(webView) } } // 2. 预连接 fun preconnectWebView(context: Context) { val connection = WebView(context).apply { settings.javaScriptEnabled = false loadUrl("https://example.com") } // DNS + TCP + TLS 预热 } // 3. 离线包 fun loadLocalContent(webView: WebView) { val html = assets.open("web/index.html").bufferedReader().readText() webView.loadDataWithBaseURL( "https://example.com", // baseUrl 保证相对路径正确 html, "text/html", "UTF-8", null ) }九、Hybrid 架构模式
┌──────────────────────────────────────────┐ │ 宿主 App │ │ ┌──────────┐ ┌──────────┐ ┌────────┐ │ │ │ 原生页面 │ │ WebView │ │ 原生组件│ │ │ │ (Kotlin) │ │ (Hybrid) │ │(共享) │ │ │ └──────────┘ └────┬─────┘ └────────┘ │ │ │ │ │ ┌────────┴────────┐ │ │ │ 路由分发层 │ │ │ │ Router Service │ │ │ └────────┬────────┘ │ │ │ │ │ ┌────────────┼────────────┐ │ │ ▼ ▼ ▼ │ │ 原生路由 WebView路由 原生浮层 │ │ /home /web/page Dialog │ │ /settings /web/article BottomSheet │ └──────────────────────────────────────────┘路由策略:
object HybridRouter { fun navigate(context: Context, url: String) { val uri = Uri.parse(url) when { uri.scheme == "myapp" -> { // 原生路由 NativeRouter.dispatch(context, uri) } uri.path?.startsWith("/web/") == true -> { // WebView 页面 context.startActivity( Intent(context, WebViewActivity::class.java) .putExtra("url", url) ) } else -> { // 外部浏览器 context.startActivity(Intent(Intent.ACTION_VIEW, uri)) } } } }十、常见问题与对策
| 问题 | 原因 | 方案 |
|---|---|---|
| 内存泄漏 | WebView 持有 Activity 引用 | 独立进程android:process+onDestroy中removeView + destroy |
| 白屏闪烁 | WebView 初始化慢 | 预加载池 / 启动页过渡 |
| 链接跳外部浏览器 | 未重写shouldOverrideUrlLoading | 拦截并返回true |
| 键盘遮挡输入框 | adjustResize失效 | windowSoftInputMode=adjustPan或 Web 端监听visualViewport |
| Cookie 不同步 | CookieManager未初始化 | CookieManager.getInstance().setAcceptCookie(true) |
| 文件上传无效 | 未实现onShowFileChooser | 实现文件选择器回调 |
| 点击无响应 | WebViewClient拦截过激 | 检查shouldOverrideUrlLoading返回值 |