目录介绍

  • 01.基础使用目录介绍

    • 1.0.1 常用的基础介绍
    • 1.0.2 Android调用Js
    • 1.0.3 Js调用Android
    • 1.0.4 WebView.loadUrl(url)流程
    • 1.0.5 js的调用时机分析
    • 1.0.6 清除缓存数据方式有哪些
    • 1.0.7 如何使用DeepLink
    • 1.0.8 应用被作为第三方浏览器打开
  • 02.优化汇总目录介绍
    • 2.0.1 视频全屏播放按返回页面被放大
    • 2.0.2 加快加载webView中的图片资源
    • 2.0.3 自定义加载异常error的状态页面
    • 2.0.4 WebView硬件加速导致页面渲染闪烁
    • 2.0.5 WebView加载证书错误
    • 2.0.6 web音频播放销毁后还有声音
    • 2.0.7 DNS采用和客户端API相同的域名
    • 2.0.8 如何设置白名单操作
    • 2.0.9 后台无法释放js导致发热耗电
    • 2.1.0 可以提前显示加载进度条
    • 2.1.1 WebView密码明文存储漏洞优化
  • 03.问题汇总目录介绍
    • 3.0.0 WebView进化史介绍
    • 3.0.1 提前初始化WebView必要性
    • 3.0.2 x5加载office资源
    • 3.0.3 WebView播放视频问题
    • 3.0.4 无法获取webView的正确高度
    • 3.0.5 使用scheme协议打开链接风险
    • 3.0.6 如何处理加载错误
    • 3.0.7 webView防止内存泄漏
    • 3.0.8 关于js注入时机修改
    • 3.0.9 视频/图片宽度超过屏幕
    • 3.1.0 如何保证js安全性
    • 3.1.1 如何代码开启硬件加速
    • 3.1.2 WebView设置Cookie
    • 3.1.4 webView加载网页不显示图片
    • 3.1.5 绕过证书校验漏洞
    • 3.1.6 allowFileAccess漏洞
    • 3.1.7 WebView嵌套ScrollView问题
    • 3.1.8 WebView中图片点击放大
    • 3.1.9 页面滑动期间不渲染/执行
    • 3.2.0 被运营商劫持和注入问题
    • 3.2.1 解决资源加载缓慢问题
    • 3.2.2 判断是否已经滚动到页面底端
    • 3.2.3 使用loadData加载html乱码
    • 3.2.4 WebView下载进度无法监听
    • 3.2.5 webView出现302/303重定向

x5封装库YCWebView开源项目地址

  • https://github.com/yangchong211/YCWebView
  • 该后续知识点,几乎包含了实际开发中绝大多数的问题,再次学习和巩固webView,希望这篇文章对你有用……更多内容,可以看我的开源项目,如果觉得给你带来一些收获,麻烦star一下,这也可以增加开发者开源项目的动力!

01.基础使用目录介绍

