前言

结合H5页面开发的App日渐多了起来,而WebView正是Html与Native的纽带,今天就借着一个新的项目需求顺便做一下WebView的知识总结,如有错漏,恳请大家指点指点。(项目需求:将适配好的网页打包成App,并能够调用系统摄像头进行二维码识别、拍照或是选择本地图片上传、获取用户位置等)


WebView 小科普

  • 官方文档
    Class Overview
    A View that displays web pages. This class is the basis upon which you can roll
    your own web browser or simply display some online content within your Activity.
    It uses the WebKit rendering engine to display web pages and includes methods
    to navigate forward and backward through a history, zoom in and out, perform text searches and more.
    理解:WebView是一个显示网页的一个View,基本应用于浏览器或是Activity中网页的简单显示。使用了WebKit渲染引擎实现一系列神奇的功能。
    Basic usage
    By default, a WebView provides no browser-like widgets, does not enable JavaScript and web page errors are ignored.
    理解:默认情况,WebView并没有开启对JavaScript的支持,仅起展示作用,因此,我们需要进一步配置WebView才能满足各种各样的需求。

WebView基本使用

  1. 在 AndroidManifest.xml 中添加联网权限(如果webView需要联网的话,仅加载本地html、js文件则不需要添加联网权限)
<uses-permission android:name="android.permission.INTERNET" />
<!--另外附上一些可能会用到的权限:-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--获取网络状态权限(情景:WebView联网前,应检查当前网络状态)-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!--通过WiFi或移动基站的方式获取用户错略的经纬度信息-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!--通过GPS芯片接收卫星的定位信息权限(情景:结合HTML5使用Geolocation API获取位置时)-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--读写存储权限(情景:拍照/选择图片,涉及图片读取、编辑、压缩等功能时)-->
  1. 一些 WebSettings 常用配置(其实还有许多没有列出来,大家可以利用IDE,点击进入方法,查看源码继续发掘)
WebSettings  mWebSetting = mWebView.getSettings();  //获取WebSetting
 setJavaScriptEnabled(true);//让WebView支持JavaScript
 setDomStorageEnabled(true);//启用H5 DOM API (默认false)
 setDatabaseEnabled(true);//启用数据库api(默认false)可结合 setDatabasePath 设置路径
 setCacheMode(WebSettings.LOAD_DEFAULT)//设置缓存模式
 setAppCacheEnabled(true);//启用应用缓存(默认false)可结合 setAppCachePath 设置缓存路径
 setAppCacheMaxSize()//已过时,高版本API上,系统会自行分配
 setPluginsEnabled(true);  //设置插件支持
 setRenderPriority(RenderPriority.HIGH);  //提高渲染的优先级
 setUseWideViewPort(true);  //将图片调整到适合webview的大小
 setLoadWithOverviewMode(true); // 缩放至屏幕的大小
 setSupportZoom(true);  //支持缩放,默认为true
 setBuiltInZoomControls(true); //设置内置的缩放控件(若SupportZoom为false,该设置项无效)
 setDisplayZoomControls(false); //隐藏原生的缩放控件
 setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN); //支持内容重新布局
 supportMultipleWindows();  //支持多窗口
 setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);  //关闭webview中缓存
 setAllowFileAccess(true);  //设置可以访问文件
 setNeedInitialFocus(true); //当webview调用requestFocus时为webview设置节点
 setJavaScriptCanOpenWindowsAutomatically(true); //支持通过JS打开新窗口
 setLoadsImagesAutomatically(true);  //自动加载图片
 setDefaultTextEncodingName("utf-8");//设置编码格式
//实际应用中,一般配置即可满足基本需求
 mWebSettings.setSupportZoom(true);
 mWebSettings.setJavaScriptEnabled(true);
 mWebSettings.setLoadWithOverviewMode(true);
 mWebSettings.setUseWideViewPort(true);
 mWebSettings.setDefaultTextEncodingName("utf-8");
 mWebSettings.setLoadsImagesAutomatically(true);
  1. 加载网页、本地、assets中的html页面基本方式
//网页
private static final String URL_NET = "http://www.google.com"; // 记得加 "http://"
//assets 中的 html 资源
private static final String URL_LOCAL ="file:///android_asset/xxx.html路径"; 
//SD 卡中的 html 资源
private static final String URL_SD_CARD ="content://com.android.htmlfileprovider/mnt/sdcard/xxx.html"; 
mWebView.loadUrl(URL_NET);
mWebView.loadUrl(URL_LOCAL);
mWebView.loadUrl(URL_SD_CARD);

WebViewClient 与 WebChromeClient

WebViewClient与WebChromeClient的区别

  • WebViewClient 用于帮助WebView处理各种通知、请求事件

WebViewClient 常用方法

说明

shouldOverrideUrlLoading

加载时调用,可捕获url

onPageStart

开始加载时调用(可以设置加载中提示)

onPageFinish

加载完成时调用(无法打开也是完成的一种,在这里取消加载提示显示)

onReceiveError

