Android WebView 一孔之见(上)

2,659 阅读7分钟

Android WebView 一孔之见(上)

什么是WebView?

WebView是Android的一个组件,用于显示web内容,例如带有JavaScript和CSS的HTML文件。显示的web内容可以从互联网下载,也可以作为本地资源包含在应用程序中,渲染引擎是WebKit,Android4.4之后直接使用了Chrome。

WebView有哪些用途?

WebView除了具有View控件的属性之外,对于url请求、页面加载、渲染、界面交互等方面给予了很大的技术支持

  • 显示和渲染Web界面
  • 直接使用html文件做布局(网络文件/本地文件)
  • 与JavaScript交互调用

如何使用WebView?

WebView的基础使用

①添加网络权限(AndroidMainfest.xml)

<uses-permission android:name="android.permission.INTERNET"/>

②添加WebView组件(layout.xml)

<WebView
    android:id="@+id/webView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

③配置相关属性(MainActivity.kt)

//是否与JavaScript交互
webView.settings.javaScriptEnabled = true
//是否打开Dom本地存储
webView.settings.domStorageEnabled = true
webView.loadUrl("https://www.baidu.com")
//是否自适应屏幕
webView.settings.useWideViewPort = true  //将图片调整至适合webView的大小
webView.settings.loadWithOverviewMode = true  //缩放至屏幕大小

//是否缩放
webView.settings.setSupportZoom(true)  //支持缩放
webView.settings.builtInZoomControls = true  //设置内置的缩放控件
webView.settings.displayZoomControls = false  //隐藏原生的缩放控件

//more settings
webView.settings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK  //关闭webView中缓存
webView.settings.allowFileAccess = true  //设置可以访问文件
webView.settings.javaScriptCanOpenWindowsAutomatically = true  //支持JS打开新窗口
webView.settings.loadsImagesAutomatically = true  //支持自动加载图片
webView.settings.defaultTextEncodingName = "utf-8"  //设置编码格式

点击查看更多属性

④设置webView的不同状态

override fun onResume() {
    super.onResume()
    //活跃状态->使WebView可以正常执行网页的响应
    webView.onResume()
}

override fun onPause() {
    super.onPause()
    //暂停状态->当页面失去焦点切换至后台时通知内核暂停DOM解析、插件执行、JavaScript等动作
    webView.onPause()
    //暂停所有webView的layout、parsing、javascript,降低cpu功耗
    webView.pauseTimers()
}

override fun onResumeFragments() {
    super.onResumeFragments()
    //恢复运行->恢复pauseTimer状态
    webView.resumeTimers()
}

override fun onDestroy() {
    super.onDestroy()
    //自定义WebView构建时传入了Activity的context对象,因此需要将其移出父容器再销毁
    val viewGroup: ViewGroup = webView.parent as ViewGroup
    if (viewGroup != null) {
        viewGroup.removeView(webView)
    }
    webView.destroy()
}

⑤设置辅助方法

  • onKeyDown(Int keyCode,KeyEvent event)
//使Back键控制网页后退
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) {
        webView.goBack()
        return true
    }
    return super.onKeyDown(keyCode, event)
}

WebView相关类解析

WebViewClient类

用途:处理各种通知&&请求事件

  • shouldOverrideUrlLoading(WebView view,String url)

当即将在当前 WebView 中加载 URL 时,让宿主应用程序有机会进行控制。如果未提供 WebViewClient,默认情况下 WebView 将要求 Activity Manager 为 URL 选择正确的处理程序。如果提供了 WebViewClient,返回 true会导致当前 WebView 中止加载 URL,而返回 false会导致 WebView 像往常一样继续加载 URL。

override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {

    if (url == null) return false

    try {
        //针对于自定义协议的url,转换成原生调用(intent)
        if (!url.startsWith("http://") && !url.startsWith("https://")) {
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
            startActivity(intent)
            return true
        }
    } catch (e: Exception) { //防止crash (如果手机上没有安装处理对应scheme开头的url的APP, 会导致crash)
        return true //没有安装该app时,返回true,表示拦截自定义链接,但不跳转,避免弹出ERR_UNKNOWN_URL_SCHEME
    }
    //处理http/https开头的url
    view?.loadUrl(url)
    Log.d(TAG, "shouldOverrideUrlLoading: 在WebView加载的url is ${webView.url}")
    return true
}
  • onPageStarted(WebView view,String url,Bitmap favicon)

