公司动态
Android WebView 设计指南
一、基础架构┌─────────────────────────────────┐ │ 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: ValueCallbackArrayUri, 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 → Nativeclass 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 OnlymixedContentMode NEVER_ALLOW生产环境JS 注入风险addJavascriptInterface加Keep 混淆保护Intent SchemeshouldOverrideUrlLoading中过滤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 LinkedListWebView() 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:processonDestroy中removeView destroy白屏闪烁WebView 初始化慢预加载池 / 启动页过渡链接跳外部浏览器未重写shouldOverrideUrlLoading拦截并返回true键盘遮挡输入框adjustResize失效windowSoftInputModeadjustPan或 Web 端监听visualViewportCookie 不同步CookieManager未初始化CookieManager.getInstance().setAcceptCookie(true)文件上传无效未实现onShowFileChooser实现文件选择器回调点击无响应WebViewClient拦截过激检查shouldOverrideUrlLoading返回值