蜡台头像
关注

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: 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 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 = 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 + onDestroyremoveView + 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

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--