通知主机应用程序页面已开始加载,每次加载主框架时都会调用一次此方法,可用于设定loading的页面,告诉用户程序在等待网络响应

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
    Log.d(TAG, "onPageStarted: $url 等待网络响应")
}
  • onPageFinished(WebView view,String url)

通知主机应用程序页面已完成加载,可用于关闭loading条,切换程序动作

override fun onPageFinished(view: WebView?, url: String?) {
    Log.d(TAG, "onPageFinished: $url 页面加载结束")
}
  • onReceivedError(WebView view,WebResourceRequest request,WebResourceError error)

向主机应用程序报告错误,在加载页面的服务器出现错误时调用,例如出现 404 可以手写html用于服务器错误时加载

override fun onReceivedError(
    view: WebView?,
    request: WebResourceRequest?,
    error: WebResourceError?
) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        when (error?.errorCode) {    //根据传回的错误码,进行对应的错误处理
            HttpURLConnection.HTTP_NOT_FOUND -> {
                Log.d(TAG, "onReceivedError: 404 not found")
            }
        }
    }
}

点击查看更多方法

image.png

状态码知识拓展

WebChromClient类

用途:辅助WebView处理JavaScript的对话框、图标、标题等

  • onProgressChanged(WebView view,String url,boolean precomposed)

告诉宿主应用程序加载页面的当前进度。

override fun onProgressChanged(view: WebView?, newProgress: Int) {
         if (newProgress < 100){
             Log.d(TAG, "onProgressChanged: 当前加载进度是 $newProgress")
         }else{
             Log.d(TAG, "onProgressChanged: 加载成功")
         }
}
  • onReceivedTitle(WebView view,String title)

通知主机应用程序文档标题的更改。

override fun onReceivedTitle(view: WebView?, title: String?) {
     Log.d(TAG, "onReceivedTitle: $title")
}

点击查看更多方法

实践中遇到的一些问题

Android WebView加载网页出错:net:ERR_UNKNOWN_URL_SCHEME

原因分析:

