联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

Android WebView — 实现保存页面功能

作者:访客发布时间:2023-12-25分类:程序开发学习浏览:68


导读:在开发公司App期间遇到一个需求,在游戏页中使用WebView展示游戏网页,退出游戏页再次进入时,如果是同一个游戏就直接回到退出时的页面。按照一般的做法,在页面关闭后销毁WebVi...

在开发公司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()  
        }  
    }  
}

效果如图:

Screen_recording_202 -big-original.gif

示例

演示代码已在示例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来隐藏启动页面时的白屏。


标签:页面功能安卓系统WebView


程序开发学习排行
最近发表
网站分类
标签列表