前言
在上篇文章中,我们介绍了如何使用全局缓存池管理来提高WebView的加载速度,源码位置:WebViewPool.kt
在本篇文章中,我们将介绍如何使用设计模式来封装WebView组件,以实现个性化定制并让你的App网页更加顺畅。通过本文的学习,您将掌握如何使用单例、享元、建造者、桥接和装饰等设计模式来优化WebView组件,并且实现不同场景下的灵活配置。
单例设计模式
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
}
}
享元设计模式
- 我们通过创建一个固定大小的对象池,以避免频繁地创建和销毁WebView实例。
- 在初始化时,将创建WebView实例并存储在对象池中。
- 当需要WebView时,从对象池中获取空闲的WebView实例,而不是每次都创建新的实例。
- 通过这种方式,可以节省内存和CPU资源,并提高应用程序性能。
建造者设计模式
在之前,我们对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进行封装
当我们需要通过不同的实现类对某个方法进行具体的实现,以便在运行时动态切换成不同的策略,这时候就可以考虑用策略设计模式
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进行封装
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其实是干了两件事情,
- 创建WebView
- 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)。
- 可以在运行时动态地改变抽象接口的实现部分,而无需修改已经存在的代码。
- 可以对实现部分进行扩展,而无需修改抽象部分的代码。
- 提高了代码的可扩展性和可维护性,使得程序更易于理解和修改
装饰设计模式
用户启动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