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">