在开发公司App期间遇到一个需求,在游戏页中运用WebView
展示游戏网页,退出游戏页再次进入时,如果是同一个游戏就直接回到退出时的页面。按照一般的做法,在页面封闭后毁掉WebView
,再次进入游戏页时,不论是否为同个游戏肯定会重新加载。
完成保存页面功用
之前同事分享了一篇提高WebView
烘托功率的文章,其中说到能够提前经过MutableContextWrapper
创立WebView
并缓存起来,在需求的页面里从缓存中获取WebView
,并把MutableContextWrapper
切换为对应Activity
或Fragment
的Context
。根据文中的测验成果来看,提升的功率用户基本无法感知,但正好能够用来完成咱们需求的功用。
详细计划如下:
- 在一个单例类(也能够直接用
Application
)中,创立一个Map
用于存放需求保留的WebView
和其翻开的网页链接。 - 在进入页面时,判别外部传入的网页链接和缓存的网页链接是否为同一个,是就运用缓存的
WebView
,不是就毁掉缓存的WebView
并创立一个新的。 - 封闭页面时,将
MutableContextWrapper
设置为Application
的Context
,并将WebView
从页面布局中移除。
示例代码如下:
- 单例类
object WebVIewCacheController {
// 经过实践测验需求如此完成
val webViewContextWrapperCache = MutableContextWrapper(ExampleApplication.exampleContext)
// Key为网页链接,Value为WebView
val webViewCache = ArrayMap<String, WebView?>()
}
- 示例页面
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? ViewGroup)?.removeView(view)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
效果如图:
示例
演示代码已在示例Demo中添加。
内存占用问题
WebView
通常会占用不少内存,在我司的App中其占用内存基本不会小于100M,甚至能到200M以上。在WebView
占用了很多内存的情况下,如果App中还有其他的功用对内存需求较高,就简单呈现OOM。其实在页面毁掉时正确的毁掉WebView
能够开释其占用的内存,但就无法完成咱们需求的功用,因此需求另寻他法。
跟leader评论后,决议采用子进程的计划,即WebView
独自运行在子进程中,不同进程的内存分配是独立的,所以基本能够解决OOM问题。
子进程的装备很简单,在AndroidManifest
中装备一下WebView
地点页面即可,如下:
<?xml version="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>
独立进程也会带来一些新的问题 :
- 跨进程通讯。
这点在我司的App中基本无需额外处理,因为其他页面与游戏页的交互只是只要传入网页链接,经过Bundle
带入即可。若Bundle
无法满足需求,能够考虑运用广播、Message
、ContentProvider
、AIDL
等跨进程通讯计划。
- 子进程初始化时,会有一小段白屏时间(与使用冷发动相同)。
初始化白屏的体验效果不好,这边供给一个思路,在WebView
地点页面的前置页加载数据的一起初始化子进程,等子进程初始化完成后再结束加载,并装备android:windowDisablePreview
来躲藏发动页面时的白屏。