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")
}
}
}
}
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
原因分析:
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: ...
原因分析:
根据Logcat提示,ExpiredStorage:No storage base class provider and 'localStorage' is undefined! Please provide a valid base storage -> 程序没有提供存储基类,未定义 localStorage,期望能提供一个有效的基本存储
知识拓展:
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
官方文档
通过官方文档我们可以获知
当即将在当前 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)
}
}