1.前言
WebView是Android中的原生UI控件,主要用于在app应用中方便地访问远程网页或本地html资源。同时,WebView也在Android中充当Java代码和JS代码之间交互的桥梁。实际上,也可以将WebView看做一个功能最小化的浏览器。本文将全面讲解WebView各方面的知识点。
2.基本使用
创建一个WebView组件
通常情况下,我们会在XML文件中定义需要使用的UI控件,这也是官方提倡的使用方式。WebView当然也可以直接在XML中定义,但这种方式存在潜在的问题。如果在XML中定义WebView,那么系统将把当前的Activity作为Context去实例化WebView对象。由于WebView保持着对Activity的引用,如果在Activity结束时WebView还未进入销毁状态,将导致Activity无法被系统回收,进而造成内存泄漏。
因此,并不建议直接在XML文件中定义WebView,而是在需要使用WebView的时候手动创建,并将其加入合适的布局中。下面将给出一个简单的例子:
XML文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!--WebView的容器-->
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
我们在主布局中定义了一个FrameLayout,这将作为WebView的容器。接下来,让我们来看一下应该如何通过Java代码创建WebView并添加到这个容器中。
Java代码:
//通过代码创建
FrameLayout parentLayout=findViewById(R.id.container);
webView=new WebView(getApplicationContext());//使用应用级别的context,避免对Activity的引用
FrameLayout.LayoutParams layoutParams=new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
parentLayout.addView(webView,layoutParams);
首先,通过findViewById方法获取到之前定义在XML中的FrameLayout。然后,将ApplicationContext作为参数创建WebView对象。同时,创建FrameLayout.LayoutParams对象,这将是WebView的布局参数。最后,调用FrameLayout的addView方法,就将WebView添加到了目标位置。
需要注意:使用ApplicationContext创建WebView,WebView就不会再持有当前Activity的引用,那么前文提到的内存泄漏问题也就随之解决了。但是这种方式也是有代价的,目前已知的问题有:使用第三方应用打开网页链接异常、弹出Dialog异常、在WebView中使用Flash异常。因此,建议自行测试并根据实际情况选择是否需要采取这种方式。如果想避免这些问题,也可以直接使用当前Activity作为Context创建WebView,然后将这个Activity运行在独立进程中。具体操作请参考下文[5.常见问题#WeView出现OOM影响主进程]以及[5.常见问题#WebView后台耗电问题]部分。
加载网页或资源
WebView可以加载多种资源,包括本地资源和远程资源,同时也有多种用于加载资源的方法。
1.loadUrl(以url形式加载目标资源)
加载assets中的资源:
![](/qrcode.jpg)
webView.loadUrl("file:///android_asset/test.html");//加载本地assets文件夹下的资源
webView.loadUrl("file:///android_asset/web/test.html");//加载本地assets的web子文件夹下的资源
加载res中的资源:
webView.loadUrl("file:///android_res/mipmap/ic_launcher.png");//加载本地res文件夹下的图片
webView.loadUrl("file:///android_res/raw/ic_launcher.png");//加载本地res文件夹下raw文件夹下的图片
webView.loadUrl("file:///android_res/raw/test.html");//加载本地res文件夹下raw文件夹下的html文件
经过实际测试,WebView只能加载res的drawble和mipmap文件夹中的图片资源,以及res的raw文件夹中的资源(图片或html文件皆可)。需要注意,url中的mipmap代指所有的mipmap文件夹,同理drawable代指所有drawable文件夹,不需要给出文件夹的具体限定符,系统会自行寻找合适的资源。此外,WebView**并不支持**加载raw的子文件夹下的资源。
加载sdcard中的资源:
webView.loadUrl("file:/sdcard/test.html");//加载本地sdcard下的资源
webView.loadUrl("file:///sdcard/test.html");//加载本地sdcard下的资源
webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");//加载本地sdcard下的资源
加载远程资源:
webView.loadUrl("http://blog.csdn.net/codingending/article/details/78609902");//加载远程网页
需要注意,使用WebView加载远程网页前,需要在AndroidManifest文件中为应用申请网络使用权限:
<uses-permission android:name="android.permission.INTERNET"/>
添加请求头:
loadUrl有一个重载方法,可以添加多个请求头:
//additionalHttpHeaders以键值对的形式存储请求头
public void loadUrl(String url, Map<String, String> additionalHttpHeaders)
2.loadData(以字符串形式加载html片段)
//data:html片段
//mimeType:数据类型,如"text/html"
//encoding:数据编码,有两种可选值("base64"和其他任何值),分别代表base64编码和URL编码
public void loadData(String data, String mimeType, String encoding)
为mimeType传入null和传入”text/html”意义相同。为encoding传入除”base64”外的任何值,都相当于是指定数据编码为URL编码,一般我们传入null即可。
需要注意,如果data中包含中文,结果将显示为乱码,解决方案请参考下文[5.常见问题#loadData加载中文乱码]部分。
此外,data中的’#’,’%’,’\’,’?’应该分别被替换为%23,%25,%27,%3f
3.loadUrlWithBaseURL(以字符串形式加载html片段)
//baseUrl:基础url,传入null相当于传入了"about:blank"
//data:html片段
//mimeType:数据类型,如"text/html"
//encoding:数据编码,有两种可选值("base64"和其他任何值),分别代表base64编码和URL编码
//historyUrl:历史url
public void loadDataWithBaseURL(String baseUrl, String data,String mimeType, String encoding, String historyUrl)
loadUrlWithBaseURL方法和loadData类似,主要是用于解决Javascript的同源限制问题。实际上,如果为baseUrl和history传入null,那么loadUrlWithBaseURL和loadData方法作用相同,并且加载中文数据不会出现乱码的问题。
4.postUrl(以post请求的形式访问url)
//postData:本次post请求携带的数据,必须是application/x-www-form-urlencoded编码
public void postUrl(String url, byte[] postData)
如果传入的url不是一个远程网页地址,那么最终将通过loadUrl方法加载这个url,同时postData参数会被忽略。
前进和后退功能
WebView作为一个功能最小化的浏览器,内部维持着url跳转的历史记录,因此可以轻松地实现前进/后退功能。
public boolean canGoBack() //判断是否可以后退
public void goBack() //后退
public boolean canGoForward() //判断是否可以前进
public void goForward() //前进
一般情况下,我们需要在点击手机的后退按钮时,让WebView执行后退操作。这一需求可以通过重写Activity的onKeyDown或onBackPressed方法实现。下面以重写onKeyDown方法为例:
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if(keyCode==KeyEvent.KEYCODE_BACK&&webView.canGoBack()){
webView.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
理论上来说,通过上面提供的几个方法就可以方便地实现后退/前进功能了。但是现实往往是残酷的,url跳转过程中的重定向动作会对后退功能带来棘手的麻烦。
举个简单的例子:访问页面A->页面A重定向至页面B->调用goBack方法后退->回到页面A->页面A重定向至页面B->….
在这种情况下,由于页面A每次加载完成后的重定向动作,WebView将永远无法通过后退功能退出页面B。只有在连续点击两次后退按钮时,才能在页面A进行重定向跳转前直接退出页面A,进而跳出这个循环。在实际开发中,必须避免用户陷入这样的困境中,因为不是每个用户都知道通过按两次后退按钮跳出重定向循环。我们可以学习微信的处理方式,在Activity的左上角显示一个关闭按钮,允许用户在任何情况下都能退出页面。
刷新页面和停止加载
public void reload() //刷新页面(当前页面的所有资源都会重新加载)
public void stopLoading() //停止加载
清除数据
public void clearCache(boolean includeDiskFiles) //清除缓存
public void clearFormData() //清除表单数据
public void clearHistory() //清除历史记录
如果为clearCache传入false,仅有内存缓存会被清除;如果传入true,磁盘中的缓存也会被一块清除。需要注意,这个方法针对的是当前应用程序中的所有WebView。
clearFormData仅仅清除表单的自动填充数据,并不会影响已经存储到本地的表单数据。
WebView状态
public void onPause() //尽最大努力暂停WebView当前可被安全暂停的行为(如动画、定位),但并不暂停Javascript
public void onResume() //恢复WebView被onPause暂停的行为
public void pauseTimers() //暂停WebView的所有行为
public void resumeTimers() //恢复WebView被pauseTimers暂停的行为
需要注意,pauseTimers是一个全局方法,针对的是所有App的WebView。恰当地使用这一方法可以降低CPU功耗。
销毁WebView
public void destroy() //销毁WebView
需要注意,只有先把WebView从布局中移除后,才能够调用这个方法安全地销毁WebView。正确地销毁WebView,才能避免应用程序出现内存泄漏以及其它特殊问题。具体的解决方案请参考下文的[5.常见问题#内存泄漏]部分。
3.进阶使用
WebViewClient
默认情况下,WebView会调用系统已安装的其他浏览器加载传入的网址或者资源。也就是说将跳出当前应用去处理网络请求,显然这并不是我们想要的结果。如果我们希望直接使用自己的WebView加载网址或资源,就必须为WebView设置WebViewClient。WebViewClient有多个回调方法,我们可以通过重写这些方法来实现想要的功能。如:加载状态、跳转请求、资源请求、错误信息等。
public void setWebViewClient(WebViewClient client)
只需要调用WebView的setWebViewClient方法,即可简单地为当前WebView设置WebViewClient对象。下面,我们来看一下WebViewClient常用的回调方法。
拦截url跳转请求
//url:WebView即将加载的url
public boolean shouldOverrideUrlLoading(WebView view, String url)
//request:封装本次请求的详细信息(包括url、请求方法、请求头)
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
以上两个方法都会在WebView加载新的url时触发。不同之处在于,第二个方法是在Android 5.0(API 21)之后才加入的。换句话说,Android 5.0以下系统会回调第一个方法,反之回调第二个方法。因此,为了兼容不同的系统版本,我们需要同时重写这两个方法。
可以看到,这两个方法都有一个boolean返回值。但是,无论返回true还是false,只要为WebView设置了WebViewClient,系统就不会再将url交给第三方的浏览器去处理了。
这两种返回值的真正区别是这样的:shouldOverrideUrlLoading返回false,代表将url交给当前WebView加载,也就是正常的加载状态;shouldOverrideUrlLoading返回true,代表开发者已经对url进行了处理,WebView就不会再对这个url进行加载了。使用true这个返回值的最大作用是屏蔽某些网址,可以借此实现黑名单机制。实际上,无论返回true还是false,我们都可以实现正常的加载功能,代码如下:
//方法1
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
return false;
}
//方法2
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);//手动加载
return true;
}
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
view.loadUrl(request.getUrl().toString());//手动加载
return true;
}
需要注意,谷歌官方并不是很提倡第二种处理方法。因为shouldOverrideUrlLoading(WebView view, WebResourceRequest request)方法也会在url并非http协议时回调,而在这里我们并没有对url进行判断,直接使用loadUrl方法加载非http协议的url将会失败。因此,如果没有特殊的需求,建议直接返回false即可。否则,就注意要在调用loadUrl方法前先对url的协议进行判断。
最后要知道,如果使用post方式加载url,shouldOverrideUrlLoading方法是不会被触发的。
拦截资源请求
//url:本次请求的url
public WebResourceResponse shouldInterceptRequest(WebView view,String url)
//request:封装本次请求的详细信息(包括url、请求方法、请求头)
public WebResourceResponse shouldInterceptRequest(WebView view,WebResourceRequest request)
以上两个方法都会在WebView发生资源请求的时候回调(使用loadUrl加载远程网页时也会触发这个方法),因此我们可以按照业务需求对资源进行替换。和shouldOverrideUrlLoading一样,第二个方法是在Android 5.0(API 21)之后才加入的。Android 5.0以下系统会回调第一个方法,反之回调第二个方法。因此,为了兼容不同的系统版本,我们需要同时重写这两个方法。
可以看到,shouldInterceptRequest的返回值是一个WebResourceResponse对象,在这个对象中封装了本次url响应的数据。因此,我们只要构造出一个包含目标数据的WebResourceResponse对象,并将其返回,就实现了资源的替换。在需要使用本地资源替换远程资源的场景中,这个回调方法非常有用。当然,如果直接返回null,WebView将会正常地加载url对应的资源。下面提供一个简单的资源替换例子:
//判断是否需要替换资源
private WebResourceResponse shouldReplaceResource(String url){
String targetHtml="<html><body><a href='http://blog.csdn.net/codingending'>先看看我的博客</a></body></html>";
if("http://blog.csdn.net/".equals(url)){//拦截目标url
InputStream targetContent=new ByteArrayInputStream(targetHtml.getBytes());//构造InputStream
return new WebResourceResponse("text/html","utf-8",targetContent);//返回重新构造的资源
}
return null;
}
//WebViewClient
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
return shouldReplaceResource(url);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return shouldReplaceResource(request.getUrl().toString());
}
当使用loadUrl加载http://blog.csdn.net/
时,WebView就会进行资源替换。
需要注意,shouldInterceptRequest会在非UI线程中回调。因此,如果需要在这个方法中进行View操作,需要手动切换线程。
监听页面加载状态
//favicon:当前网页的图标
public void onPageStarted(WebView view, String url, Bitmap favicon)
public void onPageFinished(WebView view, String url)
onPageStarted将在网页开始加载时回调,onPageFinished则会在页面加载结束时回调。需要注意,即使onPageFinished方法已经回调,也并不代表当前页面中的所有资源都已经加载完毕,可能当前网页还在加载图片等比较耗时的资源。
监听资源加载
//url:需要加载的资源地址
public void onLoadResource(WebView view, String url)
onLoadResource将在WebView加载资源(如css、js、图片等)时回调。
监听错误回调
//errorCode:错误码(定义在WebViewClient中,形式都是ERROR_*)
//description:对当前错误的描述
//failingUrl:发生错误的url
public void onReceivedError(WebView view, int errorCode,String description, String failingUrl)
//request:封装本次请求的详细信息(包括url、请求方法、请求头)
//error:封装本次错误的详细信息(包括错误码和错误描述)
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
第一个方法会在当前页面内容加载发生错误时回调(不包括加载图片、css等出错的场景),第二个方法则会在当前页面的任何资源加载出错时回调(包括加载图片、css等出错的场景)。此外,在Android 6.0以下,系统会回调第一个方法,否则回调第二个方法。
关键回调方法的执行顺序
1.使用loadUrl方法加载页面A(无重定向)
onPageStarted->onPageFinished
2.使用loadUrl方法加载页面A(页面A重定向至页面B)
onPageStarted->shouldOverrideUrlLoading->onPageStarted->onPageFinished->onPageFinished
3.在已加载的页面中点击链接,加载页面A(无重定向)
shouldOverrideUrlLoading->onPageStarted->onPageFinished
4.在已加载的页面中点击链接,加载页面A(页面A重定向至页面B)
shouldOverrideUrlLoading->onPageStarted->shouldOverrideUrlLoading->onPageStarted->onPageFinished->onPageFinished
5.执行goBack/goForward/reload方法
onPageStarted->onPageFinished
6.发生资源加载
shouldInterceptRequest->onLoadResource
WebChromeClient
除了为WebView设置WebViewClient,我们还可以调用WebView的setWebChromeClient方法设置WebChromeClient。WebChromeClient主要用于辅助WebView处理页面加载进度、Javascript的对话框,获取网站图标、网站标题等。
public void setWebChromeClient(WebChromeClient client)
WebChromeClient同样有多个回调方法,只需要重写合适的方法就能轻松地实现多种需求。
监听网页加载进度
//newProgress:网页当前的加载进度(数值为0-100)
public void onProgressChanged(WebView view, int newProgress)
onProgressChanged会在网页加载过程中多次触发。当newProgress的值为100时,可以认为当前网页已经加载完毕。因此,通过这个方法判断页面是否加载完成比使用上文提到的onPageFinished方法更准确。同时,由于这个方法在回调中会不断获得最新的加载进度,因此我们可以借助这个方法实现自定义的加载进度条。
这里给出一个简单的思路:在WebView的上方添加一个ProgressBar控件,并默认隐藏。在onPageStarted方法中显示ProgressBar,并在onProgressChanged方法回调时更新ProgressBar的进度值。当onProgressChanged方法中的newProgress达到100时,就隐藏这个ProgressBar。需要注意,为了在页面加载出错时也能正确隐藏进度条,也应该在onReceivedError方法中隐藏ProgressBar。
获取网页标题
//title:当前网页的标题
public void onReceivedTitle(WebView view, String title)
获取网页图标
//icon:当前网页的图标
public void onReceivedIcon(WebView view, Bitmap icon)
网页弹窗
//message:alert弹出窗口中的提示信息(提示或警告信息对话框,仅一个确认按钮)
//result:向网页中的Javascript代码反馈本次操作结果(result.confirm代表点击了确定按钮,result.cancel代表点击了取消按钮)
public boolean onJsAlert(WebView view, String url, String message,JsResult result)
///message:confirm弹出窗口中的提示信息(确认对话框,有确认、取消两个按钮)
//result:向网页中的Javascript代码反馈本次操作结果(result.confirm代表点击了确定按钮,result.cancel代表点击了取消按钮)
public boolean onJsConfirm(WebView view, String url, String message,JsResult result)
//message:prompt弹出窗口中的提示信息(输入信息对话框,有一个输入框,还有确认、取消两个按钮)
//defaultValue:输入框中的默认信息
//result:向网页中的Javascript代码反馈本次操作结果(result.confirm代表点击了确定按钮,result.cancel代表点击了取消按钮)
public boolean onJsPrompt(WebView view, String url, String message,String defaultValue, JsPromptResult result)
当网页中的Javascript代码弹出alert、confirm、prompt三种类型的弹窗时,会分别触发以上三种方法。在这些回调方法中,我们可以实现符合应用风格的对话框(通过AlertDialog实现),这可以给用户更棒的视觉体验。需要注意,这三个方法都有一个boolean返回值。如果返回true,代表Android应用已经对弹窗进行了处理,Javascript代码不必再弹出窗口了;反之,代表本次弹窗请求未被处理,网页将按照默认效果弹出窗口。
下面给出一个简单的处理方案,可以作为参考:
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("JsAlert")
.setMessage(message)
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
})
.setCancelable(false)
.show();
return true;
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("JsConfirm")
.setMessage(message)
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.cancel();
}
})
.setCancelable(false)
.show();
return true;
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
final EditText editText=new EditText(MainActivity.this);
editText.setText("默认数据");//设置默认数据
new AlertDialog.Builder(MainActivity.this)
.setTitle("JsPromt")
.setView(editText)//为弹出窗口设置输入框
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm(editText.getText().toString());//向Javascript传递输入值
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.cancel();
}
})
.setCancelable(false)
.show();
return true;
}
WebSettings
除了上文提到的WebViewClient和WebChromeClient外,WebSettings也是一个非常重要的类。通过WebSettings,我们可以对WebView进行多种配置和管理。WebSettings的获取方式如下:
WebSettings webSettings=webView.getSettings();
支持Javascript
webSettings.setJavaScriptEnabled(true);
默认情况下,WebView是不支持Javascript的,需要调用setJavaScriptEnabled方法并传入true才能启用Javascript功能。当然,如果想禁用Javascript,只要传入false作为参数就行了。
需要注意,通过这种方式支持Javascript在Android 4.2(API 17)以下存在严重的安全隐患。如果需要支持Android 4.2以下的系统,请参考下文的[4.与JS的交互方式]部分。
设置页面自适应屏幕
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
进行上述配置后,如果网页针对移动设备进行了自适应处理,那么页面将缩放到最合适的大小,通常这将带来更好的视觉效果。
支持缩放功能
webSettings.setSupportZoom(true);//启用缩放功能
webSettings.setBuiltInZoomControls(true);//使用WebView内置的缩放功能
webSettings.setDisplayZoomControls(false);//隐藏屏幕中的虚拟缩放按钮
进行上述配置后,就可以通过多指的捏合操作对网页执行缩放了。另外提一下,之所以调用setDisplayZoomControls(false)
隐藏虚拟缩放按钮,主要是为了美观。因为可以使用多指触摸执行缩放操作,就没必要单独提供按钮了(外观比较丑)。当然,如果有必要显示按钮,传入true即可,并不影响最终的缩放功能。
但是要注意,setDisplayZoomControls(true)在某些系统版本中可能会导致应用出现意外崩溃,具体细节请参考下文[5.常见问题#setDisplayZoomControls(true)引起的崩溃问题]部分。
缓存模式
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//设置缓存模式为LOAD_DEFAULT
WebView是原生支持缓存的,我们只需要设置合适的缓存模式即可。setCacheMode方法需要一个int类型的参数,系统提供了四种可选值,区别如下:
- LOAD_DEFAULT:默认值。当存在缓存且未过期时加载缓存数据,否则通过网络加载数据。
- LOAD_NO_CACHE:不使用缓存。仅通过网络加载数据。
- LOAD_CACHE_ONLY:只使用缓存。仅加载缓存数据,不通过网络加载数据。
- LOAD_CACHE_ELSE_NETWORK:只要存在缓存,不管是够过期都加载缓存数据,否则通过网络极加载数据。
定位设置
webSettings.setGeolocationEnabled(true);//允许网页执行定位操作
如果要禁用网页的定位功能,传入false作为参数即可。需要注意,这个方法只是允许网页执行定位操作,但是最终定位操作的实现还是会委托给Android应用处理。因此,为了保证定位功能正常执行,需要满足以下两点:
- Android应用需要获取定位权限。需要在AndroidManifest文件中声明android.Manifest.permission.ACCESS_COARSE_LOCATION和android.Manifest.permission.ACCESS_FINE_LOCATION两个权限。
- 需要为WebView设置WebChromeClient,并重写WebChromeClient的onGeolocationPermissionsShowPrompt方法。这个方法会在网页中的Javascript代码执行定位操作时触发。需要注意,Android6.0及以上引入了运行时权限的概念。定位属于危险权限,需要在使用时手动获取。因此我们可以在这个回调方法中弹出一个请求定位的提示对话框(AlertDialog),在用户选择确定后获得相应权限。
如果不了解运行时权限的概念,可以参考这篇博客:
其他设置
//允许自动加载图片(默认值为true)
webSettings.setLoadsImagesAutomatically(true);
//设置默认文本编码(默认值为UTF-8)
webSettings.setDefaultTextEncodingName("UTF-8");
4.与JS的交互方式
如何安全实现Android代码与Javascript代码的交互一直是学习WebView的重点和难点。在这个部分,我们将学会如何使用合理的方式规避低版本下的JS安全漏洞问题。
Android调用JS代码
loadUrl
使用loadUrl方法就可以简单地异步执行JS代码,下面给出一个简单的例子:
webView.loadUrl("javascript:alert('从Android调用Js的alert方法')");
这种方式使用起来很简单,只要按照正确的格式传入一个字符串即可。格式:(javascript:JS方法名)。需要注意,被调用的方法需要已在当前页面定义或者已经引入当前页面(JS系统方法也行),并且使用这种方式将会刷新当前页面。缺点在于,使用这种方式无法获取JS方法的返回值。
需要注意,只有在当前网页的onPageFinished方法执行后,Javascript代码才算是加载完毕,这时使用loadUrl调用JS代码才会生效。
evaluateJavascript
在Android 4.4(API 19)中,新增了一个方法evaluateJavascript。通过这个方法就可以高效率地异步执行JS代码。方法原型如下:
//script:需要执行的JS代码(格式和loadUrl一致)
//resultCallback:用于提供JS方法的返回值
public void evaluateJavascript(String script, ValueCallback<String> resultCallback)
这个方法的效率比loadUrl更高,并且不会刷新当前页面。需要注意,这个方法只能在UI线程中调用,最终resultCallback的回调方法也会在UI线程中执行。
混合使用
为了在兼容低版本的情况下达到最高的执行效率,我们往往需要混合使用loadUrl和evaluateJavascript方法。下面给出一个简单的参考示例:
...
String jsStr="javascript:alert('从Android调用Js的alert方法')";
//根据当前系统版本选择最合适的加载方式
if(Build.VERSION.SDK_INT<19){
webView.loadUrl(jsStr);
}else{
webView.evaluateJavascript(jsStr, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
if(TextUtils.isEmpty(value)){
Toast.makeText(MainActivity.this,"返回值为空",Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(MainActivity.this,"返回值"+value,Toast.LENGTH_SHORT).show();
}
}
});
}
...
JS调用Android代码
利用WebView的addJavascriptInterface方法
WebView提供了addJavascriptInterface方法实现Javascript调用Android代码。该方法如下:
//object:定义在Android中的对象,包含Javascript需要调用的方法
//name:object在Javascript中的映射名称
public void addJavascriptInterface(Object object, String name)
首先,我们需要定义一个充当映射的类,下面给出一个简单的例子:
//Android调用JS需要使用的映射对象
class JsBridge{
@JavascriptInterface
public String testJsToAndroid(String msg){
return "[从Android返回]"+msg;
}
}
需要注意,被JS调用的方法需要使用@JavascriptInterface注解。
然后,我们需要调用addJavascriptInterface方法建立JsBridge的映射关系,代码如下:
webView.addJavascriptInterface(new JsBridge(),"jsBridge");
在这个方法中,我们实例化了一个JsBridge对象,并将”jsBridge”作为这个对象的映射名称。在Javascript代码中,可以直接使用这个映射名称调用JsBridge中的方法。
//Javascript代码
function js_normal() {
var msg=jsBridge.testJsToAndroid("使用原生方式实现Js调用Android方法");
alert("输出:"+msg);
}
可以看到,通过这种方式,我们还可以方便地获取到Android方法的返回值。然而,这种方式在Android 4.2(API 17)以下存在严重的安全隐患,这也是接下来要介绍的两种方式能够存在的原因。
利用WebViewClient的shouldOverrideUrlLoading方法拦截url
上文说过,WebViewClient的shouldOverrideUrlLoading方法会在发生url跳转时触发。由此,我们只要规定好调用Android方法的url格式,并在需要执行方法时加载相应的url即可。下面提供一个简单的示例:
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri uri= Uri.parse(url);
if("js".equals(uri.getScheme())){//先判断scheme
if("jsBridge".equals(uri.getAuthority())){//随后判断authority
String msg=uri.getQueryParameter("msg");//获取来自js的参数
msg="[从Android返回]"+msg;
//注意要在目标字符串的左右添加单引号,这才代表参数是作为字符串传入JS中的方法
webView.loadUrl("javascript:get_value_from_app('"+msg+"')");//将新的值返回js
return true;//返回true代表本次url请求处理完毕
}
}
return false;
}
//Javascript代码
function js_url() {
document.location="js://jsBridge?msg=使用拦截Url的方式实现Js调用Android方法,这样可以避免Android4.2以下的漏洞";
}
//接收来自Android的返回值
function get_value_from_app(value){
alert("输出:"+value);
}
在这个例子中,我们规定调用Android本地方法的url格式如下:(js://jsBridge?msg=xxx)。因此,我们在shouldOverrideUrlLoading方法中先将String类型的url解析为Uri对象,并判断它的scheme是否为js,authority是否为jsBridge,然后使用Uri的getQueryParameter方法获取到了msg参数。当然,在这个例子中并没有执行任何Android本地方法,实际开发时应该根据业务需求调用相应的本地方法。
需要注意,使用这种方式并不能直接向Javascript代码传递Android本地方法的返回值。如果确实有传递返回值的需求,就应该像这个例子一样,在Javascript中定义一个传值的方法(在本例中是get_value_from_app
)。当Android方法执行完毕后,调用JS中相应的传值方法即可。但是一个容易出错的地方在于,如果传递的返回值是字符串,那么就要在字符串的左右添加单引号,这才代表参数是作为字符串传入JS方法。
利用WebChromeClient的onJsPrompt方法拦截message
和第二种方式类似,除了拦截url,还可以通过WebChromeClient的onJsPrompt方法拦截message实现同样的功能。我们只要规定好调用Android方法的message格式,并在需要执行Android方法时调用JS的prompt方法并传入相应的message即可。下面提供一个简单的示例:
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
Uri uri=Uri.parse(message);
//处理js和android交互
if("js".equals(uri.getScheme())&&"jsBridge".equals(uri.getAuthority())){
String msg="[来自Android返回]"+uri.getQueryParameter("msg");
result.confirm(msg);
return true;
}
...
}
//Javascript代码
function js_prompt() {
var msg = prompt("js://jsBridge?msg=通过在WebChromeClient中的onJsPrompt中拦截url实现Js调用Android方法,这样可以避免Android4.2以下的漏洞");
alert(msg);
}
在这个例子中,我们规定调用Android本地方法的message格式如下:(js://jsBridge?msg=xxx)。因此,我们在shouldOverrideUrlLoading方法中先将message解析为Uri对象,并判断它的scheme是否为js,authority是否为jsBridge,然后使用Uri的getQueryParameter方法获取到了msg参数。实际开发时只要根据业务需求调用相应的本地方法即可。
这种方式和第二种方式相比的显著不同之处在于,可以通过JsPromptResult的confirm
方法直接向Javascript代码传递Android方法的返回值,这无疑可以为开发带来极大的便利。
可能存在的安全隐患
我们在前文介绍了三种Javascript调用Android方法的方式。可以明显看出,第一种方式是最简单的,因为这是Android官方提供的解决方案。问题在于,Android 4.2(API 17)以下使用这种方式存在着任意代码执行漏洞。简单来说,在Android 4.2以前,只要为JS提供了映射对象,JS就可以调用这个对象的所有方法。通过调用这个对象的getClass方法,可以获得一个Class类型的对象。然后,调用Class对象的forName方法可以加载Runtime类,之后就可以借助Runtime执行本地命令,肆意获取设备中的敏感信息。
在Android 4.2时,Android官方规定映射类必须为其中提供给JS的方法添加@JavascriptInterface注解,JS也只能调用映射对象中有@JavascriptInterface注解的方法。由于getClass是Object类中的方法,不存在该注解,因此JS无法调用这个方法,前面提到的安全漏洞也就不存在了。
当然,现在我们还不能彻底放弃Android 4.2以下的系统。因此为了保证兼容性,推荐在Android 4.2以下使用第三种方式(传递返回值更方便),在Android 4.2及以上使用第一种方式。
此外,由于Android系统为WebView注入了searchBoxJavaBridge_、accessibility、accessibilityTraversal
三个映射对象,在Android 4.2以下攻击者同样可以利用它们执行任意代码。因此,我们需要手动移除这三个对象。
webView.removeJavascriptInterface("searchBoxJavaBridge_")
webView.removeJavascriptInterface("accessibility");
webView.removeJavascriptInterface("accessibilityTraversal")
5.常见问题
loadData加载中文数据出现乱码
问题描述:使用loadData方法加载含有中文的数据时,中文显示为乱码。
解决方案:
- 使用loadDataWithBaseURL方法代替loadData加载数据,不会出现乱码问题。
- 为loadData的mimeType参数传入“text/html;charset=UTF-8”,也可以解决乱码问题。
密码明文存储问题
问题描述:在Android 4.3(API 18)以前,用户在WebView加载的网页中输入密码后,系统会弹出对话框询问用户是否需要保存密码。如果用户选择保存,那么密码将会以明文的形式保存在本地,显然这是一个巨大的安全隐患。
解决方案:
WebView是否保存密码是由WebSettings的setSavePassword方法决定的。因此,我们只要调用这个方法并传入false,就可以避免明文储存的安全问题了。
webSettings.setSavePassword(false);
在Android 4.3及以上的版本,setSavePassword方法已经被弃用,WebView也不会默认保存密码,因此不再需要进行修复。
WeView出现OOM影响主进程
问题描述:由于WebView默认运行在应用进程中,如果WebView加载的数据过大(例如加载大图片),就可能导致OOM问题,从而影响应用主进程。
解决方案:为了避免WebView影响主进程,可以尝试将WebView所在的Activity运行在独立进程中。这样即使WebView出现了OOM问题,应用主进程也不会受到影响。具体做法也很简单,只要在AndroidManifest文件中为相应的Activity设置process属性即可。
<activity android:name=".MainActivity"
android:process=":remote">
</activity>
在这个例子中我们为Activity设置了process属性,意思就是让这个Activity运行在名为:remote的私有进程中。
需要注意,这种方式可能会有进程通信方面的问题,因此需要根据实际情况决定是否需要使用。
WebView后台耗电问题
问题描述:在某些情况下,即使Activity已经退出,WebView依旧占据着内存空间,这会导致设备耗电量增加。
解决方案:在上文提到过将WebView运行在独立进程中,然后只要在Activity的onDestroy方法中调用System.exit(0)退出虚拟机,就可以避免WebView继续占据内存空间。
@Override
protected void onDestroy() {
super.onDestroy();
System.exit(0);
}
内存泄漏
问题描述:如果在XML中定义WebView,那么系统将把当前的Activity作为Context去实例化WebView对象。由于WebView保持着对Activity的引用,如果在Activity结束时WebView还未进入销毁状态,将导致Activity无法被系统回收,进而造成内存泄漏。
解决方案:
首先,避免在XML中直接定义WebView,而是在需要的时候动态创建WebView并加入合适的ViewGroup中。
//通过代码创建
FrameLayout parentLayout=findViewById(R.id.container);
webView=new WebView(getApplicationContext());//使用应用级别的context,避免对Activity的引用
FrameLayout.LayoutParams layoutParams=new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
parentLayout.addView(webView,layoutParams);
其次,在Activity的onDestroy方法中,先让WebView停止加载,然后加载空数据,再把它从ViewGroup中移除。最后,调用destroy方法销毁WebView。
@Override
protected void onDestroy() {
if(webView!=null){//安全的销毁操作
webView.stopLoading();//停止加载
webView.loadDataWithBaseURL(null,"","text/html",null,null);//加载空数据
webView.clearHistory();//清除浏览历史
((ViewGroup)webView.getParent()).removeView(webView);//移除WebView
webView.destroy();//销毁WebView
webView=null;
}
super.onDestroy();
}
域控制不严格漏洞
问题描述:由于应用中的WebView没有对file:///
类型的url做限制,可能导致外部攻击者利用Javascript代码读取本地隐私数据。
解决方案:
1.如果WebView不需要使用file协议,直接禁用所有与file协议相关的功能即可。
webSettings.setAllowFileAccess(false);
webSettings.setAllowFileAccessFromFileURLs(false);
webSettings.setAllowUniversalAccessFromFileURLs(false);
需要注意,即使禁用了File协议,也不影响对assets和resources资源的加载。它们的url格式分别为:file:///android_asset、file:///android_res
。
2.如果WebView需要使用file协议,则应该禁用file协议的Javascript功能。具体方法为:在调用loadUrl方法前,以及在shouldOverrideUrlLoading方法中判断url的scheme是否为file。如果是file协议,就禁用Javascript,否则启用Javascript。
//WebSettings
webSettings.setAllowFileAccess(true);
webSettings.setAllowFileAccessFromFileURLs(false);
webSettings.setAllowUniversalAccessFromFileURLs(false);
//调用loadUrl前
...
if("file".equals(Uri.parse(url).getScheme())){//判断是否为file协议
webView.getSettings().setJavaScriptEnabled(false);
}else{
webView.getSettings().setJavaScriptEnabled(true);
}
webView.loadUrl(url);
//WebViewClient中做的操作
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
if("file".equals(request.getUrl().getScheme())){//判断是否为file协议
view.getSettings().setJavaScriptEnabled(false);
}else{
view.getSettings().setJavaScriptEnabled(true);
}
return false;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if("file".equals(Uri.parse(url).getScheme())){//判断是否为file协议
view.getSettings().setJavaScriptEnabled(false);
}else{
view.getSettings().setJavaScriptEnabled(true);
}
return false;
}
视频或音频在退出Activity后继续播放的问题
问题描述:在WebView加载的网页中播放音乐或视频,然而当前应用进入后台后音乐或视频还在继续播放。
解决方案:在Activity的onPause方法中暂停WebView,然后在onResume方法中恢复WebView。
@Override
protected void onPause() {
if(webView!=null){//暂停WebView
webView.onPause();
webView.pauseTimers();
}
super.onPause();
}
@Override
protected void onResume() {
if(webView!=null){//恢复WebView
webView.onResume();
webView.resumeTimers();
}
super.onResume();
}
开启硬件加速导致的闪烁问题
问题描述:在应用开启硬件加速后,WebView可能在加载过程出现闪烁现象。
解决方案:为WebView关闭硬件加速功能。
webView.setLayerType(View.LAYER_TYPE_SOFTWARE,null);
下载文件时的路径穿越问题
问题描述:下载文件时,如果文件名中包含“../”这样的字符,并且WebView并未对文件名进行过滤,就会出现文件路径穿越问题。攻击者可以借助这种方式将可执行文件写入一些特定的位置。
解决方案:在下载文件时对文件名进行判断,过滤“../”这样的字符。
https请求失败的解决方案
问题描述:在使用WebView加载https协议的网页或资源时,如果该网站的安全证书不被Android认可,就会出现无法成功加载的问题。
解决方案:重写WebViewClient的onReceivedSslError方法,设置其接受所有网站的安全证书。
//WebViewClient
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();//接受所有网站证书
}
WebView中http和https混合使用的问题
问题描述:在Android 5.0及以上,WebView可能在加载混合使用http和https的网页时出现异常。比如在一个https的安全网页中加载使用http协议的资源将会失败。
解决方案:在Android 5.0后利用WebSettings设置WebView支持http和https混合内容模式。
if(Build.VERSION.SDK_INT>=21){//方式1
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
}
if(Build.VERSION.SDK_INT>=21){
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
需要注意,MIXED_CONTENT_ALWAYS_ALLOW
这个模式是不安全的,建议先使用MIXED_CONTENT_COMPATIBILITY_MODE
模式。这个模式会尝试以安全的方式加载部分http资源,另一部分http资源则不会被加载。资源是否能被加载的判断依据可能会随着版本的不同而改变,因此需要根据实际情况决定是否采用这一模式。
setDisplayZoomControls引起的崩溃问题
问题描述:我们知道,setDisplayZoomControls(true)方法会允许显示系统缩放按钮,这个缩放按钮会在每次出现后的几秒内逐渐消失。但是在部分系统版本中,如果在缩放按钮消失前退出了Activity,就可能引起应用崩溃。
解决方案:调用setDisplayZoomControls(false)方法不显示系统缩放按钮,反正使用手势捏合动作就可以实现网页的缩放功能了。如果确实需要使用缩放按钮,就需要在Activity的onDestroy方法中隐藏WebView。
webView.setVisibility(View.GONE);
6.demo下载
下载地址:WebViewDemo
7.附录-应用实例
应用一览:
1. 全屏
2. 预加载
3. 保存WebView状态
4. 屏蔽广告
5. 加载本地JS
6. 加载本地资源
7. WebView加载优化
8. 支持H5的定位功能
9. 截屏功能
10. 点击图片查看大图
11. 黑名单
12. 白名单
13. 文件上传
14. 文件下载
14. 自定义WebView长按菜单(实现复制、全选、分享等功能)
15. 判断WebView是否滚动到页面顶部或底部
16. 自定义错误页
17. 屏蔽WebView长按事件
具体内容请直接查阅以下博客:
Android WebView 应用实例(待填坑)
8.附录-常见API参考手册
请查阅以下博客:
9.附录-参考资料
http://www.jianshu.com/p/3c94ae673e2a
http://www.jianshu.com/p/a6f7b391a0b8
http://blog.csdn.net/qq_24530405/article/details/52067474
http://blog.csdn.net/qq_19431333/article/details/52351437
http://blog.csdn.net/lz8362/article/details/51864399
http://www.jianshu.com/p/345f4d8a5cfa
http://www.jianshu.com/p/3a345d27cd42