1.0.1 常用的基础介绍

  • 在activity中最简单的使用

    webview.loadUrl("http://www.baidu.com/");                    //加载web资源
    //webView.loadUrl("file:///android_asset/example.html");       //加载本地资源
    //这个时候发现一个问题,启动应用后,自动的打开了系统内置的浏览器,解决这个问题需要为webview设置 WebViewClient,并重写方法:
    webview.setWebViewClient(new WebViewClient(){@Overridepublic boolean shouldOverrideUrlLoading(WebView view, String url) {view.loadUrl(url);//返回值是true的时候控制去WebView打开,为false调用系统浏览器或第三方浏览器return true;}//还可以重写其他的方法
    });
    
  • 那些因素影响页面加载速度
    • 影响页面加载速度的因素有非常多,在对 WebView 加载一个网页的过程进行调试发现

      • 每次加载的过程中都会有较多的网络请求,除了 web 页面自身的 URL 请求
      • 有 web 页面外部引用的JS、CSS、字体、图片等等都是个独立的http请求。这些请求都是串行的,这些请求加上浏览器的解析、渲染时间就会导致 WebView 整体加载时间变长,消耗的流量也对应的真多。

1.0.2 Android调用Js

  • 第一种方式:native 调用 js 的方法,方法为:

    • 注意的是名字一定要对应上,要不然是调用不成功的,而且还有一点是 JS 的调用一定要在 onPageFinished 函数回调之后才能调用,要不然也是会失败的。
    //java
    //调用无参方法
    mWebView.loadUrl("javascript:callByAndroid()");
    //调用有参方法
    mWebView.loadUrl("javascript:showData(" + result + ")");//javascript,下面是对应的js代码
    <script type="text/javascript">function showData(result){alert("result"=result);return "success";
    }function callByAndroid(){console.log("callByAndroid")showElement("Js:无参方法callByAndroid被调用");
    }
    </script>
    
  • 第二种方式:
    • 如果现在有需求,我们要得到一个 Native 调用 Web 的回调怎么办,Google 在 Android4.4 为我们新增加了一个新方法,这个方法比 loadUrl 方法更加方便简洁,而且比 loadUrl 效率更高,因为 loadUrl 的执行会造成页面刷新一次,这个方法不会,因为这个方法是在 4.4 版本才引入的,所以使用的时候需要添加版本的判断:
    if (Build.VERSION.SDK_INT < 18) {mWebView.loadUrl(jsStr);
    } else {mWebView.evaluateJavascript(jsStr, new ValueCallback<String>() {@Overridepublic void onReceiveValue(String value) {//此处为 js 返回的结果}});
    }
    
  • 两种方式的对比
    • 一般最常使用的就是第一种方法,但是第一种方法获取返回的值比较麻烦,而第二种方法由于是在 4.4 版本引入的,所以局限性比较大。
  • 注意问题
    • 记得添加ws.setJavaScriptEnabled(true)代码

1.0.3 Js调用Android

  • 第一种方式:通过 addJavascriptInterface 方法进行添加对象映射

    • 这种是使用最多的方式了,首先第一步我们需要设置一个属性:
    mWebView.getSettings().setJavaScriptEnabled(true);
    
    • 这个函数会有一个警告,因为在特定的版本之下会有非常危险的漏洞,设置完这个属性之后,Native需要定义一个类:

      • 在 API17 版本之后,需要在被调用的地方加上 @addJavascriptInterface 约束注解,因为不加上注解的方法是没有办法被调用的
    public class JSObject {private Context mContext;public JSObject(Context context) {mContext = context;}@JavascriptInterfacepublic String showToast(String text) {Toast.show(mContext, text, Toast.LENGTH_SHORT).show();return "success";}/*** 前端代码嵌入js:* imageClick 名应和js函数方法名一致** @param src 图片的链接*/@JavascriptInterfacepublic void imageClick(String src) {Log.e("imageClick", "----点击了图片");}/*** 网页使用的js,方法无参数*/@JavascriptInterfacepublic void startFunction() {Log.e("startFunction", "----无参");}
    }//特定版本下会存在漏洞
    mWebView.addJavascriptInterface(new JSObject(this), "yc逗比");
    
    • JS 代码调用

      • 这种方式的好处在于使用简单明了,本地和 JS 的约定也很简单,就是对象名称和方法名称约定好即可,缺点就是要提到的漏洞问题。
    function showToast(){var result = myObj.showToast("我是来自web的Toast");
    }function showToast(){myObj.imageClick("图片");
    }function showToast(){myObj.startFunction();
    }
    
  • 第二种方式:利用 WebViewClient 接口回调方法拦截 url
    • 这种方式其实实现也很简单,使用的频次也很高,上面介绍到了 WebViewClient ,其中有个回调接口 shouldOverrideUrlLoading (WebView view, String url)) ,就是利用这个拦截 url,然后解析这个 url 的协议,如果发现是我们预先约定好的协议就开始解析参数,执行相应的逻辑。注意这个方法在 API24 版本已经废弃了,需要使用 shouldOverrideUrlLoading (WebView view, WebResourceRequest request)) 替代,使用方法很类似,我们这里就使用 shouldOverrideUrlLoading (WebView view, String url)) 方法来介绍一下:

      • 代码很简单,这个方法可以拦截 WebView 中加载 url 的过程,得到对应的 url,我们就可以通过这个方法,与网页约定好一个协议,如果匹配,执行相应操作。
    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 代码调用
    function openActivity(){document.location = "js://openActivity?arg1=111&arg2=222";
    }
    
    • 存在问题:这个代码执行之后,就会触发本地的 shouldOverrideUrlLoading 方法,然后进行参数解析,调用指定方法。这个方式不会存在第一种提到的漏洞问题,但是它也有一个很繁琐的地方是,如果 web 端想要得到方法的返回值,只能通过 WebView 的 loadUrl 方法去执行 JS 方法把返回值传递回去,相关的代码如下:
    //java
    mWebView.loadUrl("javascript:returnResult(" + result + ")");//javascript
    function returnResult(result){alert("result is" + result);
    }
    
  • 第三种方式:利用 WebChromeClient 回调接口的三个方法拦截消息
    • 这个方法的原理和第二种方式原理一样,都是拦截相关接口,只是拦截的接口不一样:
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {return super.onJsAlert(view, url, message, result);
    }@Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {return super.onJsConfirm(view, url, message, result);
    }@Override
    public 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);
    }
    
    • 和 WebViewClient 一样,这次添加的是WebChromeClient接口,可以拦截JS中的几个提示方法,也就是几种样式的对话框,在 JS 中有三个常用的对话框方法:

      • onJsAlert 方法是弹出警告框,一般情况下在 Android 中为 Toast,在文本里面加入\n就可以换行;
      • onJsConfirm 弹出确认框,会返回布尔值,通过这个值可以判断点击时确认还是取消,true表示点击了确认,false表示点击了取消;
      • onJsPrompt 弹出输入框,点击确认返回输入框中的值,点击取消返回 null。
    • 但是这三种对话框都是可以本地拦截到的,所以可以从这里去做一些更改,拦截这些方法,得到他们的内容,进行解析,比如如果是 JS 的协议,则说明为内部协议,进行下一步解析然后进行相关的操作即可,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 方法是可以返回字符串类型的值,操作最全面方便。
  • 以上三种方案的总结和对比
    • 以上三种方案都是可行的,在这里总结一下
    • 第一种方式:是现在目前最普遍的用法,方便简洁,但是唯一的不足是在 4.2 系统以下存在漏洞问题;
    • 第二种方式:通过拦截 url 并解析,如果是已经约定好的协议则进行相应规定好的操作,缺点就是协议的约束需要记录一个规范的文档,而且从 Native 层往 Web 层传递值比较繁琐,优点就是不会存在漏洞,iOS7 之下的版本就是使用的这种方式。
    • 第三种方式:和第二种方式的思想其实是类似的,只是拦截的方法变了,这里拦截了 JS 中的三种对话框方法,而这三种对话框方法的区别就在于返回值问题,alert 对话框没有返回值,confirm 的对话框方法只有两种状态的返回值,prompt 对话框方法可以返回任意类型的返回值,缺点就是协议的制定比较麻烦,需要记录详细的文档,但是不会存在第二种方法的漏洞问题。

