Android控件之WebView

虽然你们是扮演路人甲乙丙丁,但是一样是有生命,有灵魂的。”——《喜剧之王》

前言

在开发原生Android应用过程中,某些情况下(比如活动详情页)为了赋予应用适当的动态性我们经常需要使用webview控件在应用中内置web网页,对于混合app(Hybrid App)来说WebView更是必不可少,所以学习webview相关知识,对webview有一个充分全面的了解对Android开发者来说非常重要。

(注:Android WebView从实现的Framework层大致可以分为三段Android 4.0系列,Android 4.1---4.3系列,Android 4.4及其以上系列。Android 4.4以下(不包含4.4)系统WebView底层实现是采用WebKit内核,而在Android4.4及以上Google改用Chromium 作为系统webview的底层内核。在这一变化中Android 提供的WebView相关API并没有发生大变化,在4.4上也兼容低版本的API并且引进了少部分API。基于Chromium WebView提供更广的HTML5,CSS3,JavaScript支持,提升了性能,并增加了一些新的功能支持。4.0及4.1-4.3均基于WebKit内核,两者之间的差异主要集中在Framework上的变化,WebKit内核极其在Android上的表现机制并没有发生很大变化,他们的渲染机制是相同的。可以简单理解为4.1-4.3主要对Framework进行修改,目地是为了将内核与上层API接口分离开来,为之后替换内核做准备。由于目前Android系统市场占有率4.4以下机型极少,所以本文主要针对4.4及以上webview进行讲解。)

webview简介

WebView是Android系统提供能显示网页的系统控件,它是一个特殊的View,同时它也是一个ViewGroup可以有很多其他子View。

webview作用

  • 显示和渲染Web页面
  • 加载网络上或本地assets中的html文件
  • 与JavaScript交互调用

将WebView添加到应用

需要在AndroidManifest中添加网络权限

<manifest ... >
    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifest>
  • 方式一:将以下代码添加到Activity的布局XML文件中
 <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

在Activity中引用控件

WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.loadUrl("http://www.example.com");
  • 方式二:直接在代码中添加
WebView myWebView = new WebView(activityContext);
setContentView(myWebView);
or
FrameLayout flContainer = (FrameLayout) findViewById(R.id.fl_container);
WebView myWebView = new WebView(this);
flContainer.addView(myWebView );

WebView的基础方法

  • String getUrl():获取当前页面的URL。
  • String getTitle():获取当前页面的标题。
  • Bitmap getFavicon():获取当前页面的favicon
  • int getProgress():获取当前页面的加载进度
  • setInitialScale(int scaleInPercent):设置初始缩放比例
  • reload():重新reload当前的URL,即刷新。

WebView加载URL

  • 方式1. 加载一个网页
myWebView .loadUrl("https://www.baidu.com/");
  • 方式2:加载apk包中的html页面
myWebView .loadUrl("file:///android_asset/test.html");
  • 方式3:加载手机本地的html页面
myWebView .loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");
  • 方式4: 加载 HTML 页面的一小段内容
// 创建HTML文本
// 转化未编码HTML文本为字节码,并进行Base64编码
// 加载编码后的文本
String unencodedHtml =
     "&lt;html&gt;&lt;body&gt;'%23' is the percent code for ‘#‘ &lt;/body&gt;&lt;/html&gt;";
String encodedHtml = Base64.encodeToString(unencodedHtml.getBytes(),
        Base64.NO_PADDING);
myWebView.loadData(encodedHtml, "text/html", "base64");

其他方法:

  • loadUrl(String url, Map<String, String> additionalHttpHeaders):携带http headers加载URL指定的网页
  • postUrl(String url, byte[] postData):使用POST请求加载指定的网页

WebView的状态

//激活WebView为活跃状态,能正常执行网页的响应
webView.onResume() ;

//当页面被失去焦点被切换到后台不可见状态,需要执行onPause
//通过onPause动作通知内核暂停所有的动作,比如DOM的解析、plugin的执行、JavaScript执行。
webView.onPause();

//当应用程序(存在webview)被切换到后台时,这个方法不仅仅针对当前的webview而是全局的全应用程序的webview
//它会暂停所有webview的layout,parsing,javascripttimer。降低CPU功耗。
webView.pauseTimers()
//恢复pauseTimers状态
webView.resumeTimers();

//销毁Webview
//在关闭了Activity时,如果Webview的音乐或视频,还在播放。就必须销毁Webview
//但是注意:webview调用destory时,webview仍绑定在Activity上
//这是由于自定义webview构建时传入了该Activity的context对象
//因此需要先从父容器中移除webview,然后再销毁webview:
rootLayout.removeView(webView); 
webView.destroy();

关于前进 / 后退网页

//是否可以后退
Webview.canGoBack() 
//后退网页
Webview.goBack()

//是否可以前进                     
Webview.canGoForward()
//前进网页
Webview.goForward()

//以当前的index为起始点前进或者后退到历史记录中指定的steps
//如果steps为负数则为后退,正数则为前进
Webview.goBackOrForward(intsteps) 

常见用法:Back键控制网页后退

  • 问题:在不做任何处理的情况下 ,浏览网页时点击系统的“Back”键,整个 Browser 会调用 finish()而结束自身
  • 目标:点击“Back”键后,是网页回退而不是退出浏览器
  • 解决方案:在当前Activity中处理并消费掉该 “Back”事件
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if ((keyCode == KEYCODE_BACK) && myWebView .canGoBack()) { 
        myWebView .goBack();
        return true;
    }
    return super.onKeyDown(keyCode, event);
}

清除缓存数据

//清除网页访问留下的缓存
//由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序.
Webview.clearCache(true);

//清空当前页面之前的所有记录,也就是说当前的页面记录并不
//会被删除。比如从页面A打开页面B的同时调用该方法,此时当前页面是A,清空的是A的之前的记录,A的自身
//记录还在。因此要在当前页面是B的时候调用该方法。
Webview.clearHistory();

//这个api仅仅清除自动完成填充的表单数据,并不会清除WebView存储到本地的数据
Webview.clearFormData();

常用工具类

WebSettings

设置webview属性需要依赖WebSettings类

//声明WebSettings子类
WebSettings webSettings = myWebView .getSettings();

//如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
// 若加载的 html 里有JS 在执行动画等操作,会造成资源浪费(CPU、电量)
// 在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 即可
webSettings.setJavaScriptEnabled(true);  

//支持插件
webSettings.setPluginsEnabled(true); 

//设置自适应屏幕,两者合用
webSettings.setUseWideViewPort(true); //将图片调整到适合webview的大小 
webSettings.setLoadWithOverviewMode(true); // 缩放至屏幕的大小

//缩放操作
webSettings.setSupportZoom(true); //支持缩放,默认为true。是下面那个的前提。
webSettings.setBuiltInZoomControls(true); //设置内置的缩放控件。若为false,则该WebView不可缩放
webSettings.setDisplayZoomControls(false); //隐藏原生的缩放控件

//其他细节操作
webSettings.setCacheMode(@CacheMode int mode); //设置WebView的缓存模式。当我们加载页面或从上一个页面返回的时候,会按照设置的缓存模式去检查并使用(或不使用)缓存。缓存模式有四种:
//LOAD_DEFAULT:默认的缓存使用模式。在进行页面前进或后退的操作时,如果缓存可用并未过期就优先加载缓存,否则从网络上加载数据。这样可以减少页面的网络请求次数。
//LOAD_CACHE_ELSE_NETWORK:只要缓存可用就加载缓存,哪怕它们已经过期失效。如果缓存不可用就从网络上加载数据。
//LOAD_NO_CACHE:不加载缓存,只从网络加载数据。
//LOAD_CACHE_ONLY:不从网络加载数据,只从缓存加载数据。
//通常我们可以根据网络情况将这几种模式结合使用,比如有网的时候使用LOAD_DEFAULT,离线时使用LOAD_CACHE_ONLY、LOAD_CACHE_ELSE_NETWORK,让用户不至于在离线时啥都看不到
webSettings.setAllowFileAccess(boolean allow):是否可访问本地文件,默认值 true
webSettings.setAllowFileAccessFromFileURLs(boolean flag):是否允许通过file url加载的Javascript读取本地文件,默认值 false
webSettings.setAllowUniversalAccessFromFileURLs(boolean flag):是否允许通过file url加载的Javascript读取全部资源(包括文件,http,https),默认值 false
webSettings.setJavaScriptCanOpenWindowsAutomatically(boolean allow); //是否可用Javascript(window.open)打开新窗口,默认值 false
webSettings.setLoadsImagesAutomatically(boolean allow); //是否自动加载图片
webSettings.setBlockNetworkImage(boolean flag):禁止加载网络图片
webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式
webSettings.setDefaultFontSize(int size):设置默认字体大小
webSettings.setDefaultFixedFontSize(int size):默认等宽字体尺寸
webSettings.setMinimumFontSize(int size):最小文字尺寸,默认值 8
webSettings.setMinimumLogicalFontSize(int size):最小文字逻辑尺寸,默认值 8
webSettings.setTextZoom(int textZoom):文字缩放百分比,默认值 100
webSettings.setStandardFontFamily(String font):标准字体,默认值 "sans-serif"
webSettings.setSerifFontFamily(String font):衬线字体,默认值 "serif"
webSettings.setSansSerifFontFamily(String font):无衬线字体,默认值 "sans-serif"
webSettings.setFixedFontFamily(String font):等宽字体,默认值 "monospace"
webSettings.setCursiveFontFamily(String font):手写体,默认值 "cursive"
webSettings.setFantasyFontFamily(String font):幻想体,默认值 "fantasy"
webSettings.setMediaPlaybackRequiresUserGesture(boolean require):用户是否需要通过手势播放媒体(不会自动播放),默认值 true
webSettings.setBlockNetworkLoads(boolean flag):禁止加载所有网络资源
webSettings.setDomStorageEnabled(boolean allow):启用HTML5 DOM storage API,默认值 false
webSettings.setDatabaseEnabled(boolean allow):启用Web SQL Database API,这个设置会影响同一进程内的所有WebView,默认值 false,此API已不推荐使用
webSettings.setAppCachePath(String appCachePath):设值缓存路径
webSettings.setAppCacheEnabled(boolean flag):启用Application Caches API,必需设置有效的缓存路径才能生效,默认值 false,此API已废弃
webSettings.setGeolocationEnabled(boolean flag):启用定位
webSettings.setSaveFormData(boolean save):是否保存表单数据
webSettings.setNeedInitialFocus(boolean flag):是否当webview调用requestFocus时为页面的某个元素设置焦点,默认值 true
webSettings.setUseWideViewPort(boolean use):是否支持viewport属性,默认值 false, 页面通过<meta name="viewport" ... />自适应手机屏幕
webSettings.setLoadWithOverviewMode(boolean overview):是否使用overview mode加载页面,默认值 false,当页面宽度大于WebView宽度时,缩小使页面宽度等于WebView宽度
webSettings.setLayoutAlgorithm(LayoutAlgorithm l):布局算法,默认是LayoutAlgorithm#NARROW_COLUMNS
webSettings.setSupportMultipleWindows(boolean support):是否支持多窗口,默认值false
webSettings.setAllowContentAccess(boolean allow):/是否可访问Content Provider的资源,默认值 true
webSettings.setOffscreenPreRaster(boolean enabled):是否在离开屏幕时光栅化(会增加内存消耗),默认值 false

