Administrator
发布于 2026-08-25 / 20 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 67 篇:WebView 性能优化与 JSBridge:原生与 H5 的桥接艺术

上一篇讲了前台服务与后台治理,切到 WebView——App 里嵌入 H5 页面是高频需求(电商活动页、H5 营销、报名表)。但 WebView 是 Android 性能重灾区,做不好白屏、首屏慢、内存泄漏一起来。

WebView 的性能问题

WebView 第一次初始化(Cold Start)可能花 800ms~2s:

[打开页面] → [进程启动 WebView] → [libwebviewchromium.so 加载]
  → [libwebviewchromium_plat_support.so 加载] → [V8 启动] → [页面渲染]

内存占用大:一个 WebView 实例 40MB+。多个 WebView 直接 OOM。

总结常见痛点:

痛点

表现

原因

启动慢

第一次打开白屏

进程创建、SO 加载

内存大

OOM、卡顿

多 WebView 实例

白屏

加载慢、没进度条

缺预加载

错位

H5 字体小、按钮小

缺移动端 meta

卡顿

滚动卡

GPU 渲染没启

桥接慢

H5 调原生卡

JSBridge 同步执行

进程化 WebView

业界主流方案:WebView 单独进程。

class MyWebActivity : AppCompatActivity() {
    private val webView by lazy { findViewById<WebView>(R.id.web) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_web)

        webView.settings.apply {
            javaScriptEnabled = true
            domStorageEnabled = true
            useWideViewPort = true
            loadWithOverviewMode = true
            cacheMode = WebSettings.LOAD_DEFAULT
        }
    }
}

但要让 WebView 在独立进程运行:

<activity
    android:name=".MyWebActivity"
    android:process=":webview" />

或者用 WebView 进程化库(腾讯 X5 内核也支持独立进程)。独立进程的好处:

  1. 进程死了不连带主进程崩溃

  2. 内存占用移出主进程

  3. 子进程可以独立 kill

代价:进程间通信复杂。

远程调试 WebView

开启 Chrome DevTools 调试:

if (BuildConfig.DEBUG) {
    WebView.setWebContentsDebuggingEnabled(true)
}

然后 Chrome 访问 chrome://inspect,能看到当前设备所有 WebView:

chrome://inspect/#devices

性能问题(长任务、内存泄漏)能在 Performance 面板看到。

JSBridge:原生与 H5 通信

老的 @JavascriptInterface 模式

class WebBridge(private val context: Context) {
    @JavascriptInterface
    fun showToast(text: String) {
        Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
    }
}

webView.addJavascriptInterface(WebBridge(this), "Bridge")
// H5 调用:Bridge.showToast("Hello")

问题:

  1. 安全性差:H5 直接调 Kotlin/Java,调用了不被记录的接口

  2. 主线程阻塞:H5 调用时,方法在 WebView 线程(也是主线程)执行

现代 JSBridge 设计

主流的 JSBridge 库(DSBridge、JsBridge)都遵循这套协议:

H5 → invoke(method, params, callbackId)
↓
原生 onMessageHandler 处理
↓
调用 callbackId 对应的 JS callback

// 跨上下文消息传递:H5 侧用 iframe src 改 hash

简化版实现:

class JsBridge {
    private val handlerMap = mutableMapOf<String, (String, JsCallback) -> Unit>()
    private val pendingCallbacks = mutableMapOf<String, JsCallback>()

    fun register(method: String, handler: (String, JsCallback) -> Unit) {
        handlerMap[method] = handler
    }

    // 注入到 H5
    val injectJs = """
        window.Bridge = {
            invoke(method, params, callbackId) {
                window.__Bridge.invoke(method, params, callbackId);
            }
        };
        window.__BridgeCallback = function(callbackId, result) {
            // 调用对应 callback
        };
    """.trimIndent()
}

H5 调用:

Bridge.invoke('getUserInfo', '{}', (result) => {
    console.log('user:', result);
});

实际项目用 ddsbrdige-android 或自己写一份都可以。

H5 端的常见适配

1. 移动端 viewport

H5 头部必须加:

<meta name="viewport" content="
    width=device-width, 
    initial-scale=1.0, 
    maximum-scale=1.0, 
    minimum-scale=1.0, 
    user-scalable=no, 
    viewport-fit=cover
">

否则 H5 在 WebView 里显示成"桌面版"。

2. 字体适配

H5 字体通过 CSS px 控制,Android WebView 的 density 影响显示:

webView.settings.textZoom = 100 // WebView 固定 100%,不让系统设置影响

3. 多 Window 状态

刘海屏、安全区域处理:

body {
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
}

4. H5 缓存管理

webView.settings.apply {
    cacheMode = WebSettings.LOAD_DEFAULT
    databaseEnabled = true
    domStorageEnabled = true
}

