Android中的WebView
来源:互联网 发布:淘宝电器商城 编辑:程序博客网 时间:2024/05/23 22:57
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用这个是调用<body onload="javascript:myJs.clickMe()"> ...</body>
若webview中的js调用了本地的方法,正常情况下发布的debug包时,js调用是没有问题的,但是通常发布release商业版本的apk都是要经过代码混淆,这个时候会发现之前调用正常的js无法正常调用本地方法了。这是因为混淆的时候已经把本地代码的引用给打乱了,导致js中的代码找不到本地方法的地址。
我们可以通过在proguard.cfg文件中加上一些代码来解决,声明本地中被js调用的代码不被混淆
-keep public class com.test.webview.MyJavaScriptInterface{ public <methods>;}
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>myJS</title> <!-- 引入JS --> <script src="file:///android_asset/Hybrid.js" type="text/javascript"> <script type="text/javascript"> </script></head><body><div class="blog-header"> <h3>JS调用android中的方法</h3></div><ul class="entry"> <li> 弹出气泡提示<br/> <!-- function (obj, method, params, callback)--> <button onclick="Hybrid.call('hybrid','toast',{'entity':'我是气泡 .。o0'},function(res){alert(JSON.stringify(res))})"> 点击产生气泡 </button> </li> <br/> <br/> <li> 获得设备IMEI<br/> <!-- function (obj, method, params, callback)--> <button onclick="Hybrid.call('hybrid','getDeviceVersion',{}, function(res){alert(JSON.stringify(res))})"> 点击获取Imei号 </button> </li></ul></body></html>
我们可以看到有两个按钮有两个方法,和都是都过调用JS中的call方法 “function (obj, method, params, callback)” 执行的
3. native端的 JsCallJava 和 JsCallBack
JsCallJava 类主要提供两个功能,第一个 executeJS 方法
作用是从一个Map中查找key是不是存在,不存在则反射拿到对应的Class中的所有方法,将方法是 public static void 类型的,并且参数是Webview,JSONObject,Callback 类型的三个参数,如果满足条件,则将所有满足条件的方法 put 进去
另一个方法是 callJava方法
callJava方法,就是将js传来的 uri 进行解析,然后根据调用的类名的命名从刚刚的 map 中查找是否存在,存在的话拿到该类所有方法的 methodMap ,然后根据方法名从 methodMap 拿到方法,反射调用,并将参数传进去,参数就是刚才那三个参数:WebView,JSONObject,Callback
public class JsCallJava { //保存hybird和方法类的键值对 private static Map<String, ArrayMap<String, Method>> mInjectNameMethods = new ArrayMap(); /** * 动态注入方法 * * @param exposedName * @param clazz */ public static void executeJS(String exposedName, Class<JsMethod> clazz) { if (!mInjectNameMethods.containsKey(exposedName)) { try { mInjectNameMethods.put(exposedName, getAllMethod(clazz)); } catch (Exception e) { e.printStackTrace(); } } } /** * 通过反射获取类中所有符合规定的方法 * 以public static 开头的方法 * * @param injectedClass * @return */ public static ArrayMap<String, Method> getAllMethod(Class injectedClass) { ArrayMap<String, Method> map = new ArrayMap<>(); Method[] methods = injectedClass.getDeclaredMethods(); for (Method method : methods) { String name; if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) { continue; } //获取方法参数类型,约定好传参形式 Class[] parameters = method.getParameterTypes(); if (null != parameters && parameters.length == 3) { if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == JsCallBack.class) { map.put(name, method); } } } return map; } /** * 通过Uri资源格式解析返回的数据 * @param webView * @param uriString * @return */ public static String callJava(WebView webView, String uriString) { String methodName = ""; String className = ""; String param = "{}"; String port = ""; if (!TextUtils.isEmpty(uriString) && uriString.startsWith("hybrid")) { Uri uri = Uri.parse(uriString); className = uri.getHost(); param = uri.getQuery(); port = uri.getPort() + ""; String value = uri.getPath(); if (!TextUtils.isEmpty(value)) { methodName = value.replace("/", ""); } } if (mInjectNameMethods.containsKey(className)) { ArrayMap<String, Method> methodHashMap = mInjectNameMethods.get(className); if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) { Method method = methodHashMap.get(methodName); if (method != null) { try { method.invoke(null, webView, new JSONObject(param), new JsCallBack(webView, port)); } catch (Exception e) { e.printStackTrace(); } } } } return null; }}
看到 JsCallJava 类中用了 new JsCallBack(webView, port) 进行新建对象,该对象就是用来回调 JS 中回调方法的 java 对应的类。这个类你需要将 JS 传来的port传进来之外,还需要将 WebView 的引用传进来,因为要使用到WebView的loadUrl方法,这里使用弱引用来防止内存泄漏
/** * 返回响应 */public class JsCallBack { private static Handler mHandler = new Handler(Looper.getMainLooper()); private static final String CALLBACK_JS_FORMAT = "javascript:hybrid.onFinish('%s', %s);"; private String mPort; private WeakReference<WebView> mWebViewRef; public JsCallBack(WebView view, String port) { mWebViewRef = new WeakReference<>(view); mPort = port; } public void apply(JSONObject jsonObject) { final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject)); if (mWebViewRef != null && mWebViewRef.get() != null) { mHandler.post(new Runnable() { @Override public void run() { mWebViewRef.get().loadUrl(execJs); } }); } }}
apply方法通过Handler的切换回到主线程中执行,这是因为暴露给js的方法可能会在子线程中调用这个callback,这样的话就会报错
4. 在Activity中注册这个WebView
然后就是通过Activity中加载这个WebView来显示这个界面了,还有别忘了提供提供html中对应的供JS使用的native代码,方法要满足
public static void 开头并且有Webview,JSONObject,Callback三个参数
比如toast方法
public static void toast(WebView webView, JSONObject entity, JsCallBack back)
然后我们在主Acitivity中加入这个WebView
public class MainActivity extends Activity { private WebView mWebView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mWebView = new WebView(this); setContentView(mWebView); WebSettings settings = mWebView.getSettings(); settings.setJavaScriptEnabled(true); mWebView.setWebChromeClient(new MyWebChromeClient()); mWebView.setWebViewClient(new MyWebViewClient()); mWebView.loadUrl("file:///android_asset/myHtml.html"); JsCallJava.executeJS("hybrid", JsMethod.class); }}
然后我们看一下效果
其他细节请看这个小例子:http://download.csdn.net/detail/hkx_smile/9505711
这就是关于WebView的一些总结 恩恩
- Android 中的WebView
- Android中的WebView使用
- Android WebView中的WebViewClient
- Android 中的WebView
- Android中的WebView总结
- android中的webview进度条
- Android中的WebView
- Android中的WebView
- Android:WebView中的误区
- 【WebView】Android WebView中的Cookie操作
- 【WebView】Android WebView中的Cookie操作
- Android中的WebView控件用法
- android中的webView与js
- android中的WebView缓存分析
- Android中的WebView使用(一)
- 聊聊Android中的WebView控件
- Android中的webview详细使用
- Android中的WebView常用用法
- Android 高质量高压缩比图像压缩
- C语言中自己写几个简单的库函数
- 百度地图error code:162
- 内边距
- Mybatis Generator 生成的mapper只有insert方法
- Android中的WebView
- POJ_2352_Stars
- 判断大小端问题
- spfa找负环要注意的地方
- pl/sql developer软件学习总结
- OSGi Manifest元文件
- MDK错误提示
- caffe 官方例程之R-CNN(物体检测)
- c++中.dll与.lib文件的生成与使用的详解