常见用法:设置WebView缓存实现离线加载

if (NetStatusUtil.isConnected(getApplicationContext())) {
    webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//根据cache-control决定是否从网络上取数据。
} else {
    webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//没网,则从本地获取,即离线加载
}

webSettings.setDomStorageEnabled(true); // 开启 DOM storage API 功能
webSettings.setDatabaseEnabled(true);   //开启 database storage API 功能
webSettings.setAppCacheEnabled(true);//开启 Application Caches 功能

String cacheDirPath = getFilesDir().getAbsolutePath() + APP_CACAHE_DIRNAME;
webSettings.setAppCachePath(cacheDirPath); //设置  Application Caches 缓存目录

注意: 每个 Application 只调用一次 WebSettings.setAppCachePath(),WebSettings.setAppCacheMaxSize()

WebViewClient类

这个类就像WebView的委托人一样,是帮助WebView处理各种通知和请求事件的
常见方法1:shouldOverrideUrlLoading()
打开网页时不调用系统浏览器, 而是在本WebView中显示;在网页上的所有加载都经过这个方法,这个函数我们可以做很多操作。返回true表示宿主app拦截并处理了该url,否则返回false由当前WebView处理,不处理POST请求

  myWebView .setWebViewClient(new WebViewClient(){
      @Override
      public boolean shouldOverrideUrlLoading(WebView view, String url) {
          view.loadUrl(url);
      return true;
      }
  });

此方法在API24被废弃,API24添加如下方法:

  • public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
    拦截页面加载,返回true表示宿主app拦截并处理了该url,否则返回false由当前WebView处理
    不处理POST请求,可拦截处理子frame的非http请求

常见方法2:onPageStarted()
开始载入页面时调用

我们可以设定一个loading的页面,告诉用户程序在等待网络响应。

  myWebView.setWebViewClient(new WebViewClient(){
     @Override
     public void  onPageStarted(WebView view, String url, Bitmap favicon) {
        //设定加载开始的操作
     }
 });

常见方法3:onPageFinished()
在页面加载结束时调用

我们可以关闭loading 条,切换程序动作。

 myWebView.setWebViewClient(new WebViewClient(){
      @Override
      public void onPageFinished(WebView view, String url) {
         //设定加载结束的操作
      }
  });

常见方法4:onLoadResource()
在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次。

  myWebView.setWebViewClient(new WebViewClient(){
      @Override
      public boolean onLoadResource(WebView view, String url) {
         //设定加载资源的操作
      }
  });

常见方法5:onReceivedError()
加载页面的服务器出现错误时(如404)调用。

App里面使用webview控件的时候遇到了诸如404这类的错误的时候,若也显示浏览器里面的那种错误提示页面就显得很丑陋了,那么这个时候我们的app就需要加载一个本地的错误提示页面,即webview如何加载一个本地的页面

//步骤1:写一个html文件(error_handle.html),用于出错时展示给用户看的提示页面
//步骤2:将该html文件放置到代码根目录的assets文件夹下

//步骤3:复写WebViewClient的onRecievedError方法
//该方法传回了错误码,根据错误类型可以进行不同的错误分类处理
myWebView.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;
                }
            }
        });

此方法废弃于API23,并添加如下方法:

  • public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
    加载资源时出错,通常意味着连接不到服务器。由于所有资源加载错误都会调用此方法,所以此方法应尽量逻辑简单

常见方法6:onReceivedSslError()
处理https请求,加载资源时发生了一个SSL错误,应用必需响应(继续请求或取消请求), 处理决策可能被缓存用于后续的请求,默认行为是取消请求

webView默认是不处理https请求的,页面显示空白,需要进行如下设置:

myWebView.setWebViewClient(new WebViewClient() {
            @Override
            public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
                handler.proceed();    //表示等待证书响应
                // handler.cancel();      //表示挂起连接,为默认方式
                // handler.handleMessage(null);    //可做其他处理
            }
        });

        // 特别注意:5.1以上默认禁止了https和http混用,以下方式是开启
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            myWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
        }

其他方法

  • public WebResourceResponse shouldInterceptRequest(WebView view, String url)
    调用于非UI线程拦截资源请求并返回响应数据,返回null时WebView将继续加载资源。注意:API21以下的AJAX请求会走onLoadResource,无法通过此方法拦截,此方法废弃于API21
  • public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)
    此方法添加于API21,调用于非UI线程,拦截资源请求并返回数据,返回null时WebView将继续加载资源
  • public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse)
    此方法添加于API23, 在加载资源(iframe,image,js,css,ajax...)时收到了 HTTP 错误(状态码>=400)
  • public void onFormResubmission(WebView view, Message dontResend, Message resend)
    是否重新提交表单,默认不重发
  • public void onReceivedClientCertRequest(WebView view, ClientCertRequest request)
    此方法添加于API21,在UI线程被调用, 处理SSL客户端证书请求,必要的话可显示一个UI来提供KEY。
    有三种响应方式:proceed()/cancel()/ignore(),默认行为是取消请求,如果调用proceed()或cancel(),Webview 将在内存中保存响应结果且对相同的"host:port"不会再次调用 onReceivedClientCertRequest
    多数情况下,可通过KeyChain.choosePrivateKeyAlias启动一个Activity供用户选择合适的私钥
  • public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)
    处理HTTP认证请求,默认行为是取消请求
  • public void onReceivedLoginRequest(WebView view, String realm, String account, String args)
    通知应用有个已授权账号自动登陆了
  • public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event)
    给应用一个机会处理按键事件,如果返回true,WebView不处理该事件,否则WebView会一直处理,默认返回false
  • public void onScaleChanged(WebView view, float oldScale, float newScale)
    通知应用页面缩放系数变化

一些关键方法调用流程:

  • loadUrl()无重定向时

onPageStarted()->onPageFinished()

  • loadUrl()网页A重定向到B时

onPageStarted()->shouldOverrideUrlLoading()->onPageStarted()->onPageFinished()->onPageFinished()

  • 在已加载的页面中点击链接,加载页面A(无重定向)

shouldOverrideUrlLoading()->onPageStarted()->onPageFinished()

  • 在已加载的页面中点击链接,加载页面A(页面A重定向至页面B)

shouldOverrideUrlLoading()->onPageStarted()->shouldOverrideUrlLoading()->onPageStarted()->onPageFinished()->onPageFinished()

  • 执行goBack/goForward/reload方法

onPageStarted()->onPageFinished()

  • 发生资源加载

shouldInterceptRequest->onLoadResource

WebChromeClient类

辅助 WebView 处理 Javascript 的对话框,网站图标,网站标题等等。
常见方法1: onProgressChanged()
获得网页的加载进度并显示

myWebView.setWebChromeClient(new WebChromeClient(){
      @Override
      public void onProgressChanged(WebView view, int newProgress) {
          if (newProgress < 100) {
              String progress = newProgress + "%";
              progress.setText(progress);
            } else {
        }
    });

常见方法2: onReceivedTitle()
获取Web页中的标题

每个网页的页面都有一个标题,比如www.baidu.com这个页面的标题即“百度一下,你就知道”,那么如何知道当前webview正在加载的页面的title并进行设置呢?

myWebView.setWebChromeClient(new WebChromeClient(){
    @Override
    public void onReceivedTitle(WebView view, String title) {
       titleview.setText(title);
    }

常见方法3: onJsAlert()
支持javascript的警告框

myWebView.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle("JsAlert")
                        .setMessage(message)
                        .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                result.confirm();
                            }
                        })
                        .setCancelable(false)
                        .show();
                return true;
            }
        });

常见方法4: onJsConfirm()
支持javascript的确认框

myWebView.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle("JsConfirm")
                        .setMessage(message)
                        .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                result.confirm();
                            }
                        })
                        .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                result.cancel();
                            }
                        })
                        .setCancelable(false)
                        .show();
                // 返回布尔值:判断点击时确认还是取消
                // true表示点击了确认;false表示点击了取消;
                return true;
            }
        });

常见方法5: onJsPrompt()
支持javascript输入框

点击确认返回输入框中的值,点击取消返回 null。

myWebView.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
                final EditText et = new EditText(MainActivity.this);
                et.setText(defaultValue);
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle(message)
                        .setView(et)
                        .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                result.confirm(et.getText().toString());
                            }
                        })
                        .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                result.cancel();
                            }
                        })
                        .setCancelable(false)
                        .show();

                return true;
            }
        });