webView只能识别http,https这些传统的协议,类似于百度(baidu://)、微信(weixin://)、支付宝(alipay://)这些自定义的协议,webView无法进行识别,因此会出现 net:ERR_UNKNOWN_URL_SCHEME 的Error

初步解析:

一般情况下,浏览器跳转三方应用采用一种:自定义Url Scheme的方式打开,若遇到http/https开头的url,webView会向host发起一个请求,然而如果url是自定义协议,且手机没有安装处理对应scheme开头的url的App,webView将不知道如何处理,最终出现ERR_UNKNOWN_URL_SCHEME

知识拓展

URL Scheme

URL scheme是App提供给外部的可以直接操作App的规则。

juejin.cn/ 是一个URL,在://之前的部分就称为URL Scheme,即掘金的URL Scheme 是 https

  • 比如微信提供了打开扫一扫的URL scheme。weixin://dl/scan -> weixin
  • 比如支付宝提供了转账的URL scheme。alipayqr://platformapi/startapp?saId= -> alipayqr
  • 比如知乎提供了打开回答页面的URL scheme。zhihu://answers/{id} -> zhihu

解决方法:

重写网页加载都要经过的函数:shouldOverrideUrlLoading,针对于自定义协议开头的url,转换成原生调用(intent跳转)

            /**
             * 前提:设置了WebViewClient
             * true: 拦截WebView加载url,由应用的代码处理,即程序员自己做处理
             * false:允许WebView加载url,由WebView处理 
             */
            override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {

                if (url == null) return false

                try {
                    //针对于自定义协议的url,转换成原生调用(intent)
                    if (!url.startsWith("http://") && !url.startsWith("https://")) {
                        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                        startActivity(intent)
                        return true
                    }
                } catch (e: Exception) { //防止crash (如果手机上没有安装处理对应scheme开头的url的APP, 会导致crash)
                    return true //没有安装该app时,返回true,表示拦截自定义链接,但不跳转,避免弹出ERR_UNKNOWN_URL_SCHEME
                }
                view?.loadUrl(url)
                Log.d(TAG, "shouldOverrideUrlLoading: 在WebView加载的url is ${webView.url}")
                return true
            }

存疑:

针对于百度Scheme这种自定义的协议,应用中可以采用原生跳转进行解决,那么浏览器开发中,如何处理自定义的协议?

Android WebView加载网页出错:net::ERR_CLEARTEXT_NOT_PERMITTED

原因分析:

image-20220822160805917.png

developer.android.com/training/ar…

为保证用户数据和设备的安全,从Android 9 (API级别28)开始,将默认使用加密连接,限制明文流量的网络请求,对未加密的流量不再信任;同理,如果应用中嵌套了webView,也只能使用https请求。

解决方法:

方法一:将App的网络请求方式修改为https请求
方法二:targetSdkVersion降到27以下
方法三:更改网络安全配置

res目录下新建xml文件夹,创建 network_security_config.xml 文件(文件名可任意取)

  • 选择一:允许所有网址使用非安全连接
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!--默认允许所有网址使用非安全连接-->
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>
  • 选择二:允许部分网址使用非安全连接
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
      <!--允许以下网址使用非安全的连接-->
        <domain includeSubdomains="true">my.example.com</domain>
        <domain includeSubdomains="true">you.example.com</domain>
    </domain-config>
</network-security-config>
  • 选择三:在允许所有网址使用非安全连接的情况下,限制部分网址使用非安全连接
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
      <!--不允许以下网址使用非安全连接-->
        <domain includeSubdomains="true">my.example.com</domain>
        <domain includeSubdomains="true">you.example.com</domain>
    </domain-config>
 <!--默认允许所有网址使用非安全连接-->
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

然后在AndroidMainfest.xml文件的application标签添加该配置

<manifest ...>
    <application 
        ...        
        android:networkSecurityConfig="@xml/network_security_config"
        ...>
    </application>
</manifest>
方法四:直接在AndroidMainfest.xml文件的application中添加开关
<manifest ...>
    <application
        ...
        android:usesCleartextTraffic="true"
        ...>
    </application>
</manifest>

Android WebView加载网页出错:"Uncaught TypeError:Cannot read property of null (reading 'getItem')",source: ...

image-20220822171731692.png

原因分析:

根据Logcat提示,ExpiredStorage:No storage base class provider and 'localStorage' is undefined! Please provide a valid base storage -> 程序没有提供存储基类,未定义 localStorage,期望能提供一个有效的基本存储

知识拓展:

Web storage

Web存储,有时称为DOM存储(文档对象模型存储),为Web应用程序提供了存储客户端数据的方法和协议。Web存储支持持久数据存储,类似于Cookie,但容量大大增强,并且在HTTP请求头中不发送任何信息。有两种主要的web存储类型:本地存储和会话存储,其行为分别类似于持久cookie和会话cookie。Web存储由万维网联盟(W3C)和WHATWG标准化,并得到所有主要浏览器的支持。

作为 Html5 标准的一部分,绝大多数的浏览器都支持localStorage,但是由于其安全特性(任何人都能读取,敏感数据易泄漏),Android默认关闭了该功能

解决方法:

//是否打开Dom本地存储
webView.settings.domStorageEnableb = true

Android WebView:求证shouldOverrideUrlLoading的真实作用

问题起因:

在代码实践shouldOverrideUrlLoading函数时,对该函数的作用及返回值充满了疑惑,因此,查阅了相关的一些资料,却发现关于该函数作用有着五花八门的解释,似乎好多还存在着一些错误

考察分析:

相关说法:(以下说法均存在争议)

说法1:返回true就调用系统浏览器,返回false由WebView处理。

说法2:返回true当前url即使是重定向url也不会再执行,返回false由系统执行url,直到不再执行此方法。

说法3:WebView上的所有加载都经过这个方法。

说法4:用法:在shouldOverrideUrlLoading中

override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
         view?.loadUrl(url)
         return true
}
就可以在webView中加载url
官方文档

image.png

通过官方文档我们可以获知

当即将在当前 WebView 中加载 URL 时,让宿主应用程序有机会进行控制。如果未提供 WebViewClient,默认情况下 WebView 将要求 Activity Manager 为 URL 选择正确的处理程序。如果提供了 WebViewClient,返回 true会导致当前 WebView 中止加载 URL,而返回 false会导致 WebView 像往常一样继续加载 URL。

注意:不要WebView#loadUrl(String)使用相同的 URL 调用然后返回true。这会不必要地取消当前加载并使用相同的 URL 开始新的加载。继续加载给定 URL 的正确方法是简单地返回false,而不调用WebView#loadUrl(String). -> (说法4错误

产出结论

根据官方文档我们可以简单地知道,如果没有设置WebViewClient的话,将由系统(Activity Manager)进行处理,通过浏览器打开或者弹出浏览器选择框,如果设置了WebViewClient,返回true的情况下,将由应用的代码处理url,WebView不处理,即程序员自己处理,相反,返回false的情况下,将由WebView处理url -> (说法1、2错误)

