注册

Android WebView — 实现保存页面功能

在开发公司App期间遇到一个需求,在游戏页中使用WebView展示游戏网页,退出游戏页再次进入时,如果是同一个游戏就直接回到退出时的页面。按照一般的做法,在页面关闭后销毁WebView,再次进入游戏页时,不论是否为同个游戏肯定会重新加载。

实现保存页面功能

之前同事分享了一篇提高WebView渲染效率的文章,其中提到可以提前通过MutableContextWrapper创建WebView并缓存起来,在需要的页面里从缓存中获取WebView,并把MutableContextWrapper切换为对应ActivityFragmentContext。根据文中的测试结果来看,提升的效率用户基本无法感知,但正好可以用来实现我们需要的功能。

具体方案如下:

  • 在一个单例类(也可以直接用Application)中,创建一个Map用于存放需要保留的WebView和其打开的网页链接。
  • 在进入页面时,判断外部传入的网页链接和缓存的网页链接是否为同一个,是就使用缓存的WebView,不是就销毁缓存的WebView并创建一个新的。
  • 关闭页面时,将MutableContextWrapper设置为ApplicationContext,并将WebView从页面布局中移除。

示例代码如下:

  • 单例类
object WebVIewCacheController {  

// 经过实际测试需要如此实现
val webViewContextWrapperCache = MutableContextWrapper(ExampleApplication.exampleContext)

// Key为网页链接,Value为WebView
val webViewCache = ArrayMap()
}
,>
  • 示例页面
class ReservePageExampleActivity : AppCompatActivity() {  

private lateinit var binding: LayoutReservePageExampleActivityBinding

private var currentWeb: WebView? = null

private val webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView, newProgress: Int) {
super.onProgressChanged(view, newProgress)
binding.pbWebLoadProgress.run {
post { progress = newProgress }
if (newProgress >= 100 && visibility == View.VISIBLE) {
postDelayed({ visibility = View.GONE }, 500)
}
}
}
}
private val webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
binding.pbWebLoadProgress.run { post { visibility = View.VISIBLE } }
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutReservePageExampleActivityBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// 处理系统返回事件
handleBackPress()
}
})
intent.getStringExtra(PARAMS_LINK_URL)?.let { websiteUrl ->
// 切换Context
WebVIewCacheController.webViewContextWrapperCache.baseContext = this
// 获取缓存
val cacheWebsiteUrl = WebVIewCacheController.webViewCache.entries.firstOrNull()?.key
currentWeb = WebVIewCacheController.webViewCache.entries.firstOrNull()?.value
if (websiteUrl == cacheWebsiteUrl) {
// 加载同个网页,使用缓存的WebView
currentWeb?.let {
// 确保控件没有父控件
removeViewParent(it)
// 添加到页面布局最底层。
binding.root.addView(it, 0)
}
} else {
// 加载不同网页,释放旧的WebView并创建新的
createWebView(websiteUrl)
}
}
}

private fun createWebView(webSiteUrl: String) {
releaseWebView(currentWeb)
WebVIewCacheController.webViewCache.clear()
currentWeb = WebView(WebVIewCacheController.webViewContextWrapperCache).apply {
initWebViewSetting(this)
// 设置背景为黑色,根据自己需求可以忽略
setBackgroundColor(ContextCompat.getColor(this@ReservePageExampleActivity, R.color.color_black_222))
layoutParams = ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_PARENT, ConstraintLayout.LayoutParams.MATCH_PARENT)
// 确保控件没有父控件
removeViewParent(this)
// 添加到页面布局最底层。
binding.root.addView(this, 0)
loadUrl(webSiteUrl)
// 缓存WebView
WebVIewCacheController.webViewCache[webSiteUrl] = this
}
}

@SuppressLint("SetJavaScriptEnabled")
private fun initWebViewSetting(webView: WebView) {
val settings = webView.settings
settings.cacheMode = WebSettings.LOAD_DEFAULT
settings.domStorageEnabled = true
settings.allowContentAccess = true
settings.allowFileAccess = true
settings.allowFileAccessFromFileURLs = true
settings.allowUniversalAccessFromFileURLs = true
settings.useWideViewPort = true
settings.loadWithOverviewMode = true
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW

settings.javaScriptEnabled = true
settings.javaScriptCanOpenWindowsAutomatically = true

webView.webChromeClient = webChromeClient
webView.webViewClient = webViewClient
}

private fun handleBackPress() {
if (currentWeb?.canGoBack() == true) {
currentWeb?.goBack()
} else {
minimize()
}
}

private fun minimize() {
// 切换Context
WebVIewCacheController.webViewContextWrapperCache.baseContext = applicationContext
// 暂时先把WebView移出布局
currentWeb?.let { binding.root.removeView(it) }
finish()
}

private fun releaseWebView(webView: WebView?) {
webView?.run {
loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
clearHistory()
clearCache(false)
binding.root.removeView(this)
destroy()
}
}

private fun removeViewParent(view: View) {
try {
val parent = view.parent
(parent as? ViewGr0up)?.removeView(view)
} catch (e: Exception) {
e.printStackTrace()
}
}
}

效果如图:

Screen_recording_202 -big-original.gif

示例

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee

内存占用问题

WebView通常会占用不少内存,在我司的App中其占用内存基本不会小于100M,甚至能到200M以上。在WebView占用了大量内存的情况下,如果App中还有其他的功能对内存需求较高,就容易出现OOM。其实在页面销毁时正确的销毁WebView可以释放其占用的内存,但就无法实现我们需要的功能,因此需要另寻他法。

跟leader讨论后,决定采用子进程的方案,即WebView单独运行在子进程中,不同进程的内存分配是独立的,所以基本可以解决OOM问题。

子进程的配置很简单,在AndroidManifest中配置一下WebView所在页面即可,如下:

"1.0" encoding="utf-8"?>  
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
......>


<activity
android:name=".web.reserve.ReservePageExampleActivity"
android:process=":webviewpage" />

application>
manifest>

独立进程也会带来一些新的问题 :

  1. 跨进程通信。

这点在我司的App中基本无需额外处理,因为其他页面与游戏页的交互仅仅只有传入网页链接,通过Bundle带入即可。若Bundle无法满足需求,可以考虑使用广播、MessageContentProviderAIDL等跨进程通信方案。

  1. 子进程初始化时,会有一小段白屏时间(与应用冷启动一样)。

初始化白屏的体验效果不好,这边提供一个思路,在WebView所在页面的前置页加载数据的同时初始化子进程,等子进程初始化完成后再结束加载,并配置android:windowDisablePreview来隐藏启动页面时的白屏。


作者:ChenYhong
来源:juejin.cn/post/7315727549376380964

0 个评论

要回复文章请先登录注册