// 清理缓存(退出登录等)
webView.clearCache(true)
webView.clearHistory()

性能优化

1. 预加载

App 启动时预创建 WebView:

class WebViewPool {
    private val pool = mutableListOf<WebView>()
    private val MAX_POOL_SIZE = 3

    fun acquire(context: Context): WebView {
        return if (pool.isEmpty()) {
            createWebView(context)
        } else {
            pool.removeAt(pool.size - 1).also {
                it.context = context.applicationContext
            }
        }
    }

    fun release(webView: WebView) {
        if (pool.size < MAX_POOL_SIZE) {
            pool.add(webView.apply {
                stopLoading()
                (parent as? ViewGroup)?.removeView(this)
                loadUrl("about:blank")
            })
        } else {
            webView.destroy()
        }
    }

    private fun createWebView(context: Context): WebView {
        return WebView(context.applicationContext).apply {
            settings.javaScriptEnabled = true
            // ...
        }
    }
}

但 WebView 不能直接装回 ViewGroup,原来的 parent 已经 disconnect。

实际方案:WebView 放进 FrameLayout,需要时复用整个 FrameLayout。

2. 离线包(H5 资源预下载)

class OfflineBundleDownloader {
    suspend fun downloadBundle(url: String, version: String) {
        val file = File(context.filesDir, "webview/$version.zip")
        if (file.exists() && !isOutdated(file)) return

        val response = httpClient.get(url)
        response.body!!.byteStream().use { input ->
            file.outputStream().use { output ->
                input.copyTo(output)
            }
        }

        ZipFile(file).unzipTo(File(context.filesDir, "webview/$version/"))
    }

    fun getBundlePath(version: String): String =
        "file://${File(context.filesDir, "webview/$version/index.html").absolutePath}"
}

H5 更新包发布在 CDN,App 检查版本对比,下载后离线加载:

webView.loadUrl(offlineBundle.getBundlePath(currentVersion))
// 离线优先,网络回退
webView.setOnLoadResource { resourceUrl ->
    if (shouldFallbackToNetwork(resourceUrl)) {
        webView.stopLoading()
        webView.loadUrl(resourceUrl)
    }
}

3. 图片优化

H5 图片懒加载:

const lazyImage = new IntersectionObserver(entries => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            entry.target.src = entry.target.dataset.src;
            lazyImage.unobserve(entry.target);
        }
    });
});

document.querySelectorAll('img[data-src]').forEach(img => {
    lazyImage.observe(img);
});

Or 用 lozad.js / lazysizes。

4. WebView 字体抗锯齿

webView.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING

或者用 CSS -webkit-font-smoothing: antialiased。

实战案例:电商商品详情 H5

需求:H5 商品详情页能调用原生的"加入购物车"、"打开支付"。

class WebBridge {
    @JavascriptInterface
    fun addToCart(goodsId: String, callbackId: String) {
        CoroutineScope(Dispatchers.Main).launch {
            // 调用原生 API
            val result = shoppingRepository.addToCart(goodsId)
            
            // 回传给 H5
            val response = if (result.isSuccess) {
                """{"code": 0, "data": {"cartCount": ${result.value}}}"""
            } else {
                """{"code": -1, "msg": "${result.error}"}"""
            }
            
            callback(callbackId, response)
        }
    }
}

注意:

  1. JavaScriptInterface 注解方法默认在 WebView 线程(主线程)执行,IO 要切线程

  2. 回调 H5 通过 evaluateJavascript 调用回 JS

调试 WebView 性能

// 开启 trace
WebView.setWebContentsDebuggingEnabled(true)

// Chrome 远程连接
chrome://inspect

DevTools 的 Network、Performance、Memory 面板都能用。常见问题模式:

  • 大量同步请求:H5 用了 jQuery AJAX 而不是现代 fetch

  • 长任务:JS 函数超过 50ms 在 Performance 面板能看到

  • 内存泄漏:DOM 节点不释放

WebView 替代方案

方案

优势

劣势

系统 WebView

标准

性能一般

X5 内核(腾讯)

兼容老设备、视频播放

体积大

UC WebView

小

兼容性差

自研 WebView

完全可控

维护成本高

直接放弃 WebView

性能好

动态化能力弱

X5 在 Android 5-7 时代是救命稻草,但 Android 10+ 系统 WebView 已经很强。多数项目不必引入 X5。

小结

WebView 性能优化核心三件事:

  1. 预加载:用进程化 + 共享池

  2. JSBridge:类型安全 + 异步调用

  3. 离线包:H5 资源 CDN 化

加上 Chrome DevTools 调试,定位性能问题不费力。H5 在某些场景依然是 App 内最灵活的展示方式,掌握这套技能能为产品留足动态化空间。

下一篇深入 Compose 编译器原理,看 Compose 是怎么把 Kotlin 翻译成 UI 的。


评论