1.0.4 WebView.loadUrl(url)流程

  • WebView.loadUrl(url)加载网页做了什么?

    • 加载网页是一个复杂的过程,在这个过程中,我们可能需要执行一些操作,包括:
    • 加载网页前,重置WebView状态以及与业务绑定的变量状态。WebView状态包括重定向状态(mTouchByUser)、前端控制的回退栈(mBackStep)等,业务状态包括进度条、当前页的分享内容、分享按钮的显示隐藏等。
    • 加载网页前,根据不同的域拼接本地客户端的参数,包括基本的机型信息、版本信息、登录信息以及埋点使用的Refer信息等,有时候涉及交易、财产等还需要做额外的配置。
    • 开始执行页面加载操作时,会回调WebViewClient.onPageStarted(webview,url,favicon)。在此方法中,可以重置重定向保护的变量(mRedirectProtected),当然也可以在页面加载前重置,由于历史遗留代码问题,此处尚未省去优化。
    • 加载页面的过程中,WebView会回调几个方法。
    • 页面加载结束后,WebView会回调几个方法。
  • 加载页面的过程中回调哪些方法?
    • WebChromeClient.onReceivedTitle(webview, title),用来设置标题。需要注意的是,在部分Android系统版本中可能会回调多次这个方法,而且有时候回调的title是一个url,客户端可以针对这种情况进行特殊处理,避免在标题栏显示不必要的链接。
    • WebChromeClient.onProgressChanged(webview, progress),根据这个回调,可以控制进度条的进度(包括显示与隐藏)。一般情况下,想要达到100%的进度需要的时间较长(特别是首次加载),用户长时间等待进度条不消失必定会感到焦虑,影响体验。其实当progress达到80的时候,加载出来的页面已经基本可用了。事实上,国内厂商大部分都会提前隐藏进度条,让用户以为网页加载很快。
    • WebViewClient.shouldInterceptRequest(webview, request),无论是普通的页面请求(使用GET/POST),还是页面中的异步请求,或者页面中的资源请求,都会回调这个方法,给开发一次拦截请求的机会。在这个方法中,我们可以进行静态资源的拦截并使用缓存数据代替,也可以拦截页面,使用自己的网络框架来请求数据。包括后面介绍的WebView免流方案,也和此方法有关。
    • WebViewClient.shouldOverrideUrlLoading(webview, request),如果遇到了重定向,或者点击了页面中的a标签实现页面跳转,那么会回调这个方法。可以说这个是WebView里面最重要的回调之一,后面WebView与Native页面交互一节将会详细介绍这个方法。
    • WebViewClient.onReceivedError(webview,handler,error),加载页面的过程中发生了错误,会回调这个方法。主要是http错误以及ssl错误。在这两个回调中,我们可以进行异常上报,监控异常页面、过期页面,及时反馈给运营或前端修改。在处理ssl错误时,遇到不信任的证书可以进行特殊处理,例如对域名进行判断,针对自己公司的域名“放行”,防止进入丑陋的错误证书页面。也可以与Chrome一样,弹出ssl证书疑问弹窗,给用户选择的余地。
  • 加载页面结束回调哪些方法
    • 会回调WebViewClient.onPageFinished(webview,url)。
    • 这时候可以根据回退栈的情况判断是否显示关闭WebView按钮。通过mActivityWeb.canGoBackOrForward(-1)判断是否可以回退。

1.0.5 js的调用时机分析

  • onPageFinished()或者onPageStarted()方法中注入js代码

    • 做过WebView开发,并且需要和js交互,大部分都会认为js在WebViewClient.onPageFinished()方法中注入最合适,此时dom树已经构建完成,页面已经完全展现出来。但如果做过页面加载速度的测试,会发现WebViewClient.onPageFinished()方法通常需要等待很久才会回调(首次加载通常超过3s),这是因为WebView需要加载完一个网页里主文档和所有的资源才会回调这个方法。
    • 能不能在WebViewClient.onPageStarted()中注入呢?答案是不确定。经过测试,有些机型可以,有些机型不行。在WebViewClient.onPageStarted()中注入还有一个致命的问题——这个方法可能会回调多次,会造成js代码的多次注入。
    • 从7.0开始,WebView加载js方式发生了一些小改变,官方建议把js注入的时机放在页面开始加载之后
  • WebViewClient.onProgressChanged()方法中注入js代码
    • WebViewClient.onProgressChanged()这个方法在dom树渲染的过程中会回调多次,每次都会告诉我们当前加载的进度。

      • 在这个方法中,可以给WebView自定义进度条,类似微信加载网页时的那种进度条
      • 如果在此方法中注入js代码,则需要避免重复注入,需要增强逻辑。可以定义一个boolean值变量控制注入时机
    • 那么有人会问,加载到多少才需要处理js注入逻辑呢?
      • 正是因为这个原因,页面的进度加载到80%的时候,实际上dom树已经渲染得差不多了,表明WebView已经解析了标签,这时候注入一定是成功的。在WebViewClient.onProgressChanged()实现js注入有几个需要注意的地方:
      • 1 上文提到的多次注入控制,使用了boolean值变量控制
      • 2 重新加载一个URL之前,需要重置boolean值变量,让重新加载后的页面再次注入js
      • 3 如果做过本地js,css等缓存,则先判断本地是否存在,若存在则加载本地,否则加载网络js
      • 4 注入的进度阈值可以自由定制,理论上10%-100%都是合理的,不过建议使用了75%到90%之间可以。

1.0.6 清除缓存数据方式有哪些

  • 清除缓存数据的方法有哪些?

    //清除网页访问留下的缓存
    //由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序.
    Webview.clearCache(true);//清除当前webview访问的历史记录//只会webview访问历史记录里的所有记录除了当前访问记录
    Webview.clearHistory();//这个api仅仅清除自动完成填充的表单数据,并不会清除WebView存储到本地的数据
    Webview.clearFormData();
    

1.0.7 如何使用DeepLink

  • 具体可以看这篇文章:https://www.jianshu.com/p/127c80f62655

