在如今的App开发中,如果说纯原生去做,成本是非常高的,而且线上一旦出了问题,需要即时发版处理。因此针对一些高频、可能会出问题的模块,通常会选择使用H5代替,如果线上出现问题,H5发版速度是很快的,而且用户是无感知的,但这也会带来一些问题,最大的问题就是性能的损耗。
一个页面如果使用H5来做,假设只有一个WebView,它的生命周期我们先来了解下,webview的基础配置就不讲了,当我们进到这个H5页面之后,需要loadUrl
//加载H5地址
loadUrl("https://www.baidu.com")
webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
return super.shouldInterceptRequest(view, request)
}
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
view!!.loadUrl(url!!)
return true
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
loadStart = false
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
loadFinished = true
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
loadResourceFailed = true
Log.e(TAG, "onReceivedError -- $error")
}
}
如果Android需要和H5通信,那么需要等到webview回调onPageFinished之后,才能往里塞数据;onPageFinished意味着webview整个初始化流程完成,可以渲染数据了。
那么问题就是这里,因为webview渲染需要时间,如果我们使用过webview,会发现加载的时候就是一个大白屏,当然我们可以采取任意方式,在webview加载完成之后再展示,但是中间的这个过程和时间是无法避免的,用户会明显感知,加载了很长时间才展示页面,很多人会认为这就是一个bug
第二个问题就是,webview其实是跟其他模块解耦的,因为webview加载我们很难深入内部处理,如果出了问题,肯定会直接将整个app进程干掉,因此设计webview架构需要将其放在单独的一个进程中,既然放在单独的进程中,就势必会涉及到原生与H5的跨进程通信。
因此从优化的角度来讲,高性能、高可靠才是webview使用的关键!
1 准备工作
前期准备工作,我们的目标是这个webview框架能够直接在项目中使用,现在项目大部分都是使用到了组件化的设计思想,因此实现这个webview框架,就采用组件化的思想
基础的架构就是如此,最底层的base下沉,然后common层用于给各个业务组件提供支撑,本章的webview组件就是在业务层,依赖common层,最顶层的app就是壳
一般场景下,webview会被放在activity或者Fragment当中,因此拿activity举例,需要一个WebViewActivity
# business_web # WebViewActivity
class WebViewActivity : AppCompatActivity() {
private lateinit var binding:ActivityWebViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWebViewBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.webview.loadUrl("https://wwww.baidu.com")
}
}
这里只是简单放置一个WebView,然后加载百度的网页
1.1 AutoService的路由妙招
如果涉及到组件化,那么路由是必不可少的;每个模块都有不同的页面,如果在app中启动这些页面,则需要依赖这些模块,例如我要启动WebViewActivity,app中需要依赖business_web
implementation project(':business:business_web')
如果模块特别多,那么岂不是要依赖很多module,其实不然,如果我们用过ARouter,就会知道路由跳转并不需要相互依赖,但是如果不想使用ARouter(确实不好用,本人很排斥!),在之前编译时技术中,提到过AutoSerivce,利用其便可实现简单路由。
AutoSerivce是Google的编译时服务,这个不做过多介绍,会在编译时扫描整个工程,拿到@AutoService注解,此注解值通常是某个接口,注解用来标注这个接口的实现类
interface IWebViewService {
/**
* @param url webview加载的地址
* @param title 页面的标题
*/
fun startWebPage(context: Context, url: String, title: String)
}
例如打开这个WebView,可以在common层下沉接口,各个业务模块盘点哪些界面需要打开,并且需要传什么值,然后具体的实现就是在业务模块。
@AutoService(IWebViewService::class)
class WebViewServiceImpl : IWebViewService {
override fun startWebPage(context: Context, url: String, title: String) {
val intent = Intent()
intent.putExtra("url", url)
intent.putExtra("title", title)
intent.setClass(context, WebViewActivity::class.java)
context.startActivity(intent)
}
}
此时需要在业务模块中引入autoservice
implementation 'com.google.auto.service:auto-service:1.0-rc7'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
这里如果使用Kotlin实现的话,会找不到这个service,那么需要加上以下的配置,首先引入2个插件
id 'kotlin-kapt'
id 'kotlin-android'
在依赖中,添加kapt解释这个注解
kapt 'com.google.auto.service:auto-service:1.0-rc7'
这样,无论kotlin还是java,都能够使用autoservice注解完成路由跳转
findViewById<Button>(R.id.btn_open_web).setOnClickListener {
//寻址
val web = ServiceLoader.load(IWebViewService::class.java).iterator().next()
web?.startWebPage(this,"https://www.baidu.com","web_page")
}
ServiceLoader用来加载这些路由接口得到真正的实现类,因为实现类可能会有很多,因此得到的是一个集合,这里其实就一个实现类WebViewServiceImpl,拿到实现类调用startWebPage实现跳转
/**
* 用来加载接口获取实现类
*/
object AutoServiceLoader {
fun <T> load(clazz: Class<T>) : T{
return ServiceLoader.load(clazz).iterator().next()
}
}
其实真正封装,在应用层只需要调用接口,而不需要知道我需要拿iterator来取值,这部分的api是可以下沉到base,作为一个工具类使用
findViewById<Button>(R.id.btn_open_web).setOnClickListener {
//寻址
AutoServiceLoader.load(IWebViewService::class.java)
.startWebPage(this, "https://www.baidu.com", "web_page")
}
是不是可以跟Arouter说再见了~
1.2 场景适配
上述场景是启动了一个Activity,也就是说我当前页面就是一整个webview,原生能做的就是比如action_bar等,但是实际的场景中,可能在某个页面中嵌入一个webview,然后在Fragment中嵌入一个webview,那么直接启动一个Fragment显然不现实的,那么这种场景怎么实现呢?
首先我们需要一个Fragment
class WebViewFragment : Fragment() {
private lateinit var binding:FragmentWebViewBinding
private var url:String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
url = it.getString("url").toString()
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentWebViewBinding.inflate(layoutInflater)
binding.webview.settings.javaScriptEnabled = true
binding.webview.loadUrl(url)
return binding.root
}
companion object {
fun getWebFragment(url:String) : WebViewFragment{
val fragment = WebViewFragment()
val bundle = Bundle()
bundle.putString("url",url)
fragment.arguments = bundle
return fragment
}
}
}
比如,我们在某个页面中,需要嵌入一个WebView(不区分全屏还是某块区域),例如在Activity中,想要拿到这个Fragment,需要再暴露一个接口getWebFragment
interface IWebViewService {
/**
* @param url webview加载的地址
* @param title 页面主题
*/
fun startWebPage(context: Context, url: String, title: String)
/**
* url 要加载的url地址
*/
fun getWebFragment(url:String):Fragment
}
具体实现就是创建这个Fragment
override fun getWebFragment(url: String): Fragment {
return WebViewFragment.getWebFragment(url)
}
比如在MainActivity中需要一个Webview页面,那么就将webview手动添加进来,通过getWebFragment获取Fragment的实例
val fragment = AutoServiceLoader.load(IWebViewService::class.java)
.getWebFragment("https://www.baidu.com")
supportFragmentManager.beginTransaction().replace(R.id.fl_web_container, fragment).commit()
2 WebView状态处理
在完成前期的准备工作之后,我们就可以灵活地使用webview,不管是直接启动一个webview页面,还是在页面的某一处赋予一个webFragment,都能够加载展示网页信息,那么当我们在启动webview的时候,都看到一个空白页,然后才展示数据页面,这个白屏的时长可能会很长,用户体验很差,如果从UI的角度来看,我们可以添加loading页面,然后监听webview加载完成之后,展示数据页面。
2.1 全局状态页面
在实际的项目开发中,我们会根据很多场景来展示不同的状态页面,例如loading页面、断网页面、网络崩溃页面、空页面等等,因为每个页面都会用到,因此需要下沉到base中。
/**
* 错误状态提示
*/
interface ErrorStatusHolder : LifecycleEventObserver {
var stateUI: StatusUIView?
var connectivityManager: ConnectivityManager?
var callback: ConnectivityManager.NetworkCallback?
/**
* 展示无网络状态弹窗
*/
fun showNoNetworkDialog(
context: Context,
block: () -> Unit,
lifecycleOwner: LifecycleOwner? = null,
refresh: (Boolean) -> Unit
) {
nullContextCheck(context)
ErrorStatusDialog(context).apply {
show()
setTitle("网络断开了,请检查网络~")
setButton("检查网络")
setConfirmClickListener {
block()
registerWifiStateCallback(context, lifecycleOwner, refresh)
dismiss()
}
}
}
/**
* 展示网络崩溃的弹窗
*/
fun showNetworkErrorDialog(context: Context, block: () -> Unit) {
nullContextCheck(context)
ErrorStatusDialog(context).apply {
show()
setTitle("加载失败,请重试")
setButton("重试")
setConfirmClickListener {
block()
dismiss()
}
}
}
/**
* 展示无网络页面(区分弹窗)
* 如果需要监听页面的状态,需要传入LifecycleOwner
*/
fun showNoNetwork(
context: Context,
block: () -> Unit,
lifecycleOwner: LifecycleOwner? = null,
refresh: (Boolean) -> Unit
) {
nullContextCheck(context)
if (getViewRoot() == null) {
KLog.e("ErrorStatusHolder", "root == null")
return
}
//注册
registerWifiStateCallback(context, lifecycleOwner, refresh)
stateUI = StatusUIView(context).apply {
setErrorState("网络断开了,请检查网络~")
setButton("检查网络")
setNetUIStatus(true)
setOnClickStateListener {
block()
}
setLoadStatus(false)
}
nullCheck()
getViewRoot()!!.addView(stateUI, center())
}
/**
* 展示网络崩溃页面(区分弹窗)
*/
fun showNetworkErrorView(context: Context, click: () -> Unit) {
nullContextCheck(context)
if (getViewRoot() == null) {
KLog.e("ErrorStatusHolder", "root == null")
return
}
stateUI = StatusUIView(context).apply {
setErrorState("加载失败,请重试")
setButton("重试")
setNetUIStatus(true)
setOnClickStateListener {
click()
getViewRoot()!!.removeAllViews()
}
setLoadStatus(false)
}
nullCheck()
getViewRoot()!!.addView(stateUI, center())
}
fun showEmptyView(
context: Context,
block: IEmptyFillSchedule? = null,
click: (() -> Unit)? = null
) {
nullContextCheck(context)
if (getViewRoot() == null) {
KLog.e("ErrorStatusHolder", "root == null")
return
}
stateUI = StatusUIView(context).apply {
setEmpty(true)
//配置空页面数据信息
block?.let {
setEmptyInfo(block.getEmptyContent())
setEmptyBtnText(block.getEmptyBtnContent())
}
setOnClickStateListener {
click?.invoke()
//TODO remove stateUI
}
}
nullCheck()
getViewRoot()!!.addView(stateUI, center())
}
//启动wifi连接状态判断
private fun registerWifiStateCallback(
context: Context,
lifecycleOwner: LifecycleOwner?,
refresh: (Boolean) -> Unit
) {
KLog.e("ErrorStatusHolder", "registerNetworkCallback ----")
nullContextCheck(context)
connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
//添加事件监听
lifecycleOwner?.lifecycle?.addObserver(this)
callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
//连接成功,解注册
unregister(callback = this, connectivityManager)
//移除View
Handler(Looper.getMainLooper()).post {
//wifi连接成功
refresh.invoke(true)
getViewRoot()?.removeAllViews()
}
}
override fun onUnavailable() {
super.onUnavailable()
refresh.invoke(false)
}
}
connectivityManager?.registerDefaultNetworkCallback(callback!!)
}
private fun unregister(
callback: ConnectivityManager.NetworkCallback,
connectivityManager: ConnectivityManager?
) {
KLog.e("ErrorStatusHolder", "unregisterNetworkCallback ----")
connectivityManager?.unregisterNetworkCallback(callback)
}
/**
* 去wifi设置页
*/
fun goWifiSetting(context: Context) {
val intent = Intent(Settings.ACTION_WIFI_SETTINGS)
context.startActivity(intent)
}
private fun center(): ViewGroup.LayoutParams {
return ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT, ConstraintLayout.LayoutParams.MATCH_PARENT
)
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
//销毁监听器
Log.e("ErrorStatusHolder","ON_DESTROY")
callback?.let {
Log.e("ErrorStatusHolder","unregister --- ")
unregister(it,connectivityManager)
}
}
}
/**
* 展示loading页面
*/
fun showLoading(context: Context) {
nullContextCheck(context)
stateUI = StatusUIView(context).apply {
setLoadStatus(true)
}
nullCheck()
getViewRoot()!!.addView(stateUI, center())
}
/**
* 隐藏loading页面
*/
fun hideLoading() {
nullCheck()
if (stateUI != null) {
getViewRoot()?.removeView(stateUI)
stateUI = null
}
}
fun getViewRoot(): ViewGroup? = null
private fun nullCheck() {
if (getViewRoot() == null) {
throw IllegalArgumentException("please add a container to full this view")
}
}
private fun nullContextCheck(context: Context?) {
if (context == null) {
return
}
}
}
interface IEmptyFillSchedule {
fun getEmptyContent(): String = ""
fun getEmptyBtnContent(): String = ""
}
这个是在之前的项目中,写过的一个关于全局异常状态页面展示,当然每个项目中异常状态页面是以UI为准,设计师通常会统一这些状态,因此除了文案,UI都是通用的,主要就是在界面中设置一个承接异常状态的容器,当异常发生时,在容器中添加异常UI。
那么我在什么时候显示loading,什么时候关闭loading呢,那就需要回到前面文章一开头,有一个WebviewClient
2.2 webview的生命周期
webview的生命周期是通过WebViewClient监听,不管是普通的webview还是jsbridge,都是通过onPageStarted、onPageFinished、onReceivedError来回调webview的生命周期
class MyWebViewClient(var callback: IWebViewCallback? = null) : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
callback?.onPageStart(url)
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
callback?.onPageFinish(url)
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
callback?.onPageError(error)
}
}
interface IWebViewCallback {
fun onPageStart(url: String?)
fun onPageFinish(url: String?)
fun onPageError(exception: WebResourceError?)
}
具体使用如下,通过设置webview的webViewClient属性,从而获取webview加载的回调;当webview开始加载的时候,展示loading。
从代码中可以看出,WebViewFragment实现了ErrorStatusHolder接口,然后重写getViewRoot方法将状态UI塞到csStatusHolder容器中,然后调用showLoading或者hideLoading方法,意味着状态UI从容器添加到移除
class WebViewFragment : Fragment(), ErrorStatusHolder {
private lateinit var binding: FragmentWebViewBinding
private var url: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
url = it.getString("url").toString()
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentWebViewBinding.inflate(layoutInflater)
binding.webview.settings.javaScriptEnabled = true
binding.webview.loadUrl(url)
binding.webview.webViewClient = MyWebViewClient(object : IWebViewCallback {
override fun onPageStart(url: String?) {
//展示loading
showLoading(requireContext())
}
override fun onPageFinish(url: String?) {
//取消loading
hideLoading()
}
override fun onPageError(exception: WebResourceError?) {
//取消loading
showNetworkErrorView(requireContext()){
binding.webview.reload()
}
}
})
return binding.root
}
override fun getViewRoot(): ViewGroup? {
return binding.csStatusHolder
}
companion object {
fun getWebFragment(url: String): WebViewFragment {
val fragment = WebViewFragment()
val bundle = Bundle()
bundle.putString("url", url)
fragment.arguments = bundle
return fragment
}
}
override var stateUI: StatusUIView? = null
override var connectivityManager: ConnectivityManager? = null
override var callback: ConnectivityManager.NetworkCallback? = null
}
如果因为网络的原因,导致webview加载失败,webview的onReceivedError会回调,可以选择调用webview的reload方法重新加载;
这里有个问题,网络异常页面在onPageError回调中处理合理吗?因为当webview的onReceivedError回调之后,onPageFinished也会被回调,因为在onPageFinished中都是做的webview加载成功的处理,所以需要使用标志位isError来确认,此次加载完成,是成功还是失败的。
override fun onPageFinish(url: String?) {
if (isError) {
showNetworkErrorView(requireContext()) {
binding.webview.reload()
}
} else {
hideLoading()
}
isError = false
}
override fun onPageError(exception: WebResourceError?) {
isError = true
}
2.3 真正实现JS交互的WebChromClient
前面介绍完WebClient,我们知道了WebView的加载的生命周期,那么当我们想实现Android和H5的交互,就需要WebChromClient来帮忙了。
class MyWebChromeClient(var callback: IWebViewCallback? = null) : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
super.onProgressChanged(view, newProgress)
}
override fun onReceivedTitle(view: WebView?, title: String?) {
super.onReceivedTitle(view, title)
callback?.onTitleReceive(title)
}
override fun onJsAlert(
view: WebView?,
url: String?,
message: String?,
result: JsResult?
): Boolean {
return super.onJsAlert(view, url, message, result)
}
override fun onJsConfirm(
view: WebView?,
url: String?,
message: String?,
result: JsResult?
): Boolean {
return super.onJsConfirm(view, url, message, result)
}
override fun onJsPrompt(
view: WebView?,
url: String?,
message: String?,
defaultValue: String?,
result: JsPromptResult?
): Boolean {
return super.onJsPrompt(view, url, message, defaultValue, result)
}
}
在WebChromeClient中,我们可以监听webview加载的进度、获取webview的标题,像onJsAlert、onJsConfirm、onJsPrompt用于处理js的弹窗、输入框等交互,这个后续的专题中会持续介绍
binding.webview.webChromeClient = MyWebChromeClient(object : IWebViewCallback{
override fun onTitleReceive(title: String?) {
//设置标题
}
})
其实,从这一个小的专题中我们就能对webview有一个初步的认识,其实这只是webview的冰山一角,关键在于Android与H5之间的通信,与webview跨进程的通信,在下个章节中会着重介绍。
附录整体架构图
base层:提供全局状态UI、ServiceLoader工具类
business_common层:提供路由接口,例如IWebViewService
business_web层:webview的业务模块,路由接口的具体实现,webview(activity + Fragment)支持,webview的setting配置
app层:依赖business_web