Github: github.com/oOJohn6Oo/W...
效果
以静态 html 渲染流式返回的数据为例,当前方案可实现:
- WebView 复用省去创建 WebView 的耗时
- 避免重复 loadUrl 实现数据秒渲染
掘金必须用自己的图床并且必须带水印,原图可以去 Github 看。

具体实现
缓存池实现
- 这里使用 host + path 作为 key,
WebView
作为 value,构建WebView
缓存池。- 使用
MutableContextWrapper
处理复用时Context
变化的情况。
> 极简 WebViewCache.kt
kt
object WebViewCache {
private const val MAX_POOL_SIZE = 8
private val mPools: MutableMap<String, Pools.Pool<WebView>> = mutableMapOf()
private val emptyWebViewClient = object : WebViewClient() {}
fun acquireOrCreate(context: Context, url: Uri? = null): WebView {
val hostAndPath = (url?.host ?: "") + (url?.path ?: "")
if (!mPools.contains(hostAndPath)) {
mPools[hostAndPath] = Pools.SynchronizedPool(MAX_POOL_SIZE)
}
var webView = mPools[hostAndPath]?.acquire()
if (webView == null) {
val wrapper = MutableContextWrapper(context)
webView = WebView(wrapper)
} else {
(webView.context as? MutableContextWrapper)?.baseContext = context
}
return webView
}
@MainThread
fun releaseSafe(webView: WebView) = webView.apply {
Log.d(TAG, "releaseWebView $webView")
(webView.parent as? ViewGroup)?.removeView(webView)
webView.stopLoading()
(webView.context as? MutableContextWrapper)?.baseContext = appContext
webView.webViewClient = emptyWebViewClient
val uri = webView.url?.toUri()
val hostAndPath = (uri?.host ?: "") + (uri?.path ?: "")
mPools[hostAndPath]?.release(webView)
}
}
具体使用
- 使用时根据 url 是否为空判断应该
loadUrl
还是直接调用 JS 方法- 如果是全新创建的 WebView,则在 onPageFinished 回调中调用
> JsWithCacheWebFragment.kt
kt
class JsWithCacheWebFragment: BaseWebFragment() {
private var currentWebView: WebView? = null
override fun getWebView(): WebView {
val desireUrl = "file:///android_asset/demo_js_interface.html"
return WebViewCache.acquireOrCreate(requireContext(), desireUrl.toUri()).apply {
this@JsWithCacheWebFragment.currentWebView = this
WebViewCache.setupWebView(this)
webViewClient = object: WebViewClient(){
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
printMsg.add("${SystemClock.uptimeMillis() - startTime}ms pageFinished, progress:${view?.progress}")
if(view?.progress == 100){
startCollectData(view)
}
}
}
if (this.url.isNullOrBlank()) {
loadUrl(desireUrl)
} else {
startCollectData(this)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
currentWebView?.also { WebViewCache.releaseSafe(it) }
currentWebView = null
}
}
关键问题点及解决方案
1. WebView 创建耗时
WebView
的创建及其相关操作必须在主线程,这是 Android 强制的,我们只能将创建提前,放到缓存池中,使用时从缓存池中取,不用时及时释放到缓存池。目前没有其他办法优化。
2. loadUrl 耗时
其实创建只是初次耗时会久一些,而 loadUrl
则稳定地慢,测试本地 html 文件最快都要差不多 50 ms,所以避免调用这个方法是优化耗时的关键。
3. onPageFinished 回调多次
Android Webview 内部机制决定的,可以通过 WebView.progress == 100
来判断
4. 进入页面但缓存池空白
不提前"预热",首次使用肯定还是避免不了 WebView
创建和 laodUrl
,只能提前加载。 提前加载需要确定合适的时机,一般是推荐通过 idleHandler
完成。
kt
val preloadIdleHandler = object : MessageQueue.IdleHandler {
override fun queueIdle(): Boolean {
if (mPools.isNotEmpty()) return false
val desireUrl = "file:///android_asset/demo_js_interface.html"
preloadWebView(desireUrl)
return false
}
}
fun preloadWebView(desireUrl: String) {
acquireOrCreate(appContext, desireUrl.toUri()).apply {
setupWebView(this)
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
view?.let { releaseSafe(it) }
}
}
loadUrl(desireUrl)
}
}
然后在合适的时机调用
kt
Looper.getMainLooper().queue.addIdleHandler(WebViewCache.preloadIdleHandler)
5. 内存泄漏
主要是 context 和 WebViewClient,我们在 release 的时候重置下就可以了。
7. 内存占用可能太高
主要还是跟渲染的页面内容相关,WebView
本身其实占用内存不是很大,多个实例也不会导致内存占用线性增长。
可以通过优化代码逻辑及调整缓存池大小来降低内存。
8. 可能更快的 WEB 通信方式
一直听说 WebMessagePort
相较于 JSInterface
性能更优,但是 Demo 上这种极简场景没看出有性能差别,可能交互频繁程度还是不够,ChatGPT 说适合传输音视频流的场景。
3. Demo 效果中 ViewPager2 + BottomSheet 下的一些关键问题点及其解决方案
1. 实现嵌套滑动,解决可能滑不动的问题
ViewPager2 内部其实是 RecyclerView
实现的,需要先禁用它的嵌套滑动,然后内容页面因为要与 BottomSheet 联动,需要使用 NestedScrollView
。
kt
(mBinding.viewpagerFgWeb.getChildAt(0) as RecyclerView).apply {
isNestedScrollingEnabled = false
}
另外可能会遇到切换 Tab 后无法嵌套滑动的情况,这是 BottomSheet 的已知问题,在 这篇分析文章 中有详细说明,但,Demo 中没有遇到这个问题。
1. 如何实现预加载下一页、最多同时存在三个 Tab
强烈推荐阅读这篇 ViewPager2 分析文章,简单来说:
offscreenPageLimit = 1
实现预加载下一页isItemPrefetchEnabled = false
+setItemViewCacheSize(0)
实现最多同时存在三个 Tab
2. 点击序号快速切换 Tab 卡顿
因为 ViewPager2
内部是 RecyclerView
,而快速切换默认使用 smoothScrollTo
,这会导致沿途经过的 Tab 可能会被快速加载,然后快速销毁,当逻辑一复杂,同时重建再销毁五六个 Tab 性能当然会变差。
为了避免这个问题,同时又支持左右切换,可以:
- 在点击时判断 currentPosition 和 targetPosition 的距离来决定是
scrollTo
还是smoothScrollTo
- 点击时统一直接
scrollTo
3. 默认 BottomSheetDialog 的 BottomSheet 不会全屏
BottomSheetDialog 的 BottomSheetBehavior
是加在 FrameLayout
上的,而它的默认测量行为是根据 childView 的最大值决定自身高度,而且会忽略上层传过来的 heightMeasureSpec。
所以 Demo 中是自定义了 FrameLayout
的 onMeasure
方法,高度固定取最大值,然后将 ViewPager2
塞进这个 FrameLayout
中就可以了。