WebView组件封装(二)——怎样用设计模式封装WebView,轻松实现个性化定制,让你的App网页更加顺畅

1,656 阅读10分钟

前言

在上篇文章中,我们介绍了如何使用全局缓存池管理来提高WebView的加载速度,源码位置:WebViewPool.kt

在本篇文章中,我们将介绍如何使用设计模式来封装WebView组件,以实现个性化定制并让你的App网页更加顺畅。通过本文的学习,您将掌握如何使用单例、享元、建造者、桥接和装饰等设计模式来优化WebView组件,并且实现不同场景下的灵活配置。

单例设计模式

单例设计模式.png WebViewPool的作用是管理WebView对象的缓存池,通过重复利用已经创建的WebView对象来减少内存的开销和提高性能。

对于WebViewPool来说,我们需要确保一个类只有一个实例,因此想到的是单例设计模式。那么单例设计模式有哪些好处呢?

  • 节省资源:由于只有一个实例在内存中,可以避免创建多个对象所带来的不必要的资源消耗
  • 简化调用:由于单例对象可以被全局访问,因此可以简化调用过程,减少对对象之间传递参数等复杂操作的需要
  • 统一管理:由于只有一个实例存在,可以更方便地进行统一管理
  • 提高灵活性:在某些情况下,单例设计模式可以提高代码的灵活性,例如通过使用单例模式来管理数据库连接池,可以避免频繁地创建和销毁连接对象,从而提高系统性能
internal class WebViewPool private constructor() {
    private lateinit var mUserAgent: String
    private lateinit var mWebViewPool: Array<PkWebView?>

    companion object {
        const val WEB_VIEW_COUNT = 3
        val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
            WebViewPool()
        }
    }

    /**
     * 初始化WebView池
     */
    fun initWebViewPool(context: Context?, userAgent: String = "") {
        if (context == null) return
        mWebViewPool = arrayOfNulls(WEB_VIEW_COUNT)
        mUserAgent = userAgent
        for (i in 0 until WEB_VIEW_COUNT) {
            mWebViewPool[i] = createWebView(context, userAgent)
        }
    }

    /**
     * 获取webView
     */
    fun getWebView(): PkWebView? {
        checkIsInitialized()
        for (i in 0 until WEB_VIEW_COUNT) {
            if (mWebViewPool[i] != null) {
                val webView = mWebViewPool[i]
                mWebViewPool[i] = null
                return webView
            }
        }
        return null
    }

    private fun checkIsInitialized() {
        if (!::mWebViewPool.isInitialized) {
            throw UninitializedPropertyAccessException("Please call the PkWebViewInit.init method for initialization in the Application")
        }
    }

    /**
     * Activity销毁时需要释放当前WebView
     */
    fun releaseWebView(webView: PkWebView?) {
        checkIsInitialized()
        webView?.apply {
            stopLoading()
            removeAllViews()
            clearHistory()
            destroy()
            (parent as ViewGroup?)?.removeView(this)
            for (i in 0 until WEB_VIEW_COUNT) {
                if (mWebViewPool[i] == null) {
                    mWebViewPool[i] = createWebView(webView.context, mUserAgent)
                    return
                }
            }
        }

    }

    private fun createWebView(context: Context, userAgent: String): PkWebView? {
        val webView = PkWebView(context)
        WebViewManager.instance.apply {
            initWebViewSetting(webView, userAgent)
            initWebChromeClient(webView)
            initWebClient(webView)
        }
        return webView
    }
}

享元设计模式

享元设计模式.png

  • 我们通过创建一个固定大小的对象池,以避免频繁地创建和销毁WebView实例。
  • 在初始化时,将创建WebView实例并存储在对象池中。
  • 当需要WebView时,从对象池中获取空闲的WebView实例,而不是每次都创建新的实例。
  • 通过这种方式,可以节省内存和CPU资源,并提高应用程序性能。

建造者设计模式

建造者设计模式.png 在之前,我们对WebView缓存池初始化的时候,利用了一个PkWebViewInit类

class PkWebViewInit private constructor() {
    companion object  {
        val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
            PkWebViewInit()
        }

    }

    fun init(context: Context?,userAgent:String="") {
        WebViewPool.instance.initWebViewPool(context,userAgent)
    }

}

