Android进阶宝典 --- 从0到1搭建高效WebView框架

2,125 阅读11分钟

在如今的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框架,就采用组件化的思想

image.png

image.png

基础的架构就是如此,最底层的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跨进程的通信,在下个章节中会着重介绍。

附录整体架构图

image.png

base层:提供全局状态UI、ServiceLoader工具类
business_common层:提供路由接口,例如IWebViewService
business_web层:webview的业务模块,路由接口的具体实现,webview(activity + Fragment)支持,webview的setting配置
app层:依赖business_web