其他方法

  • public void getVisitedHistory(ValueCallback<String[]> callback)
    获得所有访问历史项目的列表,用于链接着色。
  • public Bitmap getDefaultVideoPoster()
    <video /> 控件在未播放时,会展示为一张海报图,HTML中可通过它的'poster'属性来指定。
    如果未指定'poster'属性,则通过此方法提供一个默认的海报图。
  • public View getVideoLoadingProgressView()
    当全屏的视频正在缓冲时,此方法返回一个占位视图(比如旋转的菊花)。
  • public void onShowCustomView(View view, CustomViewCallback callback)
    通知应用当前页进入了全屏模式,此时应用必须显示一个包含网页内容的自定义View
  • public void onHideCustomView()
    通知应用当前页退出了全屏模式,此时应用必须隐藏之前显示的自定义View
    -public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result)
    显示一个对话框让用户选择是否离开当前页面
  • public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback)
    指定源网页内容在没有设置权限状态下尝试使用地理位置API。
    从API24开始,此方法只为安全的源(https)调用,非安全的源会被自动拒绝
  • public void onGeolocationPermissionsHidePrompt()
    当前一个调用 onGeolocationPermissionsShowPrompt() 取消时,隐藏相关的UI。
  • public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg)
    通知应用打开新窗口
  • public void onCloseWindow(WebView window)
    通知应用关闭窗口
  • public void onRequestFocus(WebView view)
    请求获取取焦点
  • public void onPermissionRequest(PermissionRequest request)
    通知应用网页内容申请访问指定资源的权限(该权限未被授权或拒绝)
  • public void onPermissionRequestCanceled(PermissionRequest request)
    通知应用权限的申请被取消,隐藏相关的UI。
  • public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams)
    为'<input type="file" />'显示文件选择器,返回false使用默认处理
  • public boolean onConsoleMessage(ConsoleMessage consoleMessage)
    接收JavaScript控制台消息

另:页面加载方法调用流程

shouldOverrideUrlLoading()
onProgressChanged[10]
shouldInterceptRequest()
onProgressChanged[...]
onPageStarted()
onProgressChanged[...]
onLoadResource()
onProgressChanged[...]
onReceivedTitle()/onPageCommitVisible()
onProgressChanged[100]
onPageFinished()
onReceivedIcon()

总结

总结

如何避免WebView内存泄露?

  • 不在xml中定义 Webview ,而是在需要的时候在Activity中创建,并且Context使用 getApplicationgContext()
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);
mLayout.addView(mWebView);
  • 在 Activity 销毁( WebView )的时候,先让 WebView 加载null内容,然后移除 WebView,再销毁 WebView,最后置空。
    @Override
    protected void onDestroy() {
        if (mWebView != null) {
            mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
            mWebView.clearHistory();
            ((ViewGroup) mWebView.getParent()).removeView(mWebView);
            mWebView.destroy();
            mWebView = null;
        }
        super.onDestroy();
    }

如何避免WebView内存泄露?

实例

  • 目标:实现显示“www.baidu.com”、获取其标题、提示加载开始 & 结束和获取加载进度
  • 具体实现:
    步骤1:添加访问网络权限
<uses-permission android:name="android.permission.INTERNET"/>

步骤2:主布局
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"

    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.carson_ho.webview_demo.MainActivity">


   <!-- 获取网站的标题-->
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""/>

    <!--开始加载提示-->
    <TextView
        android:id="@+id/text_beginLoading"
        android:layout_below="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""/>

    <!--获取加载进度-->
    <TextView
        android:layout_below="@+id/text_beginLoading"
        android:id="@+id/text_Loading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""/>

    <!--结束加载提示-->
    <TextView
        android:layout_below="@+id/text_Loading"
        android:id="@+id/text_endLoading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""/>
    
    <!--显示网页区域-->
    <WebView
        android:id="@+id/webView1"
        android:layout_below="@+id/text_endLoading"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_marginTop="10dp" />
</RelativeLayout>

步骤3:根据需要实现的功能从而使用相应的子类及其方法(注释很清楚了)
MainActivity.java

public class MainActivity extends AppCompatActivity {
    WebView mWebview;
    WebSettings mWebSettings;
    TextView beginLoading, endLoading, loading, mtitle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebview = (WebView) findViewById(R.id.webView1);
        beginLoading = (TextView) findViewById(R.id.text_beginLoading);
        endLoading = (TextView) findViewById(R.id.text_endLoading);
        loading = (TextView) findViewById(R.id.text_Loading);
        mtitle = (TextView) findViewById(R.id.title);

        mWebSettings = mWebview.getSettings();

        mWebview.loadUrl("http://www.baidu.com/");

        //设置不用系统浏览器打开,直接显示在当前Webview
        mWebview.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }
        });

        //设置WebChromeClient类
        mWebview.setWebChromeClient(new WebChromeClient() {

            //获取网站标题
            @Override
            public void onReceivedTitle(WebView view, String title) {
                System.out.println("标题在这里");
                mtitle.setText(title);
            }

            //获取加载进度
            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                if (newProgress < 100) {
                    String progress = newProgress + "%";
                    loading.setText(progress);
                } else if (newProgress == 100) {
                    String progress = newProgress + "%";
                    loading.setText(progress);
                }
            }
        });

        //设置WebViewClient类
        mWebview.setWebViewClient(new WebViewClient() {
            //设置加载前的函数
            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                System.out.println("开始加载了");
                beginLoading.setText("开始加载了");
            }

            //设置结束加载函数
            @Override
            public void onPageFinished(WebView view, String url) {
                endLoading.setText("结束加载了");
            }
        });
    }

    //点击返回上一页面而不是退出浏览器
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && mWebview.canGoBack()) {
            mWebview.goBack();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    //销毁Webview
    @Override
    protected void onDestroy() {
        if (mWebview != null) {
            mWebview.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
            mWebview.clearHistory();

            ((ViewGroup) mWebview.getParent()).removeView(mWebview);
            mWebview.destroy();
            mWebview = null;
        }
        super.onDestroy();
    }
}

CookieManager

帮助webview管理Cookie的类
常用方法:

  • CookieManager getInstance():获得一个CookieManager实例
  • String getCookie(String url):根据url获取Cookie,以字符串形式返回Cookie
  • void setCookie(String url, String value):为url设置Cookie
  • void setCookie(String url, String value, ValueCallback<Boolean> callback):
    callback的onReceiveValue方法获取的参数如果是true,代表本次设置成功,否则代表设置失败。如果并不关心执行结果,为callback参数传入null即可
  • void setAcceptCookie(boolean accept):设置WebView是否允许使用Cookie,这个方法针对的是当前应用的所有WebView。
  • void setAcceptThirdPartyCookies(WebView webview, boolean accept):设置WebView是否允许设置第三方Cookie,Android 5.0(API 21)以下默认为true,Android 5.0及以上默认为false
  • void removeSessionCookies(ValueCallback<Boolean> callback):移除所有Session Cookies(异步执行),在执行完移除操作后,会回调onReceiveValue方法
  • void removeAllCookies(ValueCallback<Boolean> callback):移除所有Cookies(异步执行),在执行完移除操作后,会回调onReceiveValue方法
  • boolean hasCookies():判断是否存在Cookies

关于Cookie
Hybrid App(混合式应用)的开发过程中少不了与WebView的交互,在涉及到账户体系的产品中,包含了一种登录状态的传递,而Cookie作为网页身份信息的载体,便需要将其传递到Web里面,从而避免二次身份认证,这时就涉及到WebView加载网页时的Cookie操作了。通常我们在登录时获取到用户的Cookie信息,然后将其保存到sdcard的WebView缓存文件当中,然后再加载网页时,WebView会自动将当前url的本地Cookie信息放在http请求的request中,传递给服务器。

Cookie 默认保存位置:
data/data/package_name/app_WebView/Cookies.db

保存Cookie
我们通常使用安卓提供的CookieManager类中的setCookie(String url, String value)或void setCookie(String url, String value, ValueCallback<Boolean> callback)方法来对url的Cookie进行设置。方法作用见上文。另外API文档注释:

Sets a cookie for the given URL. Any existing cookie with the same host, path and name will be replaced with the new cookie. The cookie being set will be ignored if it is expired.

可见具有相同host和path的cookie会自动覆盖掉旧的cookie信息,当存储的cookie信息过期时会被自动忽略。
关于host(Domain)和path

  • host(Domain)表示cookie所在的域,默认为请求地址,如网址为www.test.com/test/test.aspx,那么domain默认为www.test.com。而跨域访问,如域A为t1.test.com,域B为t2.test.com,那么在域A生产一个令域A和域B都能访问的cookie就要将该cookie的domain设置为.test.com;如果要在域A生产一个令域A不能访问而域B能访问的cookie就要将该cookie的domain设置为t2.test.com
  • path表示cookie所在的目录,asp.net默认为/,就是根目录。在同一个服务器上有目录如下:/test/,/test/cd/,/test/dd/,现设一个cookie1的path为/test/,cookie2的path为/test/cd/,那么test下的所有页面都可以访问到cookie1,而/test/和/test/dd/的子页面不能访问cookie2。这是因为cookie只能让其path路径下的页面访问。因此一般如果访问一级域名,只需要将path设置为根目录就可以。

针对一级域名同步Cookie

  1. 针对项目使用的一级域名进行设置 Cookie
  2. 选择合适的时机进行刷新
    /**
     * 同步cookie
     * @param context 上下文
     * @param url 一级域名
     * @param cookies 需要添加的Cookie值,以键值对的方式:key=value
     */
    public void synCookies(Context context,String url, List<HttpCookie> cookies) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            CookieSyncManager.createInstance(context);
        }
        CookieManager cookieManager = CookieManager.getInstance();
        cookieManager.setAcceptCookie(true);// 允许接受 Cookie
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            cookieManager.removeSessionCookie();// 移除
        } else {
            cookieManager.removeSessionCookies(null);// 移除
        }
        for (int i = 0; i < cookies.size(); i++) {
            HttpCookie cookie = cookies.get(i);
            String value = cookie.getName() + "=" + cookie.getValue();
            cookieManager.setCookie(url, value);
        }
        cookieManager.setCookie(url, "Domain="+url);
        cookieManager.setCookie(url, "Path=/");
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            CookieSyncManager.getInstance().sync();
        } else {
            cookieManager.flush();
        }
    }

这里简单说明:

  • 参数中的 URL 在使用过程中基本是域名。例如 https://www.baidu.com/ 就可以使 www.baidu.com
  • 注意使用 for 循环 进行 setCookie(String url, String value) 调用。使用分号手动拼接的 value 值会导致 Cookie 不能完整设置或者无效
  • 注意 value 的值是使用 key=value 的完整形式。文档提示:

the cookie as a string, using the format of the 'Set-Cookie' HTTP response header

  • CookieSyncManager 是个过时的类,Api21 中 WebView 可以自动同步,使用时可以添加版本判断
  • CookieSyncManager.getInstance().sync(); 方法的替代方法是 cookieManager.flush()
  • Cookie 同步方法要在 WebView 的 setting 设置完之后,WebView的loadUrl(url)之前调用,否则无效。

Cookie的过期机制
可以设置Cookie的生效时间字段名为: expires 或 max-age。

expires:过期的时间点
max-age:生效的持续时间,单位为秒。
  1. 若将Cookie的 max-age 设置为负数,或者 expires 字段设置为过期时间点,数据库更新后这条Cookie将从数据库中被删除。
  2. 如果将Cookie的 max-age 和 expires 字段设置为正常的过期日期,则到期后再数据库更新时会删除该条数据。

特殊场景介绍

1、视口(viewport)
视口是一个为网页提供绘图区域的矩形。
你可以指定数个视口属性,比如尺寸和初始缩放系数(initial scale)。其中最重要的是视口宽度,它定义了网页水平方向的可用像素总数(可用的CSS像素数)。
多数 Android 上的网页浏览器(包括 Chrome)设置默认视口为一个大尺寸(被称为"wide viewport mode",宽约 980px)。
也有许多浏览器默认会尽可能缩小以显示完整的视口宽度(被称为"overview mode")

// 是否支持viewport属性,默认值 false
// 页面通过`<meta name="viewport" ... />`自适应手机屏幕
// 当值为true且viewport标签不存在或未指定宽度时使用 wide viewport mode
settings.setUseWideViewPort(true);
// 是否使用overview mode加载页面,默认值 false
// 当页面宽度大于WebView宽度时,缩小使页面宽度等于WebView宽度
settings.setLoadWithOverviewMode(true);

viewport 语法

<meta name="viewport"
      content="
          height = [pixel_value | "device-height"] ,
          width = [pixel_value | "device-width"] ,
          initial-scale = float_value ,
          minimum-scale = float_value ,
          maximum-scale = float_value ,
          user-scalable = ["yes" | "no"]
          " />

通过WebView设置初始缩放(initial-scale)

// 设置初始缩放百分比
// 0表示依赖于setUseWideViewPort和setLoadWithOverviewMode
// 100表示不缩放
web.setInitialScale(0)

2、长按保存图片或者拨打电话
一般浏览器都有长按保存图片或者拨打图片的功能,实现这个功能和WebView.HitTestResult这个类有关,这个类会将你触摸网页的地方的类型和其他信息反馈给你。
WebView.HitTestResult的常用方法:

  • HitTestResult.getExtra():获取额外的信息。
  • HitTestResult.getType():获取所选中目标的类型,可以是如下类型。
WebView.HitTestResult.UNKNOWN_TYPE //未知类型  
WebView.HitTestResult.PHONE_TYPE //电话类型
WebView.HitTestResult.EMAIL_TYPE //电子邮件类型
WebView.HitTestResult.GEO_TYPE //地图类型
WebView.HitTestResult.SRC_ANCHOR_TYPE //超链接类型
WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE //带有链接的图片类型
WebView.HitTestResult.IMAGE_TYPE //单纯的图片类型
WebView.HitTestResult.EDIT_TEXT_TYPE //选中的文字类型

实现步骤:

  1. 给WebView设置长按监听事件;
  2. 获取WebView长按时的WebView.HitTestResult的事件类型,如果是图片,则做处理。
webView.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View view) {
        WebView.HitTestResult result = ((WebView) view).getHitTestResult();
        if(result != null){
            switch (result.getType()){
                case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
                    String imgUrl = result.getExtra();
                    ...
                    return true;
                ...
            }
        }
        return false;
    }
});

3、全屏(Fullscreen)

  • 当H5请求全屏时,会回调 WebChromeClient.onShowCustomView 方法
  • 当H5退出全屏时,会回调 WebChromeClient.onHideCustomView 方法
  1. manifest
    自己处理屏幕尺寸方向的变化(切换屏幕方向时不重建activity)
    WebView播放视频需要开启硬件加速
<activity
    android:name=".WebViewActivity"
    android:configChanges="orientation|screenSize"
    android:hardwareAccelerated="true"
    android:screenOrientation="portrait" />
  1. 处理全屏回调
CustomViewCallback mCallback;
View vCustom;

@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
    setFullscreen(true);
    vCustom = view;
    mCallback = callback;
    if (vCustom != null) {
        ViewGroup parent = (ViewGroup) vWeb.getParent();
        parent.addView(vCustom);
    }
}

@Override
public void onHideCustomView() {
    setFullscreen(false);
    if (vCustom != null) {
        ViewGroup parent = (ViewGroup) vWeb.getParent();
        parent.removeView(vCustom);
        vCustom = null;
    }
    if (mCallback != null) {
        mCallback.onCustomViewHidden();
        mCallback = null;
    } 
} 
  1. 设置全屏,切换屏幕方向
void setFullscreen(boolean fullscreen) { 
    if (fullscreen) {
        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        vToolbar.setVisibility(View.GONE);
        vWeb.setVisibility(View.GONE);
    } else {
        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
        vToolbar.setVisibility(View.VISIBLE);
        vWeb.setVisibility(View.VISIBLE);
    }
    if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
    } else {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    }
}

WebView与Javascript交互

JavaScript 和 Android 可以交互,我们可以在网页中随意调用本地的 Java 代码,实现了 WebView 和本地代码的交互。所以 WebView 的功能非常强大,我们直接在一个 WebView 中就几乎可以实现 Android 的所有功能,所以现在才会出现不少纯 HTML5 开发的 App,这往往适合创业型公司刚起步阶段。

Android与JS通过WebView互相调用方法,实际上是:

  • Android去调用JS的代码
  • JS去调用Android的代码

二者沟通的桥梁是WebView

对于Android调用JS代码的方法有2种:

  1. 通过WebView的loadUrl()
  2. 通过WebView的evaluateJavascript()

方式1:通过WebView的loadUrl()
同步调用,会导致JS页面刷新。

  • 实例介绍:点击Android按钮,即调用WebView JS(文本名为javascript)中callJS()
  • 具体实现:

步骤1:将需要调用的JS代码以.html格式放到src/main/assets文件夹里

为了方便展示,本文是采用Andorid调用本地JS代码说明;实际情况时,Android更多的是调用远程JS代码,即将加载的JS代码路径改成url即可

需要加载JS代码:javascript.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>webview</title>
    <script>
    <!--Android需要调用的方法-->
   function callJS(){
      alert("Android调用了JS的callJS()方法");
   }
    </script>
</head>
</html>

步骤2:在Android里通过WebView设置调用JS代码
Android代码:MainActivity.java

注释已经非常清楚了

public class MainActivity extends AppCompatActivity {
    Button mButton;
    WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = findViewById(R.id.btn);
        mWebView = findViewById(R.id.webview);
        WebSettings webSettings = mWebView.getSettings();
        // 设置与Js交互的权限
        webSettings.setJavaScriptEnabled(true);
        // 设置允许JS弹窗
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

        // 由于设置了弹窗检验调用结果,所以需要支持js对话框
        // webview只是载体,内容的渲染需要使用webviewChromClient类去实现
        // 通过设置WebChromeClient对象处理JavaScript的对话框
        //设置响应js 的Alert()函数
        mWebView.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
                AlertDialog.Builder b = new AlertDialog.Builder(MainActivity.this);
                b.setTitle("Alert");
                b.setMessage(message);
                b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        result.confirm();
                    }
                });
                b.setCancelable(false);
                b.create().show();
                return true;
            }
        });

        // 先载入JS代码
        // 格式规定为:file:///android_asset/文件名.html
        mWebView.loadUrl("file:///android_asset/javascript.html");


        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 通过Handler发送消息
                mWebView.post(new Runnable() {
                    @Override
                    public void run() {
                        // 注意调用的JS方法名要对应上
                        // 调用javascript的callJS()方法
                        mWebView.loadUrl("javascript:callJS()");
                    }
                });

            }
        });

    }

}

效果图

特别注意:JS代码调用一定要在 onPageFinished() 回调之后才能调用,否则不会调用。

onPageFinished()属于WebViewClient类的方法,主要在页面加载结束时调用

方式2:通过WebView的evaluateJavascript()
异步执行调用JS,并通过回调返回值,不会使JS页面刷新

优点:该方法比第一种方法效率更高、使用更简洁。
限制:API>=19

  • 具体实现:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
              @Override
              public void onReceiveValue(String value) {
                     //此处为 js 返回的结果
              }
    });
}

方式对比

方式对比

使用建议
两种方法混合使用,即Android 4.4以下使用方法1,Android 4.4以上方法2

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
               @Override
                public void onReceiveValue(String value) {
                       //此处为 js 返回的结果
                }
    });
}else {
      mWebView.loadUrl("javascript:callJS()");
}

JS通过WebView调用 Android 代码的方法有3种:

  1. 通过WebView的addJavascriptInterface()进行对象映射
  2. 通过 WebViewClient 的shouldOverrideUrlLoading ()方法回调拦截 url
  3. 通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt() 消息

方式1:通过 WebView的addJavascriptInterface()进行对象映射
步骤1:定义一个与JS对象映射关系的Android类
AndroidtoJs.java

注释已经非常清楚了

public class AndroidtoJs {
    // 定义JS需要调用的方法
    // 被JS调用的方法必须加入@JavascriptInterface注解
    @JavascriptInterface
    public void hello(String msg) {
        System.out.println("JS调用了Android的hello方法");
    }
}

步骤2:将需要调用的JS代码以.html格式放到src/main/assets文件夹里
需要加载JS代码:javascript.html

<!DOCTYPE html>
<html>
   <head>
      <meta charset="utf-8">
      <title>webview</title>  
      <script>
         function callAndroid(){
            <!-- 由于对象映射,所以调用test对象等于调用Android映射的对象-->
            test.hello("js调用了android中的hello方法");
         }
      </script>
   </head>
   <body>
      <!--点击按钮则调用callAndroid函数-->
      <button type="button" id="button1" onclick="callAndroid()"></button>
   </body>
</html>

步骤3:在Android里通过WebView设置Android类与JS代码的映射
Android代码:MainActivity.java

注释已经非常清楚了

public class MainActivity extends AppCompatActivity {

    WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebView = findViewById(R.id.webview);
        WebSettings webSettings = mWebView.getSettings();

        // 设置与Js交互的权限
        webSettings.setJavaScriptEnabled(true);
        // 通过addJavascriptInterface()将Java对象映射到JS对象
        //参数1:Javascript对象名
        //参数2:Java对象名
        mWebView.addJavascriptInterface(new AndroidtoJs(), "test");//AndroidtoJS类对象映射到js的test对象

        // 加载JS代码
        // 格式规定为:file:///android_asset/文件名.html
        mWebView.loadUrl("file:///android_asset/javascript.html");
    }
}

效果图

特点

  • 优点:使用简单
  • 缺点:存在严重的漏洞问题,后文会说明。

方式2:通过 WebViewClient 的方法shouldOverrideUrlLoading ()回调拦截 url

  • 具体原理:
  1. Android通过 WebViewClient 的回调方法shouldOverrideUrlLoading ()拦截 url
  2. 解析该 url 的协议
  3. 如果检测到是预先约定好的协议,就调用相应方法
  • 具体实现:

步骤1:在JS约定所需要的Url协议
JS代码:javascript.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Carson_Ho</title>
    <script>
         function callAndroid(){
            <!--约定的url协议为:js://webview?arg1=111&arg2=222-->
            document.location = "js://webview?arg1=111&arg2=222";
         }
      </script>
</head>
<!-- 点击按钮则调用callAndroid()方法  -->
<body>
<button type="button" id="button1" onclick="callAndroid()">点击调用Android代码</button>
</body>
</html>

当该JS通过Android的mWebView.loadUrl("file:///android_asset/javascript.html")加载后,就会回调shouldOverrideUrlLoading(),接下来继续看步骤2:
步骤2:在Android通过WebViewClient复写shouldOverrideUrlLoading ()
MainActivity.java

public class MainActivity extends AppCompatActivity {

    WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebView = findViewById(R.id.webview);
        WebSettings webSettings = mWebView.getSettings();
        // 设置与Js交互的权限
        webSettings.setJavaScriptEnabled(true);
        // 设置允许JS弹窗
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

        // 步骤1:加载JS代码
        // 格式规定为:file:///android_asset/文件名.html
        mWebView.loadUrl("file:///android_asset/javascript.html");

        // 复写WebViewClient类的shouldOverrideUrlLoading方法
        mWebView.setWebViewClient(new WebViewClient() {
                                      @Override
                                      public boolean shouldOverrideUrlLoading(WebView view, String url) {
                                          // 步骤2:根据协议的参数,判断是否是所需要的url
                                          // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
                                          //假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)
                                          Uri uri = Uri.parse(url);
                                          // 如果url的协议 = 预先约定的 js 协议
                                          // 就解析往下解析参数
                                          if (uri.getScheme().equals("js")) {
                                              // 如果 authority  = 预先约定协议里的 webview,即代表都符合约定的协议
                                              // 所以拦截url,下面JS开始调用Android需要的方法
                                              if (uri.getAuthority().equals("webview")) {
                                                  //  步骤3:
                                                  // 执行JS所需要调用的逻辑
                                                  System.out.println("js调用了Android的方法");
                                                  // 可以在协议上带有参数并传递到Android上
                                                  HashMap<String, String> params = new HashMap<>();
                                                  Set<String> collection = uri.getQueryParameterNames();
                                                  for (String s : collection) {
                                                      params.put(s, uri.getQueryParameter(s));
                                                  }
                                                  System.out.println(uri.getQuery());
                                                  System.out.println(params.toString());
                                              }
                                              return true;
                                          }
                                          return super.shouldOverrideUrlLoading(view, url);
                                      }
                                  }
        );
    }
}


效果图

特点

  • 优点:不存在方式1的漏洞;
  • 缺点:JS获取Android方法的返回值复杂。

如果JS想要得到Android方法的返回值,只能通过 WebView 的 loadUrl ()去执行 JS 方法把返回值传递回去,相关的代码如下:

// Android:MainActivity.java
mWebView.loadUrl("javascript:returnResult(" + result + ")");
// JS:javascript.html
function returnResult(result){
    alert("result is" + result);
}

方式3:通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt() 消息
在JS中,有三个常用的对话框方法:

JS对话框方法

  • 具体原理:Android通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调分别拦截JS对话框(即上述三个方法),得到他们的消息内容,然后解析即可。
  • 具体实现:

下面的例子将用拦截 JS的输入框(即prompt()方法)说明 :

常用的拦截是:拦截 JS的输入框(即prompt()方法),因为只有prompt()可以返回任意类型的值,操作最全面方便、更加灵活;而alert()对话框没有返回值;confirm()对话框只能返回两种状态(确定 / 取消)两个值

步骤1:加载JS代码,如下:
javascript.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>webview</title>
    <script>
    function clickprompt(){
        <!-- 调用prompt()-->
        var result=prompt("js://webview?arg1=111&arg2=222");
        alert("demo " + result);
    }
    </script>
</head>
<!-- 点击按钮则调用clickprompt()-->
<body>
<button type="button" id="button1" onclick="clickprompt()">点击调用Android代码</button>
</body>
</html>

当使用mWebView.loadUrl("file:///android_asset/javascript.html")加载了上述JS代码后,就会触发回调onJsPrompt()。

如果是拦截警告框(即alert()),则触发回调onJsAlert();
如果是拦截确认框(即confirm()),则触发回调onJsConfirm();

步骤2:在Android通过WebChromeClient复写onJsPrompt()

public class MainActivity extends AppCompatActivity {

    WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebView = findViewById(R.id.webview);

        WebSettings webSettings = mWebView.getSettings();
        // 设置与Js交互的权限
        webSettings.setJavaScriptEnabled(true);
        // 设置允许JS弹窗
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

        // 先加载JS代码
        // 格式规定为:file:///android_asset/文件名.html
        mWebView.loadUrl("file:///android_asset/javascript.html");

        mWebView.setWebChromeClient(new WebChromeClient() {
                                        // 拦截输入框(原理同方式2)
                                        // 参数message:代表promt()的内容(不是url)
                                        // 参数result:代表输入框的返回值
                                        @Override
                                        public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
                                            // 根据协议的参数,判断是否是所需要的url(原理同方式2)
                                            // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
                                            //假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)
                                            Uri uri = Uri.parse(message);
                                            // 如果url的协议 = 预先约定的 js 协议
                                            // 就解析往下解析参数
                                            if (uri.getScheme().equals("js")) {
                                                // 如果 authority  = 预先约定协议里的 webview,即代表都符合约定的协议
                                                // 所以拦截url,下面JS开始调用Android需要的方法
                                                if (uri.getAuthority().equals("webview")) {
                                                    // 执行JS所需要调用的逻辑
                                                    System.out.println("js调用了Android的方法");
                                                    // 可以在协议上带有参数并传递到Android上
                                                    HashMap<String, String> params = new HashMap<>();
                                                    Set<String> collection = uri.getQueryParameterNames();
                                                    for (String s : collection) {
                                                        params.put(s, uri.getQueryParameter(s));
                                                    }
                                                    System.out.println(uri.getQuery());
                                                    System.out.println(params.toString());
                                                    //参数result:代表消息框的返回值(输入值)
                                                    result.confirm("js调用了Android的方法成功啦");
                                                }
                                                return true;
                                            }
                                            return super.onJsPrompt(view, url, message, defaultValue, result);
                                        }

                                        // 通过alert()和confirm()拦截的原理相同,此处不作过多讲述
                                        // 拦截JS的警告框
                                        @Override
                                        public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
                                            return super.onJsAlert(view, url, message, result);
                                        }

                                        // 拦截JS的确认框
                                        @Override
                                        public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
                                            return super.onJsConfirm(view, url, message, result);
                                        }
                                    }
        );
    }
}


效果图

三种方式的对比 & 使用场景
三种方式的对比 & 使用场景

总结

Android与JS交互总结

WebView的使用漏洞

1. 类型

WebView中,主要漏洞有三类:

  • 任意代码执行漏洞
  • 密码明文存储漏洞
  • 域控制不严格漏洞

2. 具体分析

第一类:WebView 任意代码执行漏洞
出现该漏洞的原因有三个:

  • WebView 中 addJavascriptInterface() 接口
  • WebView 内置导出的 searchBoxJavaBridge_对象
  • WebView 内置导出的 accessibility 和 accessibilityTraversalObject 对象

原因一:addJavascriptInterface 接口引起远程代码执行漏洞
漏洞产生原因:
JS调用Android的其中一个方式是通过addJavascriptInterface接口进行对象映射:

// 参数1:Android的本地对象
// 参数2:JS的对象
// 通过对象映射将Android中的本地对象和JS中的对象进行关联,从而实现JS调用Android的对象和方法
 webView.addJavascriptInterface(new JSObject(), "myObj");

所以,漏洞产生原因是:当JS拿到Android这个对象后,就可以调用这个Android对象中所有的方法,包括系统类(java.lang.Runtime 类),从而进行任意代码执行。如可以执行命令获取本地设备的SD卡中的文件等信息从而造成信息泄露。

具体获取系统类的原理描述:(结合 Java 反射机制)

  1. Android中的对象有一公共的方法:getClass()
  2. 该方法可以获取到当前类的类型Class
  3. 该类有一关键的方法: Class.forName,
  4. 该方法可以加载一个类(可加载 java.lang.Runtime 类),而该类是可以执行本地命令的。