之前WebViewPool中对WebView进行参数设置,如WebViewSetting、WebViewClient等都是写在架构库里面的,对于用户来说是不可改变的,我们需要支持用户可以进行扩展或者修改,应该怎么办?

现在假设我们有个需求:用户需要动态设置UserAgent,WebViewSetting、WebViewClient和WebView的数量等,最后创建一个用户自定义的WebView。

这种将一个复杂对象的构建过程分解为多个简单对象的构建过程,使得同样的构建过程可以创建不同的表示,使用的设计模式便是建造者设计模式

由于代码过长,这里只截取部分代码,大家可以自行查看源码PkWebViewInit,以下是部分代码

class PkWebViewInit private constructor() {
    private var mWebViewController: WebViewController = WebViewController()

    fun initPool() {
        WebViewPool.instance.initWebViewPool(mWebViewController.P)
    }

    class Builder constructor(context: Context) {
        private val P: WebViewController.WebViewParams
        private var mPkWebViewInit: PkWebViewInit? = null

        init {
            P = WebViewController.WebViewParams(context)
        }

        /**
         * 设置WebViewSetting
         */
        fun setWebViewSetting(webViewSetting: InitWebViewSetting): Builder {
            P.mWebViewSetting = webViewSetting
            return this
        }
        /**
         * 设置UserAgent
         */
        fun setUserAgent(userAgent: String = ""): Builder {
            P.userAgent = userAgent
            return this
        }

        /**
         * 设置webView的count
         */
        fun setWebViewCount(count: Int): Builder {
            P.mWebViewCount = count
            return this
        }

        fun build() {
            if (mPkWebViewInit == null) {
                create()
            }
            mPkWebViewInit!!.initPool()
        }

        private fun create(): PkWebViewInit {
            val pkWebViewInit = PkWebViewInit()
            P.apply(pkWebViewInit.mWebViewController, P)
            mPkWebViewInit = pkWebViewInit
            return pkWebViewInit
        }
    }

}
internal class WebViewController {
    var P: WebViewParams? = null
        private set

    class WebViewParams(val context: Context) {
        var mWebViewCount: Int = 3
        var userAgent: String = ""
        var mWebViewSetting: InitWebViewSetting = DefaultInitWebViewSetting()

        var mWebViewClientCallback: WebViewClientCallback = DefaultWebViewClientCallback()
        var mWebViewChromeClientCallback: WebViewChromeClientCallback =
            DefaultWebViewChromeClientCallback()

        fun apply(controller: WebViewController, P: WebViewParams) {
            controller.P = P
        }
    }
}

上述通过Builder设计模式将所有客户端传入的参数封装到WebViewController.WebViewParams,在build之后将WebViewController.WebViewParams参数传给了WebViewPool.initWebViewPool进行创建WebView缓存池

策略设计模式:对WebViewSetting进行封装

策略设计模式.png 当我们需要通过不同的实现类对某个方法进行具体的实现,以便在运行时动态切换成不同的策略,这时候就可以考虑用策略设计模式

interface InitWebViewSetting {
    fun initWebViewSetting(webView: WebView, userAgent: String? = null)
}
class DefaultInitWebViewSetting :InitWebViewSetting{
    override fun initWebViewSetting(webView: WebView, userAgent: String?) {
        val webSettings: WebSettings = webView.settings
        WebView.setWebContentsDebuggingEnabled(true)
        webSettings.apply {
            //允许网页JavaScript
            javaScriptEnabled = true
            allowFileAccess = true
            useWideViewPort = true
            // 支持混合内容(https和http共存)
            mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
            // 开启DOM存储API
            domStorageEnabled = true
            // 开启数据库存储API
            databaseEnabled = true
            //不允许WebView中跳转到其他链接
            setSupportMultipleWindows(false)
            setSupportZoom(false)
            builtInZoomControls = false
            //cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
        }
        if (!TextUtils.isEmpty(userAgent)) {
            webSettings.userAgentString = userAgent
        }
        CookieManager.setAcceptFileSchemeCookies(true)
        CookieManager.getInstance().setAcceptCookie(true)
    }
}

大家可以实现InitWebViewSetting的接口,重写initWebViewSetting方法,然后通过PkWebViewInit#Builder.setWebViewSetting实现不同的WebViewSetting策略

桥接设计模式:对WebViewClient进行封装

