一、基础架构
┌─────────────────────────────────┐
│ 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)─── Web
5.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 返回值 |
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/simayilong/article/details/163275911