接收到错误信息时调用(通常在该方法中处理404之类的加载错误,但这里有点坑,API23中,在低于API23的设备上运行时,该方法失效,不调用我猜原因可能是: 新版的onReceiveError不再接收网页连接的错误,而是接收WebView自身运行出现的错误,另外有onReceivedHttpError方法来接收(然而在我的测试中,该方法还是未能接收到404错误)。可暂时用API23过时的方法“onReceivedError(WebView view, int errorCode, String description, String failingUrl)”替代新版本中的“onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)”;)传送门:stackoverflow Q&A

  • WebChromeClient 用于辅助 WebView处理Javascript的对话框,网站图标、Title、位置、加载进度等信息

WebChromeClient 常用方法

说明

onProgressChanged

加载进度

onReceivedTitle、onReceivedIcon

获取网页标题、图标

onGeolocationPermissionsShowPrompt

页面发起GEO定位请求时调用


WebChromeClient源码中对各种方法都有详细解析,需要用到的时候查一下即可

  • 两者用法都是简单的set方法
mWebView.setWebViewClient(mWebViewClient);
mWebView.setChromeClient(mWebChromeClient);

WebView与Javascript交互

  • 前提条件:setJavaScriptEnabled(true)
  • 调用js方法
    mWebView.loadUrl("javascript: 方法名('"+参数+"')");
  • js调用android中方法
    4.2及之前版本该方法存在漏洞:Android WebView的Js对象注入漏洞解决方案、JS与WebView交互存在的一些问题

引用”漏洞描述”:
1,WebView添加了JavaScript对象,并且当前应用具有读写SDCard的权限,也就是:android.permission.WRITE_EXTERNAL_STORAGE
2,JS中可以遍历window对象,找到存在“getClass”方法的对象的对象,然后再通过反射的机制,得到Runtime对象,然后调用静态方法来执行一些命令,比如访问文件的命令.
3,再从执行命令后返回的输入流中得到字符串,就可以得到文件名的信息了。然后想干什么就干什么,好危险。