桥接设计模式.png WebViewClient可以拦截WebView的各种事件,如页面的开始加载、加载完成、加载错误等,并允许开发者自定义相应的处理逻辑,从而实现更灵活、定制化的WebView行为。 通常我们一般的写法

public class PkWebViewClient extends WebViewClient {
    private ProgressBar mProgressBar;
    public PkWebViewClient(ProgressBar progressBar) {
        mProgressBar = progressBar;
    }
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        view.loadUrl(url);
        return true;
    }
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        mProgressBar.setVisibility(ProgressBar.VISIBLE);
    }
    @Override
    public void onPageFinished(WebView view, String url) {
        mProgressBar.setVisibility(ProgressBar.GONE);
    }
}
webView.setWebViewClient(new PkWebViewClient())

我们需求是用户可自定义WebViewClient,那肯定有人说,我们可以继续用策略设计模式呀

interface InitWebViewClient {
    fun initWebViewClient(webView: WebView, userAgent: String? = null) 
}
class DefaultInitWebViewSetting :InitWebViewSetting{
     override fun initWebViewClient(webView: WebView, userAgent: String? = null) {
        webView.setWebViewClient(new PkWebViewClient())
    }
}

但是对于用户来说,他并不想自己再次调用setWebViewClient,或者来说,用户的行为其实是不确定的,他可能调用setWebViewClient,也有可能调用setWebViewChromeClient方法,所以setWebViewClient这个方法我们需要内部设置了,把PkWebViewClient这个实现提供出去。

对于客户端来说最简单的方式只需要实现一个接口,并重写接口中的所有的方法就可以实现自己想要的逻辑是最好的。

interface WebViewClientCallback {
    fun onPageStarted(view: WebView, url: String,fragment: WebViewFragment?)
    fun onPageFinished(view: WebView, url: String, fragment: WebViewFragment?)
    fun shouldOverrideUrlLoading(view: WebView, url: String,fragment: WebViewFragment?): Boolean
    fun onReceivedError(view: WebView?, err: Int, des: String?, url: String?,fragment: WebViewFragment?)
}
class ReplaceWebViewClient:WebViewClientCallback {
    override fun onPageStarted(view: WebView, url: String, fragment: WebViewFragment?) {

    }

    override fun onPageFinished(view: WebView, url: String, fragment: WebViewFragment?) {

    }

    override fun shouldOverrideUrlLoading(
        view: WebView,
        url: String,
        fragment: WebViewFragment?
    ): Boolean {
       return false
    }

    override fun onReceivedError(
        view: WebView?,
        err: Int,
        des: String?,
        url: String?,
        fragment: WebViewFragment?
    ) {
    }
}

初始化的时候直接调用setWebViewClient即可

PkWebViewInit.Builder(this)
    .setWebViewClient(ReplaceWebViewClient())
    .build()

WebView架构库中我们需要将回调与WebViewClient进行绑定

class MyWebViewClient constructor(val webViewClientCallback: WebViewClientCallback?) :
    WebViewClient() {
    private var fragment: WebViewFragment? = null
    
    override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
        if (fragment == null) {
            fragment = WebViewManager.instance.getFragment()
        }
        fragment?.onPageStarted(view, url)
        webViewClientCallback?.onPageStarted(view, url, fragment)
    }

    override fun onPageFinished(view: WebView, url: String) {
        if (fragment == null) {
            fragment = WebViewManager.instance.getFragment()
        }
        fragment?.onPageFinished(view, url)
        webViewClientCallback?.onPageFinished(
            view,
            url,
            fragment
        )

    }
}

这样MyWebViewClient和WebViewClientCallback就进行联动,接着webView调用setWebViewClient即可 WebViewPool.createWebView

private fun createWebView(
    params: WebViewController.WebViewParams, userAgent: String
): PkWebView {
    val webView = PkWebView(params.context)
    params.apply {
        mWebViewSetting.initWebViewSetting(webView, userAgent)
        webView.webViewClient=MyWebViewClient(params.mWebViewClientCallback)
    }
    return webView
}

按道理上面其实已经可以达到我们想要的效果,但是我们会发现这里createWebView其实是干了两件事情,

  1. 创建WebView
  2. webView设置WebViewClient参数

也就违背了单一职责,那什么是单一职责呢? 但一职责是指一个类或者方法只有一项职责,即只负责一件事情。这个原则旨在降低类或者方法的复杂度,提高代码的可维护性和可读性。如果一个类或者方法负责多个职责,那么它的修改和维护会变得困难,也容易引起意外的影响。因此,单一职责原则是面向对象编程中非常重要的一个设计原则。