以下是攻击的Js核心代码:

function execute(cmdArgs)  
{  
    // 步骤1:遍历 window 对象
    // 目的是为了找到包含 getClass()的对象
    // 因为Android映射的JS对象也在window中,所以肯定会遍历到
    for (var obj in window) {  
        if ("getClass" in window[obj]) {  
            // 步骤2:利用反射调用forName()得到Runtime类对象
            alert(obj);          
            return  window[obj].getClass().forName("java.lang.Runtime")  
            // 步骤3:以后,就可以调用静态方法来执行一些命令,比如访问文件的命令
            getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);  
            // 从执行命令后返回的输入流中得到字符串,有很严重暴露隐私的危险。
            // 如执行完访问文件的命令之后,就可以得到文件名的信息了。
        }  
    }  
}   

当一些 APP 通过扫描二维码打开一个外部网页时,攻击者就可以执行这段 js 代码进行漏洞攻击。
在微信盛行、扫一扫行为普及的情况下,该漏洞的危险性非常大。
解决方案

  • Android 4.2版本之后
    Google 在Android 4.2 版本中规定对被调用的函数以 @JavascriptInterface进行注解从而避免漏洞攻击
  • Android 4.2版本之前
    在Android 4.2版本之前采用拦截prompt()进行漏洞修复。具体步骤如下:
  1. 继承 WebView ,重写 addJavascriptInterface 方法,然后在内部自己维护一个对象映射关系的 Map,将需要添加的 JS 接口放入该Map中;
  2. 每次当 WebView 加载页面前加载一段本地的 JS 代码,原理是:

让JS调用一Javascript方法:该方法是通过调用prompt()把JS中的信息(含特定标识,方法名称等)传递到Android端;
在Android的onJsPrompt()中 ,解析传递过来的信息,再通过反射机制调用Java对象的方法,这样实现安全的JS调用Android代码。
关于Android返回给JS的值:可通过prompt()把Java中方法的处理结果返回到Js中

具体需要加载的JS代码如下:

javascript:(function JsAddJavascriptInterface_(){  
// window.jsInterface 表示在window上声明了一个Js对象
// jsInterface = 注册的对象名
// 它注册了两个方法,onButtonClick(arg0)和onImageClick(arg0, arg1, arg2)
// 如果有返回值,就添加上return
    if (typeof(window.jsInterface)!='undefined') {      
        console.log('window.jsInterface_js_interface_name is exist!!');}   
    else {  
        window.jsInterface = {     
            // 声明方法形式:方法名: function(参数)
            onButtonClick:function(arg0) {   
            // prompt()返回约定的字符串
            // 该字符串可自己定义
            // 包含特定的标识符MyApp和 JSON 字符串(方法名,参数,对象名等)    
                return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onButtonClick',args:[arg0]}));  
            },  
            onImageClick:function(arg0,arg1,arg2) {   
                return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onImageClick',args:[arg0,arg1,arg2]}));  
            },  
        };  
    }  
}  
)()
// 当JS调用 onButtonClick() 或 onImageClick() 时,就会回调到Android中的 onJsPrompt ()
// 我们解析出方法名,参数,对象名
// 再通过反射机制调用Java对象的方法

关于该方法的其他细节

  1. 加载上述JS代码的时机
    由于当 WebView 跳转到下一个页面时,之前加载的 JS 可能已经失效,所以,通常需要在以下方法中加载 JS:
onLoadResource();
doUpdateVisitedHistory();
onPageStarted();
onPageFinished();
onReceivedTitle();
onProgressChanged();
  1. 需要过滤掉 Object 类的方法
    由于最终是通过反射得到Android指定对象的方法,所以同时也会得到基类的其他方法(最顶层的基类是 Object类),为了不把 getClass()等方法注入到 JS 中,我们需要把 Object 的共有方法过滤掉,需要过滤的方法列表如下:
getClass()
hashCode()
notify()
notifyAl()
equals()
toString()
wait()

原因二:searchBoxJavaBridge_接口引起远程代码执行漏洞
漏洞产生原因:
在Android 3.0以下,Android系统会默认通过searchBoxJavaBridge_的Js接口给 WebView 添加一个JS映射对象:searchBoxJavaBridge_对象,该接口可能被利用,实现远程任意代码。
解决方案
删除searchBoxJavaBridge_接口

// 通过调用该方法删除接口
removeJavascriptInterface();

原因三:accessibility和 accessibilityTraversal接口引起远程代码执行漏洞
问题分析与解决方案同上,这里不作过多阐述。
第二类:密码明文存储漏洞
漏洞原因:
WebView默认开启密码保存功能

mWebView.setSavePassword(true);

开启后,在用户输入密码时,会弹出提示框:询问用户是否保存密码;
如果选择”是”,密码会被明文保到 /data/data/com.package.name/databases/webview.db 中,这样就有被盗取密码的危险
解决方案
关闭密码保存提醒

WebSettings.setSavePassword(false) 

第三类:域控制不严格漏洞
漏洞原因:
先看Android里的* WebViewActivity.java*:

public class WebViewActivity extends Activity {
    private WebView webView;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_webview);
        webView = (WebView) findViewById(R.id.webView);

        //webView.getSettings().setAllowFileAccess(false);                    (1)
        //webView.getSettings().setAllowFileAccessFromFileURLs(true);         (2)
        //webView.getSettings().setAllowUniversalAccessFromFileURLs(true);    (3)
        Intent i = getIntent();
        String url = i.getData().toString(); //url = file:///data/local/tmp/attack.html 
        webView.loadUrl(url);
    }
 }
/**Mainifest.xml**/
// 将该 WebViewActivity 在Mainifest.xml设置exported属性
// 表示:当前Activity是否可以被另一个Application的组件启动
android:exported="true"

即 A 应用可以通过 B 应用导出的 Activity 让 B 应用加载一个恶意的 file 协议的 url,从而可以获取 B 应用的内部私有文件,从而带来数据泄露威胁

