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的一些总结 恩恩

2 0