所以这里将webView.webViewClient=MyWebViewClient(params.mWebViewClientCallback)方法放到另一个类进行,这个类提供一个initWebViewClient方法。

internal class WebViewClientImpl(webViewClientCallback: WebViewClientCallback?) :
    AbstractWebViewClient(webViewClientCallback) {
    override fun initWebClient(webView: WebView) {
        val webViewClient = WebViewClientImpl(webViewClientCallback)
        webView.webViewClient = webViewClient
    }
}

将MyWebViewClient变成抽象类,并提供initWebViewClient

class AbstractWebViewClient constructor(val webViewClientCallback: WebViewClientCallback?) :
    WebViewClient() {
    private var fragment: WebViewFragment? = null
  
    abstract  fun initWebClient(webView: WebView)
    override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
        if (fragment == null) {
            fragment = WebViewManager.instance.getFragment()
        }
        fragment?.onPageStarted(view, url)
        webViewClientCallback?.onPageStarted(view, url, fragment)
    }

    override fun onPageFinished(view: WebView, url: String) {
        if (fragment == null) {
            fragment = WebViewManager.instance.getFragment()
        }
        fragment?.onPageFinished(view, url)
        webViewClientCallback?.onPageFinished(
            view,
            url,
            fragment
        )

    }
}
小总结

什么是桥接设计模式

桥接设计模式是一种结构性设计模式,用于将抽象部分与实现部分分离,从而使他们可以独立的变化。桥接模式为这两个部分提供了不同的层次结构,并通过委托机制将它们连接起来。这样,可以在不修改原有代码的情况下,动态地改变它们之间的关系,以适应新的需求或平台。

优点

  • 将抽象部分与实现部分分离,使得它们可以独立地变化(上面的AbstractWebViewClient和WebViewClientCallback)。
  • 可以在运行时动态地改变抽象接口的实现部分,而无需修改已经存在的代码。
  • 可以对实现部分进行扩展,而无需修改抽象部分的代码。
  • 提高了代码的可扩展性和可维护性,使得程序更易于理解和修改

装饰设计模式

装饰设计模式.png 用户启动WebViewActivity的时候,一般的时候都是这么调用

val bean= WebViewConfigBean("https://www.baidu.com")
val intent=Intent(this, WebViewActivity::class.java)
 intent.putExtra(WebViewHelper.LIBRARY_WEB_VIEW,bean)
 startActivity(intent)

用户有时候并不知道你里面的的key是多少,也不并关心,只想传入一个url,你就能打开就行了,所以我们就可以创建一个类统一管理WebViewActivity的启动

class DefaultH5IntentConfigImpl {
     fun startActivity(context: Context?, url: String) {
        context?.startActivity(
            Intent(context, WebViewActivity::class.java)
                .putExtra(WebViewConstants.LIBRARY_WEB_VIEW, WebViewConfigBean(url))
        )
    }

     fun startActivityForResult(context: Activity?, url: String, requestCode: Int) {
        val intent = Intent(context, WebViewActivity::class.java)
            .putExtra(WebViewConstants.LIBRARY_WEB_VIEW, WebViewConfigBean(url))
        context?.startActivityForResult(intent, requestCode)
    }

     fun startActivityForResult(context: Fragment?, url: String, requestCode: Int) {
        if (context == null || context.context == null) return
        val intent = Intent(context.context, WebViewActivity::class.java)
            .putExtra(WebViewConstants.LIBRARY_WEB_VIEW, WebViewConfigBean(url))
        context.startActivityForResult(intent, requestCode)
    }


     fun startActivityForResult(
        context: FragmentActivity?,
        launcher: ActivityResultLauncher<Intent>?,
        url: String
    ) {
        val intent = Intent(context, WebViewActivity::class.java)
            .putExtra(WebViewConstants.LIBRARY_WEB_VIEW, WebViewConfigBean(url))
        launcher?.launch(intent)
    }

     fun startActivityForResult(
        context: Fragment?,
        launcher: ActivityResultLauncher<Intent>?,
        url: String
    ) {
        if (context == null || context.context == null) return
        val intent = Intent(context.context, WebViewActivity::class.java)
            .putExtra(WebViewConstants.LIBRARY_WEB_VIEW, WebViewConfigBean(url))
        launcher?.launch(intent)
    }
}

