在开发公司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<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中添加。

ExampleDemo github

ExampleDemo gitee

内存占用问题

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>

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

  1. 跨进程通讯。

这点在我司的App中基本无需额外处理,因为其他页面与游戏页的交互只是只要传入网页链接,经过Bundle带入即可。若Bundle无法满足需求,能够考虑运用广播、MessageContentProviderAIDL等跨进程通讯计划。

  1. 子进程初始化时,会有一小段白屏时间(与使用冷发动相同)。

初始化白屏的体验效果不好,这边供给一个思路,在WebView地点页面的前置页加载数据的一起初始化子进程,等子进程初始化完成后再结束加载,并装备android:windowDisablePreview来躲藏发动页面时的白屏。