1.0.8 应用被作为第三方浏览器打开

  • 微信里的文章页面,可以选择“在浏览器打开”。现在很多应用都内嵌了WebView,那是否可以使自己的应用作为第三方浏览器打开此文章呢?
  • 在Manifest文件中,给想要接收跳转的Activity添加配置:
    <activityandroid:name=".X5WebViewActivity"android:configChanges="orientation|screenSize"android:hardwareAccelerated="true"android:launchMode="singleTask"android:screenOrientation="portrait"android:theme="@style/Theme.AppCompat.Light.NoActionBar"><!--需要添加下面的intent-filter配置--><intent-filter tools:ignore="AppLinkUrlError"><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><!--使用http,则只能打开http开头的网页--><data android:scheme="https" /></intent-filter>
    </activity>
    
  • 然后在 X5WebViewActivity 中获取相关传递数据。具体可以看lib中的X5WebViewActivity类代码。
    public class X5WebViewActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_web_view);getIntentData();initTitle();initWebView();webView.loadUrl(mUrl);// 处理 作为三方浏览器打开传过来的值getDataFromBrowser(getIntent());}/*** 使用singleTask启动模式的Activity在系统中只会存在一个实例。* 如果这个实例已经存在,intent就会通过onNewIntent传递到这个Activity。* 否则新的Activity实例被创建。*/@Overrideprotected void onNewIntent(Intent intent) {super.onNewIntent(intent);getDataFromBrowser(intent);}/*** 作为三方浏览器打开传过来的值* Scheme: https* host: www.jianshu.com* path: /p/yc* url = scheme + "://" + host + path;*/private void getDataFromBrowser(Intent intent) {Uri data = intent.getData();if (data != null) {try {String scheme = data.getScheme();String host = data.getHost();String path = data.getPath();String text = "Scheme: " + scheme + "\n" + "host: " + host + "\n" + "path: " + path;Log.e("data", text);String url = scheme + "://" + host + path;webView.loadUrl(url);} catch (Exception e) {e.printStackTrace();}}}
    }
    
  • 一些重点说明
    • 在微信中“通过浏览器”打开自己的应用,然后将自己的应用切到后台。重复上面的操作,会一直创建应用的实例,这样肯定是不好的,为了避免这种情况我们设置启动模式为:launchMode=“singleTask”。

02.优化汇总目录介绍

2.0.1 视频全屏播放按返回页面被放大(部分手机出现)

  • 至于原因暂时没有找到,解决方案如下所示

    /*** 当缩放改变的时候会调用该方法* @param view                              view* @param oldScale                          之前的缩放比例* @param newScale                          现在缩放比例*/
    @Override
    public void onScaleChanged(WebView view, float oldScale, float newScale) {super.onScaleChanged(view, oldScale, newScale);//视频全屏播放按返回页面被放大的问题if (newScale - oldScale > 7) {//异常放大,缩回去。view.setInitialScale((int) (oldScale / newScale * 100));}
    }
    

2.0.2 加载webView中的资源时,加快加载的速度优化,主要是针对图片

  • html代码下载到WebView后,webkit开始解析网页各个节点,发现有外部样式文件或者外部脚本文件时,会异步发起网络请求下载文件,但如果在这之前也有解析到image节点,那势必也会发起网络请求下载相应的图片。在网络情况较差的情况下,过多的网络请求就会造成带宽紧张,影响到css或js文件加载完成的时间,造成页面空白loading过久。解决的方法就是告诉WebView先不要自动加载图片,等页面finish后再发起图片加载。

    //初始化的时候设置,具体代码在X5WebView类中
    if(Build.VERSION.SDK_INT >= KITKAT) {//设置网页在加载的时候暂时不加载图片ws.setLoadsImagesAutomatically(true);
    } else {ws.setLoadsImagesAutomatically(false);
    }/*** 当页面加载完成会调用该方法* @param view                              view* @param url                               url链接*/
    @Override
    public void onPageFinished(WebView view, String url) {super.onPageFinished(view, url);//页面finish后再发起图片加载if(!webView.getSettings().getLoadsImagesAutomatically()) {webView.getSettings().setLoadsImagesAutomatically(true);}
    }
    

2.0.3 自定义加载异常error的状态页面,比如下面这些方法中可能会出现error