Android WebView — 实现保存页面功能

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

实现保存页面功能

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

具体方案如下:

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

示例代码如下:

  • 单例类
kotlin 复制代码
object WebVIewCacheController {  
   
    // 经过实际测试需要如此实现
    val webViewContextWrapperCache = MutableContextWrapper(ExampleApplication.exampleContext)  
  
    // Key为网页链接,Value为WebView
    val webViewCache = ArrayMap<String, WebView?>()  
}
  • 示例页面
kotlin 复制代码
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 复制代码
<?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来隐藏启动页面时的白屏。

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui