Android webview详解

来源:互联网 发布:图片制作视频软件 手机 编辑:程序博客网 时间:2024/05/29 16:47
    Android Hybrid解析

    市面上的App大致分为3类::Native App、Web App和Hybrid App,Hybrid App兼具"Native App"良好的用户交互体验和"Web App"跨平台开发的优势

   Webview解析

    解决webview加载劣势——资源预加载:

         资源加载缓慢: H5页面是从服务器上下发的,客户端的页面在内存里面,加载网页的时间更长,而且受限于网络情况,但是这种问题在某种程度上市可以弥补的,比如我们可以做一些资源预加载方案,下面列举资源预加载:

         一、使用webview自身的缓存机制: 如果我们在App里面访问一个页面,短时间内再次访问这个页面,就会感觉第二次打开的时候顺畅很多,加载速度比第一次的时间要短,这个就是因为webview自身内部会做一些缓存,只要打开过得资源,他都会试着缓存到本地,第二次需要访问的时候它直接从本地读取,但这个读取其实是不太稳定的东西,关掉之后,或者说这种缓存失效之后,系统会自动把他清楚。我们在应用启动的时候开一个像素的webview,事先访问以下我们常用的资源,后续打开页面的时候如果再用到这些资源他就可以从本地读取,页面加载的时间会短一些。

         二、自己去构建,自己管理缓存: 把这些需要预加载的资源放在App里面,他可能是预先放进去的,也可能是后续下载的,问题在于前端这些页面怎么去缓存,两个方案,第一种是前端可以在H5打包的时候把里面的资源URL进行替换,这样可以直接访问本地地址;第二种是客户端可以拦截这些网页发出的所有请求做替换:美团就是使用的的第二种预加载方案: 详情看美团大众点评Hybrid化建设,实现原理:每当webview发起资源请求的时候,我们会拦截这些资源的请求,去本地检查一下我们这些静态资源本地离线包有没有。针对本地的缓存文件我们有些策略能够及时的去更新它,为了安全考虑,也需要同时做一些预加载和安全包的加密工作。预加载有以下几点优势:

           1、我们拦截了webview里面发出的所有的请求,但是并没有替换里面的前端应用的任何代码,前端这套页面代码可以在App内,或者其他的App里面都可以直接访问,他不需要为我们App做定制化的东西

            2、这些URL请求,他会直接带上先前用户操作留下的Cookie,因为我们没有更改资源原始URL地址;

            3、整个前端在用离线包和缓存文件的时候是完全无感知的,前端只用管写一个自己的页面,客户端会帮他处理好这样一些静态资源预加载的问题,有这个离线包的话加载速度会变快很多,特别是弱网情况下,没有这些离线包加载速度会慢一些。而且如果本地离线包的版本不能跟H5匹配的话,H5页面也不会发生什么问题。

    WebView的常见设置:

            WebSetting webSettings=webView.getSettings();

             //设置这个属性为true允许webview和js代码进行交互,这个本身会有漏洞

            webSettings.setJavaScriptEnabled(true);

            //设置WebView是否可以打开WebView新窗口

            webSettings.setJavaScriptCanOpenWindowAutomatically(true);

            //webview是否支持多窗口,如果设置未true,需要重写

            //WebChromeClient#onCreateWindow(WebView,boolean,boolean,Message)函数,默认为false

            webSettings.setSuppportMutipleWindows(true); 

           //这个属性用来设置webview是否能够加载图片资源,包括哪些使用data uri协议嵌入的图片。使用setBlockNetworkImage(boolean)方法来控制仅仅加载使用网络URI协议的图片,需要提到的一点是如果这个设置从false变为true之后,所有被内容引用的正在显示的webview图片资源都会被自动加载,该标识默认值为true。

           webSettings.setLoadsImagesAutomatically(false);

           //标识是否加载网络上的图片(使用http或者https域名的资源),需要注意的是如果getLoadsImageAutomatically不返回true,这个标识将没有作用

            webSettings.setBlockNetworkImage(boolean)

            //显示webView提供的缩放控件

            webSettings.setDisplayZoomControls(true);

            webSettings.setBuiltInZoomControls(true)

             //设置是否启动WebView API,默认值为false

            webSettings.setDatabaseEnabled(true);

            //打开webview的storage功能,这样JS的localStorage,sessionStorage对象才可以使用(比如一张网页的拼图js页面)

            webSettings.setDomStorageEnabled(true);

            //打开WebView的LBS功能,这样JS的geolocation对象才可以使用

            webSettings.setGeolocationEnabled(true);

            webSettings.setGeolocationDatabasePath("");

            //设置是否打开webview表单数据的保存功能

             webSettings.setSaveFormData(true);

            //设置webview的默认的userAgent字符串

            webSettings.setUserAgentString("");

            //设置是否WebView支持"viewport"的HTML meta tag,这个标识用来屏幕自适应的,当这个标识设置为false时,页面布局的宽度被一直设置为css中控制的webview的宽度;如果设置为true并且页面含有viewport meta tag,那么被这个tag声明的宽度将会被使用。

            //webSettings.setUseWideViewPort(false);

            //设置webview的字体,可以通过这个函数,改变webview的字体,默认字体为"sans-serif"

             webSettings.setStandardFontFamily("");

              //设置webview字体的大小,默认大小为16

             webSettings.setDefaultFontSize(20);

             //设置webview支持的最小字体大小,默认为8

              webSettings.setMinimumFontSize(12);

              //设置页面是否支持缩放

              webSettings.setSupportZoom(true);

               //设置文本的缩放倍数,默认为100

              webSettings.setTextZoom(2);

             

然后还有最常用的 WebViewClient 和 WebChromeClient,WebViewClient主要辅助WebView执行处理各种响应请求事件的,比如:

  • onLoadResource

  • onPageStart

  • onPageFinish

  • onReceiveError

  • onReceivedHttpAuthRequest

  • shouldOverrideUrlLoading

WebChromeClient 主要辅助 WebView 处理J avaScript 的对话框、网站 Logo、网站 title、load 进度等处理:

  • onCloseWindow(关闭WebView)

  • onCreateWindow

  • onJsAlert

  • onJsPrompt

  • onJsConfirm

  • onProgressChanged

  • onReceivedIcon

  • onReceivedTitle

  • onShowCustomView    

   WebView的缓存模式:
        LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
        LOAD_DEFAULT: 根据cache-control决定是否从网络上取数据
        LOAD_NO_CACHE: 不使用缓存,只从网络获取数据
        LOAD_CACHE_ELSE_NETWORK: 只要本地有,无论是否过期,或者网页使用的缓存模式是"no-cache",都使用缓存中的数据,在没有缓存时才从网络上获取
    Webview清空缓存和清空历史记录:
          CacheManager处理webview缓存相关,mWebview.clearCache(true):清空缓存,mWebview.clearHistory()
     Webview 与 native的交互
           js调用native三种方式:
           第一种方式:通过addJavascriptInterface方法进行添加对象映射: 第一步,设置属性:mWebView.getSettings().setJavaScriptEnabled(true);第二步,Native需要定义一个类并编写带有JavascriptInterface注解的调用方法
            
public class JSObject {    private Context mContext;    public JSObject(Context context) {        mContext = context;    }    @JavascriptInterface    public String showToast(String text) {        Toast.show(mContext, text, Toast.LENGTH_SHORT).show();        return "success";    }}...//特定版本下会存在漏洞mWebView.addJavascriptInterface(new JSObject(this), "myObj");
        js调用代码:
function showToast(){    var result = myObj.showToast("我是来自web的Toast");}
       第二种方式: 利用WebViewClient接口回调方法拦截url: 这种方式使用频次也很高,上面介绍的WebViewClient,其中有个回调接口shouldOverrideUrlLoading(WebView view,String url),我们利用这个拦截url,然后解析这个url的协议,如果发现是我们预先约定好的协议就开始解析参数,执行相应的逻辑,注意:这个方法在API24版本已经废弃了,需要使用shouleOverrideUrlLoading(WebView view,WebResourceRequest request)
拦截示例代码如下:
public boolean shouldOverrideUrlLoading(WebView view, String url) {    //假定传入进来的 url = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数    Uri uri = Uri.parse(url);    String scheme = uri.getScheme();    //如果 scheme 为 js,代表为预先约定的 js 协议    if (scheme.equals("js")) {          //如果 authority 为 openActivity,代表 web 需要打开一个本地的页面        if (uri.getAuthority().equals("openActivity")) {              //解析 web 页面带过来的相关参数            HashMap<String, String> params = new HashMap<>();            Set<String> collection = uri.getQueryParameterNames();            for (String name : collection) {                params.put(name, uri.getQueryParameter(name));            }            Intent intent = new Intent(getContext(), MainActivity.class);            intent.putExtra("params", params);            getContext().startActivity(intent);        }        //代表应用内部处理完成        return true;    }    return super.shouldOverrideUrlLoading(view, url);}
     js中的代码指定location地址:
function openActivity(){    document.location = "js://openActivity?arg1=111&arg2=222";}
      webview通过shouldOverrideUrlLoading拦截js相关数据并做处理,处理完成后如果web端想要得到方法的返回值,只能通过webview的loadUrl方法去执行js方法把返回值传递回去,相关代码如下:
      
//javamWebView.loadUrl("javascript:returnResult(" + result + ")");//javascriptfunction returnResult(result){    alert("result is" + result);}
        备注:这种方式打开Native页面还是很合适的,制定好相应的协议,就能够让web端具有打开所有本地页面的能力了。
        第三种方式: 利用WebChromeClient回调接口的三个方法拦截消息
        
@Overridepublic boolean onJsAlert(WebView view, String url, String message, JsResult result) {    return super.onJsAlert(view, url, message, result);}@Overridepublic boolean onJsConfirm(WebView view, String url, String message, JsResult result) {    return super.onJsConfirm(view, url, message, result);}@Overridepublic boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {    //假定传入进来的 message = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数    Uri uri = Uri.parse(message);    String scheme = uri.getScheme();    if (scheme.equals("js")) {        if (uri.getAuthority().equals("openActivity")) {            HashMap<String, String> params = new HashMap<>();            Set<String> collection = uri.getQueryParameterNames();            for (String name : collection) {                params.put(name, uri.getQueryParameter(name));            }            Intent intent = new Intent(getContext(), MainActivity.class);            intent.putExtra("params", params);            getContext().startActivity(intent);            //代表应用内部处理完成            result.confirm("success");        }        return true;    }    return super.onJsPrompt(view, url, message, defaultValue, result);}
       通过WebChromeClient接口,拦截JS中的几个提示方法,也就是几种样式的对话框,在JS中有三个常用的对话框方法:
        1、onJsAlert弹出警告框,一般情况下在Android中为Toast,在文本里面加入\n就可以换行;
        2、onJsConfirm 弹出确认框,会返回布尔值,通过这个值可以判断点击是确认还是取消,true表示点击了确认,false表示点击了取消
        3、onJsPrompt弹出输入框,点击确认返回输入框中的值,点击取消返回null
        prompt方法调用如下所示:
        
function clickprompt(){    var result=prompt("js://openActivity?arg1=111&arg2=222");    alert("open activity " + result);}

       这里需要注意的是 prompt 里面的内容是通过 message 传递过来的,并不是第二个参数的 url,返回值是通过 JsPromptResult 对象传递。为什么要拦截 onJsPrompt 方法,而不是拦截其他的两个方法,这个从某种意义上来说都是可行的,但是如果需要返回值给 web 端的话就不行了,因为 onJsAlert 是不能返回值的,而 onJsConfirm 只能够返回确定或者取消两个值,只有 onJsPrompt 方法是可以返回字符串类型的值,操作最全面方便。
      以上三种方案的总结对比
      第一种,调用onJavaScriptInterface方法: 是现在目前最普遍的用法,方便简洁,但唯一的不足是在4.2系统以下存在漏洞问题;
      第二种,通过shouldOverrideUrlLoading,拦截url并解析,如果是已经约定好的协议则进行相应规定好的操作,缺点是协议的约束需要记录一个规范的文档,而且从Native层往web层传递值比较繁琐,优点就是不会存在漏洞,ios7之下的版本就是使用的这种方式
      第三种,利用js拦截方法,和第二种方式的思想其实是类似的,只是拦截的方法变了,这里拦截了JS中的三种对话框方法,二这三种对话框方法的区别在于返回值问题,alert对话框没有返回值,confirm对话框只有两种状态的返回值,prompt对话框可以返回任类型的返回值,缺点就是协议的制定比较麻烦,需要记录详细的文档,但是不会存在第二种方法的漏洞问题,这里有个疑问?如果l拦截onJsPromt是不是网页需要有个输入弹出框然后执行我们的拦截,如果实际需求不需要弹出输入框这种情况是不是就不能用了?
       native调用js
       第一种方式,webview的loadUrl方法:
       
//javamWebView.loadUrl("javascript:show(" + result + ")");//javascript<script type="text/javascript">function show(result){    alert("result"=result);    return "success";}</script>
       注意,调用的名字一定要对应上,要不然调用不成功,而且js的调用一定要在onPageFinished函数回调之后才能调用,要不然会失败。
        第二种方式
       Google在Android 4.4为我们新增了一个方法,这个方法比loadUrl方法更方便简洁,而且比loadUrl效率更高,因为loadUrl的执行会造成页面刷新一次,这个方法不会,因为这个方法是在4.4版本财引入的,所以我们使用的时候需要添加版本判断
     
final int version = Build.VERSION.SDK_INT;if (version < 18) {    mWebView.loadUrl(jsStr);} else {   /*jsStr是javascript执行脚本,ValueCallBack是执行完脚本的返回值,也可能是null,在没有返回的情况*/    mWebView.evaluateJavascript(jsStr, new ValueCallback<String>() {        @Override        public void onReceiveValue(String value) {            //此处为 js 返回的结果        }    });}
                                                                                        参考自微信公众号——App架构师,Android WebView详解
1 0