上一篇讲了前台服务与后台治理,切到 WebView——App 里嵌入 H5 页面是高频需求(电商活动页、H5 营销、报名表)。但 WebView 是 Android 性能重灾区,做不好白屏、首屏慢、内存泄漏一起来。
WebView 的性能问题
WebView 第一次初始化(Cold Start)可能花 800ms~2s:
[打开页面] → [进程启动 WebView] → [libwebviewchromium.so 加载]
→ [libwebviewchromium_plat_support.so 加载] → [V8 启动] → [页面渲染]
内存占用大:一个 WebView 实例 40MB+。多个 WebView 直接 OOM。
总结常见痛点:
进程化 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 内核也支持独立进程)。独立进程的好处:
进程死了不连带主进程崩溃
内存占用移出主进程
子进程可以独立 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")
问题:
安全性差:H5 直接调 Kotlin/Java,调用了不被记录的接口
主线程阻塞: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)
}
}
}
注意:
JavaScriptInterface 注解方法默认在 WebView 线程(主线程)执行,IO 要切线程
回调 H5 通过
evaluateJavascript调用回 JS
调试 WebView 性能
// 开启 trace
WebView.setWebContentsDebuggingEnabled(true)
// Chrome 远程连接
chrome://inspect
DevTools 的 Network、Performance、Memory 面板都能用。常见问题模式:
大量同步请求:H5 用了 jQuery AJAX 而不是现代 fetch
长任务:JS 函数超过 50ms 在 Performance 面板能看到
内存泄漏:DOM 节点不释放
WebView 替代方案
X5 在 Android 5-7 时代是救命稻草,但 Android 10+ 系统 WebView 已经很强。多数项目不必引入 X5。
小结
WebView 性能优化核心三件事:
预加载:用进程化 + 共享池
JSBridge:类型安全 + 异步调用
离线包:H5 资源 CDN 化
加上 Chrome DevTools 调试,定位性能问题不费力。H5 在某些场景依然是 App 内最灵活的展示方式,掌握这套技能能为产品留足动态化空间。
下一篇深入 Compose 编译器原理,看 Compose 是怎么把 Kotlin 翻译成 UI 的。