通过代码实践,发现并不是WebView中的所有加载都会经过该方法,通过查阅相关的源码解析知晓 -> WebView的前进、后退、刷新、以及post请求都不会调用shouldOverrideUrlLoading方法,除去以上行为,还得满足( ! isLoadUrl || isRedirect) 即 (不是通过webView.loadUrl来加载的 或者 是重定向) 这个条件,才会调用shouldOverrideUrlLoading方法。 -> (说法3错误)

代码实践

class MainActivity : AppCompatActivity() {
    private val TAG: String = "MainActivity"
    lateinit var webView: WebView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initView()
        handleWebViewClient()
        handleWebChromeClient()
    }

    /**
     * 辅助WebView处理JavaScript的对话框、图标、标题等
     */
    private fun handleWebChromeClient() {
        webView.webChromeClient = object :WebChromeClient(){

            /**
             * 获取网页的加载进度并打印
             */
            override fun onProgressChanged(view: WebView?, newProgress: Int) {
                if (newProgress < 100){
                    Log.d(TAG, "onProgressChanged: 当前加载进度是 $newProgress")
                }else{
                    Log.d(TAG, "onProgressChanged: 加载成功")
                }
            }

            /**
             * 获取网页的标题并打印
             */
            override fun onReceivedTitle(view: WebView?, title: String?) {
                Log.d(TAG, "onReceivedTitle: $title")
            }
        }
    }

    /**
     * WebViewClient类:处理各种通知&&请求事件
     */
    private fun handleWebViewClient() {

        webView.webViewClient = object : WebViewClient() {

            /**
             * 前提:设置了WebViewClient
             * ture: 拦截WebView加载url,由应用的代码处理,即程序员自己做处理
             * false: 允许WebView加载url,由WebView处理
             */
            override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {

                if (url == null) return false

                try {
                    //针对于自定义协议的url,转换成原生调用(intent)
                    if (!url.startsWith("http://") && !url.startsWith("https://")) {
                        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                        startActivity(intent)
                        return true
                    }
                } catch (e: Exception) { //防止crash (如果手机上没有安装处理对应scheme开头的url的APP, 会导致crash)
                    return true //没有安装该app时,返回true,表示拦截自定义链接,但不跳转,避免弹出ERR_UNKNOWN_URL_SCHEME
                }
                //处理http/https开头的url
                view?.loadUrl(url)
                Log.d(TAG, "shouldOverrideUrlLoading: 在WebView加载的url is ${webView.url}")
                return true
            }

            /**
             * 开始载入页面时调用,可用于设定loading的页面,告诉用户程序在等待网络响应
             */

            override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                Log.d(TAG, "onPageStarted: $url 等待网络响应")
            }

            /**
             * 在页面加载结束时调用,可用于关闭loading条,切换程序动作
             */
            override fun onPageFinished(view: WebView?, url: String?) {
                Log.d(TAG, "onPageFinished: $url 页面加载结束")
            }

            /**
             * 在加载页面资源时调用,每个资源加载均会调用(such as a picture)
             */
            override fun onLoadResource(view: WebView?, url: String?) {
//                super.onLoadResource(view, url)
            }

            /**
             * 加载页面的服务器出现错误时调用(such as 404)
             * 可以手写一个html用于服务器错误时加载
             */
            override fun onReceivedError(
                view: WebView?,
                request: WebResourceRequest?,
                error: WebResourceError?
            ) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    when (error?.errorCode) {    //根据传回的错误码,进行对应的错误处理
                        HttpURLConnection.HTTP_NOT_FOUND -> {
                            Log.d(TAG, "onReceivedError: 404 not found")
                        }
                    }
                }
            }

            /**
             * 当load有ssl层的https页面时,如果该网站的安全证书在Android无法得到认证,WebView就会变成一个空白页
             */
            override fun onReceivedSslError(
                view: WebView?,
                handler: SslErrorHandler?,
                error: SslError?
            ) {
                //默认处理方式,webView变成空白页
                //handler.cancel()
                handler?.proceed()  //接受证书
            }
        }
    }

    private fun initView() {
        webView = findViewById(R.id.show_webView)
        //是否与JavaScript交互
        webView.settings.javaScriptEnabled = true
        //是否打开Dom本地存储
        webView.settings.domStorageEnabled = true
        webView.loadUrl("https://www.baidu.com")
    }

    /**
     * 使Back键控制网页后退
     */
    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) {
            webView.goBack()
            return true
        }
        return super.onKeyDown(keyCode, event)
    }
}