Android中的WebView
Android基于效率和灵活性的考虑,现在越来越多的开发者采用Hybrid方式开发App,那么如何使android和h5有效结合呢,WebView就可以使网页轻松的内嵌到app里,还可以直接跟js相互调用。这么看来Hybrid开发离不开WebView这个组件了,那就让我们探讨一下Webview的一些属性和功能以及用法
WebView是什么
WebView类是一个扩展Android的视图类,允许将Web页面作为活动布局的一部分。它并不一个完整网络浏览器,它采用了WebKit渲染引擎来显示网页,默认情况下,只显示一个Web页面。WebView可以方便的在线更新内容,不需要发布新版本的app来更新模块。所以使用WebView都需要在android清单文件中加入如下连接网络的权限,除非访问的是本地assets中的html资源
< uses-permission android:name=”android.permission.INTERNET”/>
那么我们如果在android界面中加入一个WebView呢,其实很简单我们可以通过在xml中配置WebView这个组件
<?xml version="1.0" encoding="utf-8"?><WebView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="match_parent"/>
同样我们可以在Activity中直接建立一个WebView显示这个网页
WebView mWeb = new WebView(this);mWeb.loadUrl("http://www.baidu.com");setContentView(mWeb);
那么我们如何用WebView来显示一个网页呢,其实也是很简单的,我们可以通过loadUrl方法来显示一个网页,上面在Activity中的WebView已经使用了这个方法,如果加载本地文件我们可以使用
webView.loadUrl("file:///android_asset/XX.html");
这个本地的html文件存放于assets 文件中。如果使用xml中配置的WebView我们同样也适用
WebView myWebView = (WebView) findViewById(R.id.webview);myWebView.loadUrl("http://www.example.com");
通过实际运行一下这个WebView,我们可以发现显示效果并不是我们想象的那样,app并没有在这个WebView的控件中显示这个网页,而是启动了手机的浏览器。我们可以通过WebViewClient覆盖WebView默认使用第三方或系统默认浏览器打开网页的行为,使网页用WebView打开
mWeb = (WebView) findViewById(R.id.mWebView); mWeb.loadUrl("http://www.baidu.com"); mWeb.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); //true 说明事件被webview消费了,不用再向上传播,否则就要上传播 return true; } });
这样就网页就显示在我们自己WebView中了 ,
WebChromeClient和WebViewClient
实际使用的话,如果你的WebView只是用来处理一些html的页面内容,只用WebViewClient就行了,如果需要更丰富的处理效果,比如JS、进度条等,就要用到WebChromeClient。
WebViewClient
主要帮助WebView处理各种通知、请求事件。如果希望链接在当前WebView中显示而不是外部浏览器,必须覆盖 webview的WebViewClient对象。
- shouldOverrideUrlLoading(WebView view, String url)
在web页面里单击链接的时候,会自动调用android自带的浏览器来打开链接,需要通过这个方法在本页面打开
- onLoadResource(WebView view, String url)
通知主程序WebView要通过给定的url来加载资源了,这个方法在加载资源时响应
- onPageStarted(WebView view, String url, Bitmap favicon)
通知主程序开始加载界面,在加载页面时响应
- onPageFinished(WebView view, String url)
通知主程序界面加载完毕,在加载页面结束时响应
- onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
通知主程序加载错误,在加载出错时响应
- onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)
通知主程序,在加载资源时发生了SSL错误
WebChromeClient
主要辅助WebView处理Javascript的对话框、网站图标、网站title、加载进度等,一下是一些方法,更多需要看API
- onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg)
通知主程序启动一个新窗口
- onCloseWindow(WebView window)
通知主机应用程序关闭指定的WebView,如有必要,将其从系统中删除。
- onJsConfirm(WebView view, String url, String message, JsResult result)
通知客户端显示一个确认对话框给用户
- onJsAlert(WebView view, String url, String message, JsResult result)
通知客户端显示一个警告对话框
- onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)
通知客户端显示一个提示对话框
- onProgressChanged(WebView view, int newProgress)
通知主应用程序加载页面的当前进展情况。
WebSettings
在创建WebView时,系统有一个默认的设置,我们可以通过WebView.getSettings来得到这个设置。负责管理一个web视图设置,当第一次创建一个web视图时,它获得了一组默认设置,这些默认设置通过所有的getxx方式返回。从WebView.getSettings方法获得的WebSettings对象绑定到WebView的生命周期中,如果这个web视图被破坏,在WebSettings调用任何方法将抛出IllegalStateException。这个对象一般负责管理WebView的缩放、字体、编码等设置。
以下是一些常用方法,具体查询API
- getAllowFileAccess()
获取此的WebView是否支持文件访问
- setBlockNetworkImage(boolean flag)
设置WebView是否从网络上加载图像资源,是否显示网络图像
- setBuiltInZoomControls(boolean enabled)
设置是否显示缩放工具
- setCacheMode(int mode)
设置WebView的缓存模式,覆盖默认的缓存模式,有以下几种
LOAD_NO_CACHE:不要使用缓存,从网络加载
LOAD_CACHE_ELSE_NETWORK:如果内容已经存在cache 则使用cache,即使是过去的历史记录。如果cache中不存在,从网络中获取。所以加上这句,不仅可以使用cache离线显示用户浏览过的内容,还可以在有网络的情况下优先调用缓存,为用户减少流量
LOAD_CACHE_ONLY:只从缓存中加载,不使用网络
LOAD_DEFAULT:不设置时候的默认缓存模式,即不使用缓存
- setDefaultFontSize(int size)
设置默认的字体大小(1-72),默认值是16
- setDefaultTextEncodingName(String encoding)
设置解码HTML页面时使用的默认文本编码名称,默认值是“UTF-8”。
- setJavaScriptEnabled(boolean flag)
通知的WebView可以执行JavaScript, 默认为false。
- setSupportZoom (boolean support)
设置是否支持变焦
WebView开发问题
浏览网页的回退
当我们在使用一些浏览器浏览网页的时候,经常会遇到这种功能,点击Android的返回键,就会产生网页回退,这个功能应该如何实现呢
webview.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { if (keyCode == KeyEvent.KEYCODE_BACK && webview.canGoBack()) { webview.goBack(); return true; } } return false; } });
错误处理
当我们使用浏览器的时候,通常因为加载的页面的服务器的各种原因导致各种出错的情况,最平常的比如404错误,通常情况下浏览器会提示一个错误提示页面。事实上这个错误提示页面是浏览器在加载了本地的一个页面,用来提示用户目前已经出错了。是当我们的app里面使用webview控件的时候遇到了诸如404这类的错误的时候,若也显示浏览器里面的那种错误提示页面就显得很丑陋了,那么这个时候我们的app就需要加载一个本地的错误提示页面,这里就是其实就是webview如何加载一个本地的页面
webview.setWebViewClient(new WebViewClient(){ @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { switch(errorCode) { case HttpStatus.SC_NOT_FOUND: view.loadUrl("file:///android_assets/error_handle.html"); break; } } });
其实,当出错的时候,我们也可以选择隐藏掉WebView,而显示native的错误处理控件,这个时候只需要在onReceivedError里面显示出错误处理的native控件同时隐藏掉webview即可。
WebView和javaScript代码交互
1. WebView调用js
这种方式比较简单,可以通过loadUrl方法实现
webView.loadUrl(“javascript:play()”);
表示webview在调用js中的一个叫做play的方法
2. js调用WebView
下面这个类是用于暴露给js的类
/** * 暴露给js的类和方法 */class MyJavaScriptInterface { private Context context; public MyJavaScriptInterface(Context con) { this.context = con; } @JavascriptInterface public void clickMe(Context context) { Toast.makeText(context, "click", Toast.LENGTH_SHORT).show(); }}
那么在js中应该如何调用这个java代码呢,我们需要首先设置WebView允许JavaScript执行,然后将本地的类(用于被js调用的类)映射出去
“myJs”这个名字就是公布出去给JS调用的,那么js就可以直接调用本地的MyJavaScriptInterface类中的方法了
webview.getSettings().setJavaScriptEnabled(true); webview.addJavascriptInterface(new MyJavaScriptInterface(context),"myJs");//-----js用这个是调用"javascript:myJs.clickMe()"> ...
若webview中的js调用了本地的方法,正常情况下发布的debug包时,js调用是没有问题的,但是通常发布release商业版本的apk都是要经过代码混淆,这个时候会发现之前调用正常的js无法正常调用本地方法了。这是因为混淆的时候已经把本地代码的引用给打乱了,导致js中的代码找不到本地方法的地址。
我们可以通过在proguard.cfg文件中加上一些代码来解决,声明本地中被js调用的代码不被混淆
-keep public class com.test.webview.MyJavaScriptInterface{ public ;}
js对象注入漏洞解决
问题及解决
在上面我们通过webview.addJavascriptInterface()方法将这个功能类暴露给了js,但是addJavascriptInterface()方法存在安全隐患,在JavaScript中可以反射调用到Class的任意属性,比如可以通过对象取得包名和类名,获取类的结构等,再有甚者可以通过这种方式进行远程挂马,可以通过网页挂马的形式来恶意获取用户信息破坏运行环境等行为。Google官方在android 4.2之后通过加入@JavascriptInterface 注释来选择性的暴露方法,即只有标示了@JavascriptInterface的方法JavaScript才能调到。 所以在新版android中可以通过addJavascriptInterface/@JavascriptInterface这个方式解决这个漏洞, 但是由于目前绝大多数app支持android 4.2以前的版本。那么针对android 4.2之前的版本我们还有什么方法么?
首先,我们肯定不能再调用addJavascriptInterface方法了。那么我们如何交互呢,我们知道JS与Java进行交互,有以下几种,比如prompt, alert等,这样的动作都会对应着WebChromeClient类中相应的方法,对于prompt,它对应的方法是onJsPrompt方法,即WebChromeClient 输入框弹出模式,通过这个方法,JS能把信息传递到Java,而Java同样也能把信息传递给JS
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)
我们可以利用这个方式,进行数据传递,在使用时候,我们需要判断系统版本是否在4.2以下,因为在4.2以上,Android修复了这个安全问题,我们只是需要针对4.2以下的系统作修复
原理: 我们让JS调用一个Javascript方法,这个方法调用prompt方法,通过prompt把JS中的信息传递过来,这些信息是我们自己组合的包括方法名称参数等信息,我们最好这个信息做成一个json样式,这样更方便解析。然后在WebView端的onJsPrompt方法中,我们用json的方式去解析传递过来的文本,得到方法名、参数等,然后通过反射机制,调用指定对象的方法,这样就实现了js调用WebView,也就可以解决js注入的漏洞
用法及流程
那么我们以一个实例来看一下如何使用 prompt 方式的传递信息,实现 native 和 webview 两端的详细通信
1. 制定通信协议
数据的传输需要约定双方的通信协议,所以我们需要定制一套通信协议来让 js 和 native 相互联系
1)js传递给native的通信协议
我们可以通过uri的形式传递我们的数据,通过 scheme://host:port/path 来定制传递的信息,我们可以定制如下 “hybird协议”
hybrid://className:port/methodName?jsonObject
hybrid:做为传输协议
className:对应的native类
port:native的执行结果callback 的缓存位置
methodName:native类中提供的方法
jsonObject:用json封装好的方法参数
比如我们要调用android中Toast的makeText方法,我们就可以如下方式传递
hybird://Toast:callbackAddress/makeText ? {“msg”:”native log”}
2) native传递给js的通信协议
native向js的通信协议也需要制定,一个必不可少的元素就是返回值,这个返回值和js的参数做法一样,通过json对象进行传递,该json对象中有状态码code,提示信息msg,以及返回结果result,如果code为非0,则执行过程中发生了错误,错误信息在msg中,返回结果result为null,如果执行成功,返回的json对象在result中,看一下这个jsonObj
//失败的样子 eg:{ "code":404, "msg":"method is not exist", "result":null}//成功的样子{ "code":0, "msg":"success", "result":{ "key1":"value1", "key2":"value2", }}
获取返回值通过 native调用js暴露的方法即可,需要将返回的jsonObj和js层传给native层的port一并带上
webView.loadUrl("javascript:Hybrid.onFinish(port,jsonObj);");
2. 构建 Hybrid.js 和 myHtml.html 文件
首先我们看一下 Hybrid.js 文件
(function (win) { var hasOwnProperty = Object.prototype.hasOwnProperty; var Hybrid = win.Hybrid || (win.Hybrid = {}); var Inner = { callbacks: {}, call: function (obj, method, params, callback) { var port = Util.getPort(); this.callbacks[port] = callback; var uri=Util.getUri(obj,method,params,port); window.prompt(uri, ""); }, onFinish: function (port, jsonObj){ var callback = this.callbacks[port]; callback && callback(jsonObj); delete this.callbacks[port]; }, }; var Util = { getPort: function () { return Math.floor(Math.random() * (1 << 30)); }, getUri:function(obj, method, params, port){ params = this.getParam(params); var uri = 'hybrid://' + obj + ':' + port + '/' + method + '?' + params; return uri; }, getParam:function(obj){ if (obj && typeof obj === 'object') { return JSON.stringify(obj); } else { return ''; } } }; for (var key in Inner) { if (!hasOwnProperty.call(Hybrid, key)) { Hybrid[key] = Inner[key]; } }})(window);
我们定义了Util类,其中包含三个方法,分别是
getPort:用于随机生成port
getUri:用于生成native需要的协议uri
getParam:用于生成json字符串
Inner类包含call和onFinish方法,在 call 方法中,调用 Util.getPort() 获得了 port 值,然后将 callback 对象存储在了callbacks中的 port 位置,接着调用 Util.getUri() 将参数传递过去,将返回结果赋值给 uri,调用window.prompt(uri, “ ”) 将uri传递到native层。而 onFinish() 方法接受native回传的 port 值和执行结果,根据 port 值从 callbacks 中得到原始的 callback 函数,执行 callback 函数,然后从 callbacks 中删除。最后将Inner类中的函数暴露给外部的JSBrige对象,通过一个for循环一一赋值
然后再看一下我们的 myHtml.html 文件
<html><head> <meta charset="utf-8"> <title>myJStitle> <script src="file:///android_asset/Hybrid.js" type="text/javascript">