这是最基础的startActivity,如果用户想对这基础的startActivity修改呢?于是我们就可以定义一个接口

interface H5IntentConfig {
    fun startActivity(context: Context?, url: String)
    fun startActivityForResult(context: Activity?, url: String, requestCode: Int)
    fun startActivityForResult(context: Fragment?, url: String, requestCode: Int)
    fun startActivityForResult(
        context: FragmentActivity?,
        launcher: ActivityResultLauncher<Intent>?,
        url: String
    )
    fun startActivityForResult(
        context: Fragment?,
        launcher: ActivityResultLauncher<Intent>?,
        url: String
    )
}

之后让DefaultH5IntentConfigImpl实现H5IntentConfig接口即可,随后创建H5Utils

class H5Utils(decoratorConfig: H5IntentConfig = DefaultH5IntentConfigImpl())

基础的startActivity功能已完成,但是此时用户说我想在不改基础的startActivity的基础之上,对url进行重新拼接,比如加上token,应该怎么做?这时候我就可以用装饰设计模式,对H5IntentConfig功能进行扩展

abstract class AbstractH5IntentConfigDecorator(private val decoratorConfig: H5IntentConfig) :
    H5IntentConfig {
    override fun startActivity(context: Context?, url: String) {
        decoratorConfig.startActivity(context, url)
    }

    override fun startActivityForResult(context: Activity?, url: String, requestCode: Int) {
        decoratorConfig.startActivityForResult(context, url, requestCode)
    }

    override fun startActivityForResult(context: Fragment?, url: String, requestCode: Int) {
        decoratorConfig.startActivityForResult(context, url, requestCode)
    }
    override fun startActivityForResult(
        context: Fragment?,
        launcher: ActivityResultLauncher<Intent>?,
        url: String
    ) {
        decoratorConfig.startActivityForResult(context,launcher,url)
    }

    override fun startActivityForResult(
        context: FragmentActivity?,
        launcher: ActivityResultLauncher<Intent>?,
        url: String
    ) {
        decoratorConfig.startActivityForResult(context,launcher,url)
    }
}
class H5Utils(decoratorConfig: H5IntentConfig = DefaultH5IntentConfigImpl()) :
    AbstractH5IntentConfigDecorator(decoratorConfig)

使用也就变得简单了

H5Utils(ReplaceH5ConfigDecorator())
    .startActivityForResult(
        this, launcher,
        "https://qa-xbu-activity.at-our.com/mallIndex")

基础startActivity功能替换

class ReplaceH5IntentConfigImpl:H5IntentConfig {
    override fun startActivity(context: Context?, url: String) {
      Log.e("TAG","ReplaceH5IntentConfigImpl url:$url")
    }

    override fun startActivityForResult(context: Activity?, url: String, requestCode: Int) {
    }

    override fun startActivityForResult(context: Fragment?, url: String, requestCode: Int) {

    }

    override fun startActivityForResult(
        context: FragmentActivity?,
        launcher: ActivityResultLauncher<Intent>?,
        url: String
    ) {

    }

    override fun startActivityForResult(
        context: Fragment?,
        launcher: ActivityResultLauncher<Intent>?,
        url: String
    ) {

    }

}

装饰设计模式扩展功能修改,对url扩展,添加token

class ReplaceH5ConfigDecorator:AbstractH5IntentConfigDecorator(ReplaceH5IntentConfigImpl()){
    override fun startActivity(context: Context?, url: String) {
        var replaceUrl=url
        replaceUrl+="/token?=111"
        super.startActivity(context, replaceUrl)
    }
}

结语

通过使用设计模式,我们可以更好地封装WebView,并提高Android应用的代码质量和开发效率。在上文中,我们用到了单例设计模式、享元设计模式、建造者设计模式、策略设计模式、桥接设计模式和装饰设计模式。

以策略模式为例,我们将WebViewClient初始化的责任分离出俩,并通过定义接口和实现类来实现高度灵活的定制化。这种方式使得我们能够快速、方便地实现个性化需求,同时也让整个应用的代码结构更加清晰和易于维护。

根据实际情况选择合适的模式进行设计和实现,既能提高应用的可扩展性和可维护性,也能够帮组开发人员更好地理解和掌握面向对象编程的思想。

本项目源码:PkWebView