具体:当其他应用启动此 Activity 时, intent 中的 data 直接被当作 url 来加载(假定传进来的 url 为 file:///data/local/tmp/attack.html ),其他 APP 通过使用显式 ComponentName 或者其他类似方式就可以很轻松的启动该 WebViewActivity 并加载恶意url。
下面我们着重分析WebView中getSettings类的方法对 WebView 安全性的影响:

  • setAllowFileAccess()
  • setAllowFileAccessFromFileURLs()
  • setAllowUniversalAccessFromFileURLs()

1. setAllowFileAccess()

// 设置是否允许 WebView 使用 File 协议
// 默认设置为true,即允许在 File 域下执行任意 JavaScript 代码
webView.getSettings().setAllowFileAccess(true);     

使用 file 域加载的 js代码能够使用同源策略跨域访问,从而导致隐私信息泄露

  • 同源策略跨域访问:对私有目录文件进行访问
  • 针对 IM 类产品,泄露的是聊天信息、联系人等等
  • 针对浏览器类软件,泄露的是cookie 信息泄露。

如果不允许使用 file 协议,则不会存在上述的威胁;

webView.getSettings().setAllowFileAccess(false);     

但同时也限制了 WebView 的功能,使其不能加载本地的 html 文件,如下图:
移动版的 Chrome 默认禁止加载 file 协议的文件

效果图

解决方案

  • 对于不需要使用 file 协议的应用,禁用 file 协议;
setAllowFileAccess(false); 
  • 对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript。
setAllowFileAccess(true); 

// 禁止 file 协议加载 JavaScript
if (url.startsWith("file://") {
    setJavaScriptEnabled(false);
} else {
    setJavaScriptEnabled(true);
}

2. setAllowFileAccessFromFileURLs()

// 设置是否允许通过 file url 加载的 Js代码读取其他的本地文件
// 在Android 4.1前默认允许
// 在Android 4.1后默认禁止
webView.getSettings().setAllowFileAccessFromFileURLs(true);

当AllowFileAccessFromFileURLs()设置为 true 时,攻击者的JS代码为:

<script>
function loadXMLDoc()
{
    var arm = "file:///etc/hosts";
    var xmlhttp;
    if (window.XMLHttpRequest)
    {
        xmlhttp=new XMLHttpRequest();
    }
    xmlhttp.onreadystatechange=function()
    {
        //alert("status is"+xmlhttp.status);
        if (xmlhttp.readyState==4)
        {
              console.log(xmlhttp.responseText);
        }
    }
    xmlhttp.open("GET",arm);
    xmlhttp.send(null);
}
loadXMLDoc();
</script>

// 通过该代码可成功读取 /etc/hosts 的内容数据

解决方案
禁用 file url 加载的 Js代码读取其他的本地文件

setAllowFileAccessFromFileURLs(false);

当设置成为 false 时,上述JS的攻击代码执行会导致错误,表示浏览器禁止从 file url 中的 javascript 读取其它本地文件。
3. setAllowUniversalAccessFromFileURLs()

// 设置是否允许通过 file url 加载的 Javascript 可以访问其他的源(包括http、https等源)
// 在Android 4.1前默认允许(setAllowFileAccessFromFileURLs()不起作用)
// 在Android 4.1后默认禁止
webView.getSettings().setAllowUniversalAccessFromFileURLs(true);

当AllowFileAccessFromFileURLs()被设置成true时,攻击者的JS代码是:

// 通过该代码可成功读取 http://www.so.com 的内容
<script>
function loadXMLDoc()
{
    var arm = "http://www.so.com";
    var xmlhttp;
    if (window.XMLHttpRequest)
    {
        xmlhttp=new XMLHttpRequest();
    }
    xmlhttp.onreadystatechange=function()
    {
        //alert("status is"+xmlhttp.status);
        if (xmlhttp.readyState==4)
        {
             console.log(xmlhttp.responseText);
        }
    }
    xmlhttp.open("GET",arm);
    xmlhttp.send(null);
}
loadXMLDoc();
</script>

解决方案:设置setAllowUniversalAccessFromFileURLs(false);
4. setJavaScriptEnabled()
// 设置是否允许 WebView 使用 JavaScript(默认是不允许)
// 但很多应用(包括移动浏览器)为了让 WebView 执行 http 协议中的 JavaScript,都会主动设置为true,不区别对待是非常危险的。
webView.getSettings().setJavaScriptEnabled(true);
即使把setAllowFileAccessFromFileURLs()和setAllowUniversalAccessFromFileURLs()都设置为 false,通过 file URL 加载的 javascript 仍然有方法访问其他的本地文件:符号链接跨源攻击

前提是允许 file URL 执行 javascript,即webView.getSettings().setJavaScriptEnabled(true);

这一攻击能奏效的原因是:通过 javascript 的延时执行和将当前文件替换成指向其它文件的软链接就可以读取到被符号链接所指的文件。具体攻击步骤:

  1. 把恶意的 js 代码输出到攻击应用的目录下,随机命名为 xx.html,修改该目录的权限;
  2. 修改后休眠 1s,让文件操作完成;
  3. 完成后通过系统的 Chrome 应用去打开该 xx.html 文件
  4. 等待 4s 让 Chrome 加载完成该 html,最后将该 html 删除,并且使用 ln -s 命令为 Chrome 的 Cookie 文件创建软连接

注:在该命令执行前 xx.html 是不存在的;执行完这条命令之后,就生成了这个文件,并且将 Cookie 文件链接到了 xx.html 上。

于是就可通过链接来访问 Chrome 的 Cookie

Google 没有进行修复,只是让Chrome 最新版本默认禁用 file 协议,所以这一漏洞在最新版的 Chrome 中并不存在,但是,在日常大量使用 WebView 的App和浏览器,都有可能受到此漏洞的影响。通过利用此漏洞,容易出现数据泄露的危险

如果是 file 协议,禁用 javascript 可以很大程度上减小跨源漏洞对 WebView 的威胁。但并不能完全杜绝跨源文件泄露。

例:应用实现了下载功能,对于无法加载的页面,会自动下载到 sd 卡中;由于 sd 卡中的文件所有应用都可以访问,于是可以通过构造一个 file URL 指向被攻击应用的私有文件,然后用此 URL 启动被攻击应用的 WebActivity,这样由于该 WebActivity 无法加载该文件,就会将该文件下载到 sd 卡下面,然后就可以从 sd 卡上读取这个文件了

最终解决方案

  • 对于不需要使用 file 协议的应用,禁用 file 协议;
// 禁用 file 协议;
setAllowFileAccess(false); 
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
  • 对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript。
// 需要使用 file 协议
setAllowFileAccess(true); 
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
// 禁止 file 协议加载 JavaScript
if (url.startsWith("file://") {
    setJavaScriptEnabled(false);
} else {
    setJavaScriptEnabled(true);
}

WebView及前端H5缓存机制

前端H5的缓存机制

  • 定义
    缓存,即离线存储
  1. 这意味着 H5网页 加载后会存储在缓存区域,在无网络连接时也可访问
  2. WebView的本质 = 在 Android中嵌入 H5页面,所以,Android WebView自带的缓存机制其实就是 H5页面的缓存机制
  • 作用
  1. 离线浏览:用户可在没有网络连接时进行H5页面访问
  2. 提高页面加载速度 & 减少流量消耗:直接使用已缓存的资源,不需要重新加载
  • 具体应用
    此处讲解主要讲解 前端H5的缓存机制 的缓存机制 & 缓存模式 :
    a. 缓存机制:如何将加载过的网页数据保存到本地
    b. 缓存模式:加载网页时如何读取之前保存到本地的网页缓存

前者是保存,后者是读取,请注意区别

缓存机制

浏览器缓存机制
原理
根据 HTTP 协议头里的 Cache-Control(或 Expires)和 Last-Modified(或 Etag)等字段来控制文件缓存的机制,下面详细介绍Cache-Control、Expires、Last-Modified & Etag四个字段:

  • Cache-Control:用于控制文件在本地缓存有效时长

如服务器回包:Cache-Control:max-age=600,则表示文件在本地应该缓存,且有效时长是600秒(从发出请求算起)。在接下来600秒内,如果有请求这个资源,浏览器不会发出 HTTP 请求,而是直接使用本地缓存的文件。

  • Expires:与Cache-Control功能相同,即控制缓存的有效时间
  • Expires是 HTTP1.0 标准中的字段,Cache-Control 是 HTTP1.1 标准中新加的字段
  • 当这两个字段同时出现时,Cache-Control 优先级较高
  • Last-Modified:标识文件在服务器上的最新更新时间

下次请求时,如果文件缓存过期,浏览器通过 If-Modified-Since 字段带上这个时间,发送给服务器,由服务器比较时间戳来判断文件是否有修改。如果没有修改,服务器返回304告诉浏览器继续使用缓存;如果有修改,则返回200,同时返回最新的文件。

  • Etag:功能同Last-Modified ,即标识文件在服务器上的最新更新时间。不同的是,Etag 的取值是一个对文件进行标识的特征字串。
  • 在向服务器查询文件是否有更新时,浏览器通过If-None-Match 字段把特征字串发送给服务器,由服务器和文件最新特征字串进行匹配,来判断文件是否有更新:没有更新回包304,有更新回包200
  • Etag 和 Last-Modified 可根据需求使用一个或两个同时使用。两个同时使用时,只要满足基中一个条件,就认为文件没有更新。
    常见用法
  • Cache-Control与 Last-Modified 一起使用;
    -Expires与 Etag一起使用;
    即一个用于控制缓存有效时间,一个用于在缓存失效后,向服务查询是否有更新

特别注意:浏览器缓存机制 是 浏览器内核的机制,一般都是标准的实现,即Cache-Control、 Last-Modified 、 Expires、 Etag都是标准实现,你不需要操心

特点
优点:支持 Http协议层
不足:缓存文件需要首次加载后才会产生;浏览器缓存的存储空间有限,缓存有被清除的可能;缓存的文件没有校验。

对于解决以上问题,可以参考手 Q 的离线包

应用场景
静态资源文件的存储,如JS、CSS、字体、图片等。

Android Webview会将缓存的文件记录及文件内容会存在当前 app 的 data 目录中。

具体实现
浏览器缓存机制是浏览器内核的机制,一般都是标准的实现,Android WebView内置自动实现,即不需要设置即实现.

Android WebView自带的缓存机制有5种:

  1. Application Cache 缓存机制
  2. Dom Storage 缓存机制
  3. Web SQL Database 缓存机制
  4. Indexed Database 缓存机制
  5. File System 缓存机制(H5页面新加入的缓存机制,虽然Android WebView暂时不支持,但会进行简单介绍)

下面将详细介绍每种缓存机制。

1. Application Cache 缓存机制

原理

  • 以文件为单位进行缓存,且文件有一定更新机制(类似于浏览器缓存机制)
  • AppCache 原理有两个关键点:manifest 属性和 manifest 文件。
<!DOCTYPE html>
<html manifest="demo_html.appcache">
// HTML 在头中通过 manifest 属性引用 manifest 文件
// manifest 文件:就是上面以 appcache 结尾的文件,是一个普通文件文件,列出了需要缓存的文件
// 浏览器在首次加载 HTML 文件时,会解析 manifest 属性,并读取 manifest 文件,获取 Section:CACHE MANIFEST 下要缓存的文件列表,再对文件缓存
<body>
...
</body>
</html>

// 原理说明如下:
// AppCache 在首次加载生成后,也有更新机制。被缓存的文件如果要更新,需要更新 manifest 文件
// 因为浏览器在下次加载时,除了会默认使用缓存外,还会在后台检查 manifest 文件有没有修改
//发现有修改,就会重新获取 manifest 文件,对 Section:CACHE MANIFEST 下文件列表检查更新
// manifest 文件与缓存文件的检查更新也遵守浏览器缓存机制
// 如用户手动清了 AppCache 缓存,下次加载时,浏览器会重新生成缓存,也可算是一种缓存的更新
// AppCache 的缓存文件,与浏览器的缓存文件分开存储的,因为 AppCache 在本地有 5MB(分 HOST)的空间限制

特点
方便构建Web App的缓存

专门为 Web App离线使用而开发的缓存机制

应用场景
存储静态文件(如JS、CSS、字体文件),应用场景同浏览器缓存机制,但AppCache 是对 浏览器缓存机制 的补充,不是替代。
具体实现

        // 通过设置WebView的settings来实现
        WebSettings settings = getSettings();

         // 1. 设置缓存路径
        String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/";
        settings.setAppCachePath(cacheDirPath);
          // 2. 设置缓存大小
         settings.setAppCacheMaxSize(20*1024*1024);
         // 3. 开启Application Cache存储机制
        settings.setAppCacheEnabled(true);

特别注意:
每个 Application 只调用一次 WebSettings.setAppCachePath() 和WebSettings.setAppCacheMaxSize()

2.Dom Storage 缓存机制

原理
通过存储字符串的 Key - Value 对来提供.DOM Storage 分为 sessionStorage & localStorage; 二者使用方法基本相同,区别在于作用范围不同:

  • sessionStorage:具备临时性,即存储与页面相关的数据,它在页面关闭后无法使用
  • localStorage:具备持久性,即保存的数据在页面关闭后也可以使用。
    特点
  • 存储空间大( 5MB):存储空间对于不同浏览器不同,如Cookies 才 4KB
  • 存储安全、便捷: Dom Storage 存储的数据在本地,不需要经常和服务器进行交互,不像 Cookies每次请求一次页面,都会向服务器发送网络请求

应用场景
存储临时、简单的数据

  • 代替 **将 不需要让服务器知道的信息 存储到 cookies **的这种传统方法
  • Dom Storage 机制类似于 Android 的 SharedPreference机制
    具体实现
        // 通过设置 `WebView`的`Settings`类实现
        WebSettings settings = getSettings();
        // 开启DOM storage
        settings.setDomStorageEnabled(true);

3.Web SQL Database 缓存机制

原理
基于 SQL 的数据库存储机制
特点
充分利用数据库的优势,可方便对数据进行增加、删除、修改、查询
应用场景
存储适合数据库的结构化数据
具体实现

        // 通过设置WebView的settings实现
        WebSettings settings = getSettings();

        // 设置缓存路径
        String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/";
        settings.setDatabasePath(cacheDirPath);
        // 开启 数据库存储机制
        settings.setDatabaseEnabled(true);

特别说明
根据官方说明,Web SQL Database存储机制不再推荐使用(不再维护),取而代之的是 IndexedDB缓存机制,下面会详细介绍

4.IndexedDB 缓存机制

原理
属于 NoSQL 数据库,通过存储字符串的 Key - Value 对来提供,类似于 Dom Storage 存储机制 的key-value存储方式
特点

优点

应用场景
存储 复杂、数据量大的结构化数据
具体实现

        // 通过设置WebView的settings实现
        WebSettings settings = getSettings();
        // 只需设置支持JS就自动打开IndexedDB存储机制
        // Android 在4.4开始加入对 IndexedDB 的支持,只需打开允许 JS 执行的开关就好了。
        settings.setJavaScriptEnabled(true);

5.File System

原理

  • 为 H5页面的数据 提供一个虚拟的文件系统
  • 可进行文件(夹)的创建、读、写、删除、遍历等操作,就像 Native App 访问本地文件系统一样
  • 虚拟的文件系统是运行在沙盒中
  • 不同 WebApp 的虚拟文件系统是互相隔离的,虚拟文件系统与本地文件系统也是互相隔离的。
  • 虚拟文件系统提供了两种类型的存储空间:临时 & 持久性:
  • 临时的存储空间:由浏览器自动分配,但可能被浏览器回收
  • 持久性的存储空间:需要显式申请;自己管理(浏览器不会回收,也不会清除内容);存储空间大小通过配额管理,首次申请时会一个初始的配额,配额用完需要再次申请。
    特点
  • 可存储数据体积较大的二进制数据
  • 可预加载资源文件
  • 可直接编辑文件

应用场景
通过文件系统 管理数据
具体使用
由于 File System是 H5 新加入的缓存机制,所以Android WebView暂时不支持
缓存机制汇总

缓存机制汇总

使用建议

  • 综合上述缓存机制的分析,我们可以根据 需求场景的不同(缓存不同类型的数据场景) 从而选择不同的缓存机制(组合使用)
  • 以下是缓存机制的使用建议:


    使用建议

缓存模式

  • 定义
    缓存模式是一种 当加载H5网页时该如何读取之前保存到本地缓存从而进行使用 的方式,即告诉Android WebView 什么时候去读缓存,以哪种方式去读缓存
  • Android WebView 自带的缓存模式有4种:
      // 缓存模式说明: 
      // LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
      // LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.
      // LOAD_DEFAULT: (默认)根据cache-control决定是否从网络上取数据。
      // LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。
  • 具体使用
// 设置参数即可
WebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);

WebView 存在的性能问题

  • Android WebView 里 H5 页面加载速度慢
  • 耗费流量

下面会详细介绍。

H5页面加载速度慢

H5页面加载速度慢

下面会详细介绍:
渲染速度慢
前端H5页面渲染的速度取决于两个方面:

  • Js 解析效率:Js 本身的解析过程复杂、解析速度不快 & 前端页面涉及较多 JS 代码文件,所以叠加起来会导致 Js 解析效率非常低。
  • 手机硬件设备的性能:由于Android机型碎片化,这导致手机硬件设备的性能不可控,而大多数的Android手机硬件设备无法达到很好很好的硬件性能。

总结:上述两个原因 导致 H5页面的渲染速度慢。
页面资源加载缓慢
H5 页面从服务器获得,并存储在 Android手机内存里:

  • H5页面一般会比较多
  • 每加载一个 H5页面,都会产生较多网络请求:
    1.HTML 主 URL 自身的请求;
    2.HTML外部引用的JS、CSS、字体文件,图片也是一个独立的 HTTP 请求
    每一个请求都串行的,这么多请求串起来,这导致 H5页面资源加载缓慢

总结:H5页面加载速度慢的原因:渲染速度慢 & 页面资源加载缓慢导致。

耗费流量

  • 每次使用 H5页面时,用户都需要重新加载 Android WebView的H5 页面
  • 每加载一个 H5页面,都会产生较多网络请求(上面提到)
  • 每一个请求都串行的,这么多请求串起来,这导致消耗的流量也会越多

总结
综上所述,产生Android WebView性能问题主要原因是:

性能问题原因

上述问题导致了Android WebView的H5 页面体验 与 原生Native 存在较大差距。

解决方案

针对上述Android WebView的性能问题,结合刚刚分析的WebView和h5前端缓存机制延伸出3种优化思路供参考:

  • 利用WebView和前端H5的缓存机制(WebView 自带)
  • 资源预加载
  • 资源拦截
    第一种方式在上面已经提到,下面对后面两种方式进行详细介绍:

资源预加载

  • 定义
    提早加载将需使用的H5页面,即 提前构建缓存,使用时直接取过来用而不用在需要时才去加载。
  • 具体实现
  1. 预加载WebView对象
  2. 预加载H5资源

预加载WebView对象

  • 此处主要分为2方面:首次使用的WebView对象 & 后续使用的WebView对象
  • 具体如下图


    示意图

预加载H5资源

原理
在应用启动、初始化第一个WebView对象时,直接开始网络请求加载H5页面,后续需打开这些H5页面时就直接从该本地对象中获取。

事先加载常用的H5页面资源(加载后就有缓存了),此方法虽然不能减小WebView初始化时间,但数据请求和WebView初始化可以并行进行,总体的页面加载时间就缩短了。
具体实现
在Android 的BaseApplication里初始化一个WebView对象(用于加载常用的H5页面资源);当需使用这些页面时再从BaseApplication里取过来直接使用。

应用场景

对于Android WebView的首页建议使用这种方案,能有效提高首页加载的效率
自身构建缓存
为了有效解决 Android WebView 的性能问题,除了使用 Android WebView 自身的缓存机制,还可以自己针对某一需求场景构建缓存机制。
需求场景

示意图

实现步骤

  1. 事先将更新频率较低、常用 & 固定的H5静态资源 文件(如JS、CSS文件、图片等) 放到本地
  2. 拦截H5页面的资源网络请求 并进行检测
  3. 如果检测到本地具有相同的静态资源 就 直接从本地读取进行替换而不发送该资源的网络请求 到 服务器获取
    示意图

    具体实现
    重写WebViewClient 的 shouldInterceptRequest 方法,当向服务器访问这些静态资源时进行拦截,检测到是相同的资源则用本地资源代替
        // 假设现在需要拦截一个图片的资源并用本地资源进行替代
        mWebView.setWebViewClient(new WebViewClient() {
            // 重写 WebViewClient  的  shouldInterceptRequest ()
            // API 21 以下用shouldInterceptRequest(WebView view, String url)
            // API 21 以上用shouldInterceptRequest(WebView view, WebResourceRequest request)
            // 下面会详细说明

            // API 21 以下用shouldInterceptRequest(WebView view, String url)
            @Override
            public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
                // 步骤1:判断拦截资源的条件,即判断url里的图片资源的文件名
                // 假设网页里该图片资源的地址为:http://abc.com/imgage/logo.gif
                // 图片的资源文件名为:logo.gif
                if (url.contains("logo.gif")) {
                    // 步骤2:创建一个输入流
                    InputStream is = null;
                    try {
                        // 步骤3:获得需要替换的资源(存放在assets文件夹里)
                        // a. 先在app/src/main下创建一个assets文件夹
                        // b. 在assets文件夹里再创建一个images文件夹
                        // c. 在images文件夹放上需要替换的资源(此处替换的是abc.png图片)
                        is = getApplicationContext().getAssets().open("images/abc.png");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    // 步骤4:替换资源
                    // 参数1:http请求里该图片的Content-Type,此处图片为image/png
                    // 参数2:编码类型
                    // 参数3:存放着替换资源的输入流(上面创建的那个)
                    WebResourceResponse response = new WebResourceResponse("image/png",
                            "utf-8", is);
                    return response;
                }
                return super.shouldInterceptRequest(view, url);
            }

            // API 21 以上用shouldInterceptRequest(WebView view, WebResourceRequest request)
            @TargetApi(Build.VERSION_CODES.LOLLIPOP)
            @Override
            public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
                // 步骤1:判断拦截资源的条件,即判断url里的图片资源的文件名
                // 假设网页里该图片资源的地址为:http://abc.com/imgage/logo.gif
                // 图片的资源文件名为:logo.gif
                if (request.getUrl().toString().contains("logo.gif")) {
                    // 步骤2:创建一个输入流
                    InputStream is = null;
                    try {
                        // 步骤3:获得需要替换的资源(存放在assets文件夹里)
                        // a. 先在app/src/main下创建一个assets文件夹
                        // b. 在assets文件夹里再创建一个images文件夹
                        // c. 在images文件夹放上需要替换的资源(此处替换的是abc.png图片
                        is = getApplicationContext().getAssets().open("images/abc.png");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    // 步骤4:替换资源
                    // 参数1:http请求里该图片的Content-Type,此处图片为image/png
                    // 参数2:编码类型
                    // 参数3:存放着替换资源的输入流(上面创建的那个)
                    WebResourceResponse response = new WebResourceResponse("image/png",
                            "utf-8", is);
                    return response;
                }
                return super.shouldInterceptRequest(view, request);
            }

        });

另:

Android加载网页的选择有三种方案,分别是:

  • 使用Android系统自带的WebView
  • 使用腾讯X5内核的WebView
  • 使用基于chrome webkit的crosswalk WebView
    方案对比及建议如下:


    方案对比及建议

参考文章

Android开发:最全面、最易懂的Webview详解
最全面总结 Android WebView与 JS 的交互方式
手把手教你构建 Android WebView 的缓存机制 & 资源预加载方案
你不知道的 Android WebView 使用漏洞
android webView 内核对比

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容