/**
 * This method can be used to allow JavaScript to control the host
 * application. This is a powerful feature, but also presents a security
 * risk for apps targeting{@link android.os.Build.VERSION_CODES#JELLY_BEAN} 
 * or earlier.
 * /
 //理解:该方法可以让js控制app,很强势,但在API17(4.2)及之前的版本存在安全问题
 addJavascriptInterface(Object object, String name);

 //使用方法
 mWebView.addJavascriptInterface(MethodObject,"name");

 //还需要写一个方法类
 class MethodObject extends Object {
    //无参函数,js中通过:var str = window.name.HtmlcallJava(); 获取到
    @JavascriptInterface
    public String HtmlcallJava() {
        return "Html call Java";
    }

    //有参函数,js中通过:window.jsObj.HtmlcallJava2("IT-homer blog");
    @JavascriptInterface
    public String HtmlcallJava2(final String param) {
        return "Html call Java : " + param;
    }
}

WebView小技巧

/**
 * 捕获Url
 * 多页面在同一个WebView打开
 * /
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if(url.equal("需要捕获的url")){
                // 捕获后的操作
                // 前面提到的项目需求中,调用系统摄像头识别二维码、拍照、选择本地图片等功能
                // 均可该方式简单实现(当然addJavascriptInterface方式也能达到相同效果)
                return true;
            }
            view.loadUrl(url);//该方法可让多页面在同一个WebView中打开(不用新建Activity或是调用浏览器)
            return true;
        }
//我们再回头看看该方法的官方API,会发现上面的方法多少还是有点坑(重定向问题,出现原理)     
/**
     * Give the host application a chance to take over the control when a new
     * url is about to be loaded in the current WebView. If WebViewClient is not
     * provided, by default WebView will ask Activity Manager to choose the
     * proper handler for the url. If WebViewClient is provided, return true
     * means the host application handles the url, while return false means the
     * current WebView handles the url.
     * /

     //该方法为应用提供处理新url的机会,如果WebView没有设置WebViewClient,WebView会调用系统来找到合适应用来处理该url;而如果设置了WebViewClient,该方法返回true说明该url由应用自行处理,而false则交给WebView自动处理。

     //那么,问题来了:上面方法中,我们returne的是true,而处理代码是让WebView直接loadUrl(不管什么情况都是直接loadUrl,并把该url加入历史记录),如果该url会重定向到其他url,如果调用了goBack,返回到该url,而该url又重定向到另外一个url,造成goBack失败。

     //解决方式:将处理重定向的url交给webView本身,webView能自行判断url是否为重定向url,能够确保历史记录准确性,自身跳转则需要另想办法:
     //1. 与前端人员协商能够去掉重定向url? 
     //2. 建立自身的历史栈,舍弃goBack()方法,移除重定向url与重定向后的url,根据需求自行进行loadUrl(需要思考一个合理的跳转逻辑)
     //3. 建立自身的历史栈,与前端配合,提供js函数判断是否为重定向url,捕获url调用js函数,若为重定向url则作过滤处理,则不加入历史栈
public boolean shouldOverrideUrlLoading(WebView view, String url){
    return false;
}
/**
 * 返回上一浏览页面
 * /
public boolean onKeyDown(int keyCode, KeyEvent event) {       
    if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) {       
        mWebView.goBack();       
        return true;       
    }       
    return super.onKeyDown(keyCode, event);       
}
// WebClient 中 onReceivedError 的旧方法(API23 已过时)
 @Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
      showErrorTips();// 显示错误页面/提示(大家可以将 WebView 错误页面替换掉、结合 mWebView.reload 实现点击重连)
}

WebView 进阶

WebView 内存泄漏问题

WebView解析网页时会申请Native堆内存用于保存页面元素,当页面较复杂时会有很大的内存占用。如果页面包含图片,内存占用会更严重。并且打开新页面时,为了能快速回退,之前页面占用的内存也不会释放。有时浏览十几个网页,都会占用几百兆的内存。这样加载网页较多时,会导致系统不堪重负,最终强制关闭应用,也就是出现应用闪退或重启。

由于占用的都是Native堆内存,所以实际占用的内存大小不会显示在常用的DDMS Heap工具中(这里看到的只是Java虚拟机分配的内存,一般即使Native堆内存已经占用了几百兆,这里显示的还只是几兆或十几兆)。只有使用adb shell中的一些命令比如dumpsys meminfo 包名,或者在程序中使用Debug.getNativeHeapSize()才能看到。

据说由于WebView的一个BUG,即使它所在的Activity(或者Service)结束也就是onDestroy()之后,或者直接调用WebView.destroy()之后,它所占用这些内存也不会被释放。

解决这个问题最直接的方法是:把使用了WebView的Activity(或者Service)放在单独的进程里。然后在检测到应用占用内存过大有可能被系统干掉或者它所在的Activity(或者Service)结束后,调用System.exit(0),主动Kill掉进程。由于系统的内存分配是以进程为准的,进程关闭后,系统会自动回收所有内存。

WebView 缓存机制

推荐文章Android WebView缓存机制详解深入探究webView的缓存机制

经过一番搜索得来的结果:
1. WebView缓存分为:页面缓存和数据缓存。页面缓存指加载网页时,对页面或资源数据的缓存。一般使用RE管理器进入目录: “/data/data/(packageName)/cache/org.chromium.android_webview“可看到;
数据缓存又分为 AppCache 与 DOM Storage 。AppCache可以有选择地缓存我们所想要缓存的东西;DOM Storage 则是HTML5的一个缓存机制,常用于存储简单的表单数据,关于DOM Storage,详情可学习参考下 “浅谈HTML5 的DOM Storage机制” 一文。
2. webView的缓存模式:
LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
LOAD_DEFAULT: 根据cache-control决定是否从网络上取数据。
LOAD_CACHE_NORMAL: API level 17中已经废弃, 从API level 11开始作用同LOAD_DEFAULT模式
LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.
LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。
3. 将缓存路径转移到外置sd卡
查看 Context 类API 发现这样一个方法,重写该方法即可(注意赋予相关权限,但Android 4.4 上权限限制,会使该方法失效)
另外趁机附上:Android 外部存储权限分析(译)Android存储访问及目录

/**
     * Returns the absolute path to the application specific cache directory
     * on the filesystem. These files will be ones that get deleted first when the
     * device runs low on storage.
     * There is no guarantee when these files will be deleted.
     *
     * <strong>Note: you should not <em>rely</em> on the system deleting these
     * files for you; you should always have a reasonable maximum, such as 1 MB,
     * for the amount of space you consume with cache files, and prune those
     * files when exceeding that space.</strong>
     *
     * @return The path of the directory holding application cache files.
     *
     * @see #openFileOutput
     * @see #getFileStreamPath
     * @see #getDir
     */
        public abstract File getCacheDir();
File extStorageAppCachePath = null;
    public File getCacheDir() {
        //读写权限判断
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            //获取外置存储路径,生成相应缓存路径
            File externalStorageDir = Environment.getExternalStorageDirectory();
            if (externalStorageDir != null) {
                extStorageAppCachePath = new File(externalStorageDir.getAbsolutePath() +
                        File.separator + "Android" + File.separator + "data" + File.separator + getPackageName() + File.separator + "webViewCache");
                //路径不存在,创建
                if (!extStorageAppCachePath.exists()) {
                    if (!extStorageAppCachePath.mkdirs()) {
                        //创建路径失败
                        extStorageAppCachePath = null;
                        return super.getCacheDir();
                    } else {
                        //成功创建,返回路径
                        if (extStorageAppCachePath != null) {
                            return extStorageAppCachePath;
                        }
                    }
                } else {
                    //路径已存在,直接返回
                    if (extStorageAppCachePath != null) {
                        return extStorageAppCachePath;
                    }
                }
            }
        }
        return super.getCacheDir();
    }

结束语

本文主要是对自己学习WebView的过程、应用WebView遇到的一些问题,结合强大的网络资源总结而来,如果错漏,恳请指教,希望能给大家提供小小的帮助,在分享技术过程中,提升、成长!(有很长一段时间没有发布过博客,贵在坚持,贵在坚持!