Android WebView 全面开发指南

Android WebView 全面开发指南

前言

总结Android WebView常用的相关知识点,令包含以下干货内容分析:Js注入漏洞,WebView遇到的坑,JsBridge原理以及框架使用(JsBridgeDSBridge-Android),缓存机制应用,性能优化,腾讯开源框架VasSonic (之后会进行代码分析)。


目录

目录

一,简介

这部分主要介绍下WebView,WebView是一个用来显示Web网页的控件,继承自AbsoluteLayout,和使用系统其他控件没什么区别,只是WeView控件方法比较多比较丰富。因为它就是一个微型浏览器,包含一个浏览器该有的基本功能,例如:滚动,缩放,前进,后退下一页,搜索,执行Js等功能。

在Android 4.4之前使用WebKit作为渲染内核,4.4之后采用chrome内核.Api使用兼容低版本。

官方WebView.html
显示网页的视图。此类是您可以在其中滚动自己的Web浏览器或仅在活动中显示某些在线内容的基础。它使用WebKit渲染引擎来显示网页,并包括在历史记录中前后导航,放大和缩小,执行文本搜索等方法。


二,基本使用

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code><span style="color:#929292">//配置网络权限</span>
<uses-permission android:name=<span style="color:#98c379">"android.permission.INTERNET"</span>/>

 <span style="color:#929292">//布局</span>
<span style="color:#61aeee"><?</span>xml version=<span style="color:#98c379">"1.0"</span> encoding=<span style="color:#98c379">"utf-8"</span><span style="color:#61aeee">?></span>
    <LinearLayout
      xmlns:android=<span style="color:#98c379">"http://schemas.android.com/apk/res/android"</span>
      android:id=<span style="color:#98c379">"@+id/main"</span>
      android:layout_width=<span style="color:#98c379">"match_parent"</span>
      android:layout_height=<span style="color:#98c379">"match_parent"</span>>

<com.webview.SafeWebView
    android:id=<span style="color:#98c379">"@+id/web_view"</span>
    android:layout_width=<span style="color:#98c379">"match_parent"</span>
    android:layout_height=<span style="color:#98c379">"match_parent"</span>/>
</LinearLayout>

  <span style="color:#929292">// BaseWebView 是我自己封装的 WebView </span>

  <span style="color:#929292">//实际使用时请采用 new 的方式</span>
  mWebView = (SafeWebView) findViewById(R.id.web_view);
  mWebView.addJavascriptInterface(<span style="color:#c678dd">new</span> NativeInterface(this), <span style="color:#98c379">"AndroidNative"</span>);
  mWebView.loadUrl(<span style="color:#98c379">"http://www.jianshu.com/u/fa272f63280a"</span>);
</code></span></span>

三,WebView方法

主要包含WebView的使用方法。我们基于这些方法能扩展很多其他功能,例如:JsBridge,缓存等。

常用方法

  • void loadUrl(String url):加载网络链接url
  • boolean canGoBack():判断WebView当前是否可以返回上一页
  • GoBack的():回退到上一页
  • boolean canGoForward():判断WebView当前是否可以向前一页
  • goForward():回退到前一页
  • onPause():类似活动生命周期,页面进入后台不可见状态
  • pauseTimers():该方法面向全局整个应用程序的webview,它会暂停所有webview的布局,解析,JavaScript Timer。当程序进入后台时,该方法的调用可以降低CPU功耗。
  • onResume():在调用onPause()后,可以调用该方法来恢复WebView的运行。
  • resumeTimers():恢复pauseTimers时的所有操作。(注:pauseTimers和resumeTimers方法必须一起使用,否则再使用其它场景下的WebView会有问题)
  • destroy():销毁WebView
  • clearHistory():清除当前WebView访问的历史记录。
  • clearCache(boolean includeDiskFiles):清空网页访问留下的缓存数据。需要注意的时,由于缓存是全局的,所以只要是WebView用到的缓存都会被清空,即便其他地方也会使用到。该方法接受一个参数,从命名即可看出作用。若设为假,则只清空内存里的资源缓存,而不清空磁盘里的。
  • 重新加载():重新加载当前请求
  • setLayerType(int layerType,Paint paint):设置硬件加速,软件加速
  • removeAllViews():清除子图。
  • clearSslPreferences():清除SSL信息。
  • clearMatches():清除网页查找的高亮匹配字符。
  • removeJavascriptInterface(String interfaceName):删除interfaceName对应的注入对象
  • addJavascriptInterface(Object object,String interfaceName):注入java对象。
  • setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled):设置垂直方向滚动条。
  • setHorizo​​ntalScrollBarEnabled(boolean horizo​​ntalScrollBarEnabled):设置横向滚动条。
  • loadUrl(String url,Map <String,String> additionalHttpHeaders):加载制定url并携带http header数据。
  • evaluateJavascript(String script,ValueCallback <String> resultCallback):Api 19之后可以采用此方法之行Js。
  • stopLoading():停止WebView当前加载。
  • clearView():在Android 4.3及其以上系统这个api被丢弃了,并且这个api大多数情况下会有bug,经常不能清除掉之前的渲染数据。官方建议通过loadUrl(“about:blank”)来实现这个功能,阴雨需要重新加载一个页面自然时间会收到影响。
  • freeMemory():释放内存,不过貌似不好用。
  • clearFormData():清除自动完成填充的表单数据需要注意的是,该方法仅仅清除当前表单域自动完成填充的表单数据,并不会清除web视图存储到本地的数据。

我这里在介绍下下面几组方法,比较重要,项目当中可能会遇到坑

  • onPause()尽力尝试暂停可以暂停的任何处理,如动画和地理位置。不会暂停JavaScript。要全局暂停JavaScript,可使用pauseTimers。

  • onResume()恢复onPause()停掉的操作;

  • pauseTimers()暂停所有WebView的布局,解析和JavaScript定时器。这个是一个全局请求,不仅限于这个WebView。

  • resumeTimers()恢复所有WebView的所有布局,解析和JavaScript计时器,将恢复调度所有计时器。

另外注意JS端setTimeout(),setInterval()方法使用,自测来看,当不使用pauseTimers()和pauseTimers(),从活动返回上一个包含WebView的活动时间,页面里的setTimeout()是不执行的,setInterval()是可以恢复执行的。

在适当的生命周期使用pauseTimers()和pauseTimers()既可以恢复setTimeout()执行。

一份WebView方法使用清单

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code>    mWebView.loadUrl(<span style="color:#98c379">"http://www.jianshu.com/u/fa272f63280a"</span>);<span style="color:#929292">// 加载url,也可以执行js函数</span>
    mWebView.setWebViewClient(<span style="color:#c678dd">new</span> SafeWebViewClient());<span style="color:#929292">// 设置 WebViewClient </span>
    mWebView.setWebChromeClient(<span style="color:#c678dd">new</span> SafeWebChromeClient());<span style="color:#929292">// 设置 WebChromeClient</span>
    mWebView.onResume();<span style="color:#929292">// 生命周期onResume</span>
    mWebView.resumeTimers();<span style="color:#929292">//生命周期resumeTimers</span>
    mWebView.onPause();<span style="color:#929292">//生命周期onPause</span>
    mWebView.pauseTimers();<span style="color:#929292">//生命周期pauseTimers (上数四个方法都是成对出现)</span>
    mWebView.stopLoading();<span style="color:#929292">// 停止当前加载</span>
    mWebView.clearMatches();<span style="color:#929292">// 清除网页查找的高亮匹配字符。</span>
    mWebView.clearHistory();<span style="color:#929292">// 清除当前 WebView 访问的历史记录</span>
    mWebView.clearSslPreferences();<span style="color:#929292">//清除ssl信息</span>
    mWebView.clearCache(<span style="color:#56b6c2">true</span>);<span style="color:#929292">//清空网页访问留下的缓存数据。需要注意的时,由于缓存是全局的,所以只要是WebView用到的缓存都会被清空,即便其他地方也会使用到。该方法接受一个参数,从命名即可看出作用。若设为false,则只清空内存里的资源缓存,而不清空磁盘里的。</span>
    mWebView.loadUrl(<span style="color:#98c379">"about:blank"</span>);<span style="color:#929292">// 清空当前加载</span>
    mWebView.removeAllViews();<span style="color:#929292">// 清空子 View</span>
    <span style="color:#c678dd">if</span> (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
        mWebView.removeJavascriptInterface(<span style="color:#98c379">"AndroidNative"</span>);<span style="color:#929292">// 向 Web端注入 java 对象</span>
    }
    mWebView.destroy();<span style="color:#929292">// 生命周期销毁</span>
</code></span></span>

四,常用属性

主要包含三部分:WebSettings,WebViewClient,WebChromeClient。

WebSettings

常用方法

  • setJavaScriptEnabled(boolean flag):是否支持Js使用。
  • setCacheMode(int mode):设置WebView的缓存模式。
  • setAppCacheEnabled(boolean flag):是否启用缓存模式。
  • setAppCachePath(String appCachePath):Android私有缓存存储,如果你不调用setAppCachePath方法,WebView将不会产生这个目录。
  • setSupportZoom(boolean support):是否支持缩放。
  • setTextZoom(int textZoom):以百分比形式设置页面的文本缩放。默认值为100。
  • setAllowFileAccess(boolean allow):是否允许加载本地html文件/ false。
  • setDatabaseEnabled(boolean flag):是否开启数据库缓存
  • setDomStorageEnabled(boolean flag):是否开启DOM缓存。
  • setUserAgentString(String ua):设置UserAgent属性。
  • setLoadsImagesAutomatically(boolean flag):支持自动加载图片
  • setAllowFileAccessFromFileURLs(boolean flag ::允许通过文件url加载的Javascript读取其他的本地文件,Android 4.1之前默认是true,在Android 4.1及以后默认是false,也就是禁止。
  • setAllowUniversalAccessFromFileURLs(boolean flag):允许通过文件url加载的Javascript可以访问其他的源,包括其他的文件和http,https等其他的源,Android 4.1之前默认是true,在Android 4.1及以后默认是false,也就是禁止如果此设置是允许,则setAllowFileAccessFromFileURLs不起做用。
  • boolean getLoadsImagesAutomatically():是否支持自动加载图片。

一份使用清单

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code>    WebSettings webSettings = mWebView.getSettings();
    <span style="color:#c678dd">if</span> (webSettings == <span style="color:#c678dd">null</span>) <span style="color:#c678dd">return</span>;
    <span style="color:#929292">// 支持 Js 使用</span>
    webSettings.setJavaScriptEnabled(<span style="color:#c678dd">true</span>);
    <span style="color:#929292">// 开启DOM缓存,默认状态下是不支持LocalStorage的</span>
    webSettings.setDomStorageEnabled(<span style="color:#c678dd">true</span>);
    <span style="color:#929292">// 开启数据库缓存</span>
    webSettings.setDatabaseEnabled(<span style="color:#c678dd">true</span>);
    <span style="color:#929292">// 支持自动加载图片</span>
    webSettings.setLoadsImagesAutomatically(hasKitkat());
    <span style="color:#929292">// 设置 WebView 的缓存模式</span>
    webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
    <span style="color:#929292">// 支持启用缓存模式</span>
    webSettings.setAppCacheEnabled(<span style="color:#c678dd">true</span>);
    <span style="color:#929292">// 设置 AppCache 最大缓存值(现在官方已经不提倡使用,已废弃)</span>
    webSettings.setAppCacheMaxSize(<span style="color:#d19a66">8</span> * <span style="color:#d19a66">1024</span> * <span style="color:#d19a66">1024</span>);
    <span style="color:#929292">// Android 私有缓存存储,如果你不调用setAppCachePath方法,WebView将不会产生这个目录</span>
    webSettings.setAppCachePath(getCacheDir().getAbsolutePath());
    <span style="color:#929292">// 数据库路径</span>
    <span style="color:#c678dd">if</span> (!hasKitkat()) {
        webSettings.setDatabasePath(getDatabasePath(<span style="color:#98c379">"html"</span>).getPath());
    }
    <span style="color:#929292">// 关闭密码保存提醒功能</span>
    webSettings.setSavePassword(<span style="color:#c678dd">false</span>);
    <span style="color:#929292">// 支持缩放</span>
    webSettings.setSupportZoom(<span style="color:#c678dd">true</span>);
    <span style="color:#929292">// 设置 UserAgent 属性</span>
    webSettings.setUserAgentString(<span style="color:#98c379">""</span>);
    <span style="color:#929292">// 允许加载本地 html 文件/false</span>
    webSettings.setAllowFileAccess(<span style="color:#c678dd">true</span>);
    <span style="color:#929292">// 允许通过 file url 加载的 Javascript 读取其他的本地文件,Android 4.1 之前默认是true,在 Android 4.1 及以后默认是false,也就是禁止</span>
    webSettings.setAllowFileAccessFromFileURLs(<span style="color:#c678dd">false</span>);
    <span style="color:#929292">// 允许通过 file url 加载的 Javascript 可以访问其他的源,包括其他的文件和 http,https 等其他的源,</span>
    <span style="color:#929292">// Android 4.1 之前默认是true,在 Android 4.1 及以后默认是false,也就是禁止</span>
    <span style="color:#929292">// 如果此设置是允许,则 setAllowFileAccessFromFileURLs 不起做用</span>
    webSettings.setAllowUniversalAccessFromFileURLs(<span style="color:#c678dd">false</span>);
</code></span></span>

WebViewClient

1,常用方法

  • onPageStarted(WebView视图,String url,Bitmap favicon):WebView开始加载页面时回调,一次帧加载对应一次回调。
  • onLoadResource(WebView视图,String url):WebView加载页面资源时会回调,每一个资源产生的一次网络加载,除非本地有当前url对应有缓存,否则就会加载。
  • shouldInterceptRequest(WebView视图,String url):WebView可以拦截某一次的请求来返回我们自己加载的数据,这个方法在后面缓存会有很大作用。
  • shouldInterceptRequest(WebView视图,android.webkit.WebResourceRequest请求):WebView可以拦截某一次的请求来返回我们自己加载的数据,这个方法在后面缓存会有很大作用。
  • shouldOverrideUrlLoading(WebView view,String url):是否在WebView内加载页面。
  • onReceivedSslError(WebView视图,SslErrorHandler处理程序,SslError错误):WebView ssl访问证书出错,handler.cancel()取消加载,handler.proceed()对然错误也继续加载。
  • onPageFinished(WebView视图,String url):WebView完成加载页面时回调,一次帧加载对应一次回调。
  • onReceivedError(WebView视图,int errorCode,String description,String failingUrl):WebView访问url出错。

2,一份使用清单

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code><span style="color:#c678dd">public</span> <span style="color:#c678dd">class</span> <span style="color:#e6c07b">SafeWebViewClient</span> <span style="color:#c678dd">extends</span> <span style="color:#e6c07b">WebViewClient</span> {

    <span style="color:#929292">/**
     * 当WebView得页面Scale值发生改变时回调
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">void</span> <span style="color:#61aeee">onScaleChanged</span>(WebView view, <span style="color:#c678dd">float</span> oldScale, <span style="color:#c678dd">float</span> newScale) {
        <span style="color:#c678dd">super</span>.onScaleChanged(view, oldScale, newScale);
    }

    <span style="color:#929292">/**
     * 是否在 WebView 内加载页面
     *
     * <span style="color:#c678dd">@param</span> view
     * <span style="color:#c678dd">@param</span> url
     * <span style="color:#c678dd">@return</span>
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">boolean</span> <span style="color:#61aeee">shouldOverrideUrlLoading</span>(WebView view, String url) {
        view.loadUrl(url);
        <span style="color:#c678dd">return</span> <span style="color:#c678dd">true</span>;
    }

    <span style="color:#929292">/**
     * WebView 开始加载页面时回调,一次Frame加载对应一次回调
     *
     * <span style="color:#c678dd">@param</span> view
     * <span style="color:#c678dd">@param</span> url
     * <span style="color:#c678dd">@param</span> favicon
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">void</span> <span style="color:#61aeee">onPageStarted</span>(WebView view, String url, Bitmap favicon) {
        <span style="color:#c678dd">super</span>.onPageStarted(view, url, favicon);
    }

    <span style="color:#929292">/**
     * WebView 完成加载页面时回调,一次Frame加载对应一次回调
     *
     * <span style="color:#c678dd">@param</span> view
     * <span style="color:#c678dd">@param</span> url
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">void</span> <span style="color:#61aeee">onPageFinished</span>(WebView view, String url) {
        <span style="color:#c678dd">super</span>.onPageFinished(view, url);
    }

    <span style="color:#929292">/**
     * WebView 加载页面资源时会回调,每一个资源产生的一次网络加载,除非本地有当前 url 对应有缓存,否则就会加载。
     *
     * <span style="color:#c678dd">@param</span> view WebView
     * <span style="color:#c678dd">@param</span> url  url
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">void</span> <span style="color:#61aeee">onLoadResource</span>(WebView view, String url) {
        <span style="color:#c678dd">super</span>.onLoadResource(view, url);
    }

    <span style="color:#929292">/**
     * WebView 可以拦截某一次的 request 来返回我们自己加载的数据,这个方法在后面缓存会有很大作用。
     *
     * <span style="color:#c678dd">@param</span> view    WebView
     * <span style="color:#c678dd">@param</span> request 当前产生 request 请求
     * <span style="color:#c678dd">@return</span> WebResourceResponse
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> WebResourceResponse <span style="color:#61aeee">shouldInterceptRequest</span>(WebView view, WebResourceRequest request) {
        <span style="color:#c678dd">return</span> <span style="color:#c678dd">super</span>.shouldInterceptRequest(view, request);
    }

    <span style="color:#929292">/**
     * WebView 访问 url 出错
     *
     * <span style="color:#c678dd">@param</span> view
     * <span style="color:#c678dd">@param</span> request
     * <span style="color:#c678dd">@param</span> error
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">void</span> <span style="color:#61aeee">onReceivedError</span>(WebView view, WebResourceRequest request, WebResourceError error) {
        <span style="color:#c678dd">super</span>.onReceivedError(view, request, error);
    }

    <span style="color:#929292">/**
     * WebView ssl 访问证书出错,handler.cancel()取消加载,handler.proceed()对然错误也继续加载
     *
     * <span style="color:#c678dd">@param</span> view
     * <span style="color:#c678dd">@param</span> handler
     * <span style="color:#c678dd">@param</span> error
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">void</span> <span style="color:#61aeee">onReceivedSslError</span>(WebView view, SslErrorHandler handler, SslError error) {
        <span style="color:#c678dd">super</span>.onReceivedSslError(view, handler, error);
    }
}
</code></span></span>

WebChromeClient

1,常用方法

  • onConsoleMessage(String message,int lineNumber,String sourceID):输出Web端日志。
  • onProgressChanged(WebView视图,int newProgress):当前WebView加载网页进度。
  • onJsPrompt(WebView视图,String url,String消息,String defaultValue,JsPromptResult结果):处理JS中的提示对话框
  • onJsAlert(WebView视图,String url,String消息,JsResult结果):Js中调用alert()函数,产生的对话框。
  • onReceivedTitle(WebView视图,字符串标题):接收web页面的标题。
  • onReceivedIcon(WebView视图,位图图标):接收web页面的图标。

2,一份使用清单

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code><span style="color:#c678dd">public</span> <span style="color:#c678dd">class</span> <span style="color:#e6c07b">SafeWebChromeClient</span> <span style="color:#c678dd">extends</span> <span style="color:#e6c07b">WebChromeClient</span> {

    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">boolean</span> <span style="color:#61aeee">onConsoleMessage</span>(ConsoleMessage consoleMessage) {
        <span style="color:#c678dd">return</span> <span style="color:#c678dd">super</span>.onConsoleMessage(consoleMessage);
    }

    <span style="color:#929292">/**
     * 当前 WebView 加载网页进度
     *
     * <span style="color:#c678dd">@param</span> view
     * <span style="color:#c678dd">@param</span> newProgress
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">void</span> <span style="color:#61aeee">onProgressChanged</span>(WebView view, <span style="color:#c678dd">int</span> newProgress) {
        <span style="color:#c678dd">super</span>.onProgressChanged(view, newProgress);
    }

    <span style="color:#929292">/**
     * Js 中调用 alert() 函数,产生的对话框
     *
     * <span style="color:#c678dd">@param</span> view
     * <span style="color:#c678dd">@param</span> url
     * <span style="color:#c678dd">@param</span> message
     * <span style="color:#c678dd">@param</span> result
     * <span style="color:#c678dd">@return</span>
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">boolean</span> <span style="color:#61aeee">onJsAlert</span>(WebView view, String url, String message, JsResult result) {
        <span style="color:#c678dd">return</span> <span style="color:#c678dd">super</span>.onJsAlert(view, url, message, result);
    }

    <span style="color:#929292">/**
     * 处理 Js 中的 Confirm 对话框
     *
     * <span style="color:#c678dd">@param</span> view
     * <span style="color:#c678dd">@param</span> url
     * <span style="color:#c678dd">@param</span> message
     * <span style="color:#c678dd">@param</span> result
     * <span style="color:#c678dd">@return</span>
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">boolean</span> <span style="color:#61aeee">onJsConfirm</span>(WebView view, String url, String message, JsResult result) {
        <span style="color:#c678dd">return</span> <span style="color:#c678dd">super</span>.onJsConfirm(view, url, message, result);
    }

    <span style="color:#929292">/**
     * 处理 JS 中的 Prompt对话框
     *
     * <span style="color:#c678dd">@param</span> view
     * <span style="color:#c678dd">@param</span> url
     * <span style="color:#c678dd">@param</span> message
     * <span style="color:#c678dd">@param</span> defaultValue
     * <span style="color:#c678dd">@param</span> result
     * <span style="color:#c678dd">@return</span>
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">boolean</span> <span style="color:#61aeee">onJsPrompt</span>(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        <span style="color:#c678dd">return</span> <span style="color:#c678dd">super</span>.onJsPrompt(view, url, message, defaultValue, result);
    }

    <span style="color:#929292">/**
     * 接收web页面的icon
     *
     * <span style="color:#c678dd">@param</span> view
     * <span style="color:#c678dd">@param</span> icon
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">void</span> <span style="color:#61aeee">onReceivedIcon</span>(WebView view, Bitmap icon) {
        <span style="color:#c678dd">super</span>.onReceivedIcon(view, icon);
    }

    <span style="color:#929292">/**
     * 接收web页面的 Title
     *
     * <span style="color:#c678dd">@param</span> view
     * <span style="color:#c678dd">@param</span> title
     */</span>
    <span style="color:#61aeee">@Override</span>
    <span style="color:#c678dd">public</span> <span style="color:#c678dd">void</span> <span style="color:#61aeee">onReceivedTitle</span>(WebView view, String title) {
        <span style="color:#c678dd">super</span>.onReceivedTitle(view, title);
    }

}
</code></span></span>

五,于JavaScript交互

介绍

注:这里着重介绍下第一种标准方式,后面会介绍其他两种方式。

1,使用系统方法addJavascriptInterface注入java对象来实现。

2,利用WebViewClient中shouldOverrideUrlLoading(WebView视图,String url)接口,拦截操作。这个就是很多公司在用的方案方式,通过制定url协议,双方各自解析,使用iframe来调用native代码,实现互通。

3,利用WebChromeClient中的onJsAlert,onJsConfirm,onJsPrompt提示接口,同样也是拦截操作。

使用清单:

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code><span style="color:#929292">//开启Js可用</span>
mWebView.getSettings().setJavaScriptEnabled(<span style="color:#c678dd">true</span>);

<span style="color:#929292">// 创建要注入的 Java 类</span>
<span style="color:#c678dd">public</span> <span style="color:#c678dd">class</span> <span style="color:#e6c07b">NativeInterface</span> {

    <span style="color:#c678dd">private</span> Context mContext;

    <span style="color:#c678dd">public</span> NativeInterface(Context context) {
        mContext = context;
    }

    @JavascriptInterface
    <span style="color:#c678dd">public</span> void hello() {
        Toast.makeText(mContext, <span style="color:#98c379">"hello"</span>, Toast.LENGTH_SHORT).show();
    }

    @JavascriptInterface
    <span style="color:#c678dd">public</span> void hello(String params) {
        Toast.makeText(mContext, params, Toast.LENGTH_SHORT).show();
    }

    @JavascriptInterface
    <span style="color:#c678dd">public</span> String getAndroid() {
        Toast.makeText(mContext, <span style="color:#98c379">"getAndroid"</span>, Toast.LENGTH_SHORT).show();
        <span style="color:#c678dd">return</span> <span style="color:#98c379">"Android data"</span>;
    }

}

<span style="color:#929292">// WebView 注入即可</span>
mWebView.addJavascriptInterface(<span style="color:#c678dd">new</span> NativeInterface(this), <span style="color:#98c379">"AndroidNative"</span>);

<span style="color:#929292">//Js编写</span>
<script>
    <span style="color:#c678dd">function</span> <span style="color:#61aeee">callHello</span>(){
        AndroidNative.hello();
    }

    <span style="color:#c678dd">function</span> <span style="color:#61aeee">callHello1</span>(){
        AndroidNative.hello(<span style="color:#98c379">'hello Android'</span>);
    }

    <span style="color:#c678dd">function</span> <span style="color:#61aeee">callAndroid</span>(){
        <span style="color:#c678dd">var</span> temp = AndroidNative.getAndroid();
        console.log(temp);
        alert(temp);
    }  

</script>
</code></span></span>

Native调用Js:mWebView.loadUrl(js);

Js调用Native:AndroidNative.getAndroid();

4.2版本以下会存在漏洞,4.2以上需要添加@JavascriptInterface注解才能被调用到,Js调用方式不变。


六,Js注入漏洞

虽然可以通过注入方式来实现WebView和JS交互,但是实现功能的同时也带了安全问题,通过注入的Java类作为桥梁,JS就可以利用这个漏洞。

常见漏洞

目前已知的WebView漏洞有4个,分别是:

1,CVE-2012-6636,揭露了WebView中addJavascriptInterface接口会引起远程代码执行漏洞; 
2,CVE-2013-4710,针对某些特定机型会存在addJavascriptInterface API引起的远程代码执行漏洞; 
3,CVE- 2014-1939爆出WebView中内置导出的“searchBoxJavaBridge_”Java对象可能被利用,实现远程任意代码; 
4,CVE-2014-7224,类似于CVE-2014-1939,WebView内置导出“accessibility”和“accessibilityTraversal”两个Java Object接口,可被利用实现远程任意代码执行。

如何解决漏洞

1,Android 4.2以下不要在使用JavascriptInterface方式,4.2以上需要添加注解@JavascriptInterface才能调用。(这部分和JsBrige有关,更详细的内容后面会介绍)

2,同1解决;

3,在创建WebView时,使用removeJavascriptInterface方法将系统注入的searchBoxJavaBridge_对象删除。

4,当系统辅助功能服务被开启时,在Android 4.4以下的系统中,由系统提供的WebView组件都默认导出“accessibility”和“accessibilityTraversal”这两个接口,同两个接口同样存在远程任意代码执行的威胁,同样的需要通过removeJavascriptInterface方法将这两个对象删除。

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code>       <span style="color:#c678dd">super</span>.removeJavascriptInterface(<span style="color:#98c379">"searchBoxJavaBridge_"</span>);
       <span style="color:#c678dd">super</span>.removeJavascriptInterface(<span style="color:#98c379">"accessibility"</span>);
       <span style="color:#c678dd">super</span>.removeJavascriptInterface(<span style="color:#98c379">"accessibilityTraversal"</span>);
</code></span></span>

以上都是系统机制层面上的漏洞,还有一些是使用WebView不挡产生的漏洞。

5,通过WebSettings.setSavePassword(false)关闭密码保存提醒功能,防止明文密码存在本地被盗用。

6,WebView默认是可以使用文件协议的,也就是setAllowFileAccess(true),我们应该是主动设置为setAllowFileAccess(false),防止加载本地文件,移动版的Chrome默认禁止加载文件协议的文件

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code>setAllowFileAccess(<span style="color:#56b6c2">true</span>);<span style="color:#98c379">//</span>设置为 <span style="color:#56b6c2">false</span> 将不能加载本地 html 文件
setAllowFileAccessFromFileURLs(<span style="color:#56b6c2">false</span>);
setAllowUniversalAccessFromFileURLs(<span style="color:#56b6c2">false</span>);
<span style="color:#c678dd">if</span> (url.startsWith(<span style="color:#98c379">"file://"</span>) {
    setJavaScriptEnabled(<span style="color:#56b6c2">false</span>);
} <span style="color:#c678dd">else</span> {
    setJavaScriptEnabled(<span style="color:#56b6c2">true</span>);
}
</code></span></span>

安全修复案例

推荐SafeWebView这个库中解决了Android WebView中Js注入漏洞问题,另外还包含了一些异常处理。可以自行下载阅读源码。


七,一些坑

主要总结WebView相关的疑难bug,由于Android版本严重碎片化,在使用WebView的时候也会遇到各种个样的坑,特别是4.4之后更换了WebView内核,4.2以下有部分漏洞,所以想把经历过的WebView这些坑记录下来,仅供参考。

1,android.webkit.AccessibilityInjector $ TextToSpeechWrapper

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code>java.lang.NullPointerException
    at android.webkit.AccessibilityInjector<span style="color:#d19a66">$TextToSpeechWrapper</span><span style="color:#d19a66">$1</span>.onInit(AccessibilityInjector.java:753)
    at android.speech.tts.TextToSpeech.dispatchOnInit(TextToSpeech.java:640)
    at android.speech.tts.TextToSpeech.initTts(TextToSpeech.java:619)
    at android.speech.tts.TextToSpeech.<init>(TextToSpeech.java:553)
    at android.webkit.AccessibilityInjector<span style="color:#d19a66">$TextToSpeechWrapper</span>.<init>(AccessibilityInjector.java:676)
    at android.webkit.AccessibilityInjector.addTtsApis(AccessibilityInjector.java:480)
    at android.webkit.AccessibilityInjector.addAccessibilityApisIfNecessary(AccessibilityInjector.java:168)
    at android.webkit.AccessibilityInjector.onPageStarted(AccessibilityInjector.java:340)
    at android.webkit.WebViewClassic.onPageStarted(WebViewClassic.java:4480)
    at android.webkit.CallbackProxy.handleMessage(CallbackProxy.java:366)
    at android.os.Handler.dispatchMessage(Handler.java:107)
    at android.os.Looper.loop(Looper.java:194)
    at android.app.ActivityThread.main(ActivityThread.java:5407)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:525)
    at com.android.internal.os.ZygoteInit<span style="color:#d19a66">$MethodAndArgsCaller</span>.run(ZygoteInit.java:833)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:600)
    at dalvik.system.NativeStart.main(Native Method)
</code></span></span>

此问题在4.2.1和4.2.2比较集中,关闭辅助功能,google下很多结果都是一样的。

修复方法:在初始化WebView时调用disableAccessibility方法即可。

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code><span style="color:#c678dd">public</span> <span style="color:#c678dd">static</span> <span style="color:#c678dd">void</span> <span style="color:#61aeee">disableAccessibility</span>(Context context) {
        <span style="color:#c678dd">if</span> (Build.VERSION.SDK_INT == <span style="color:#d19a66">17</span><span style="color:#929292">/*4.2 (Build.VERSION_CODES.JELLY_BEAN_MR1)*/</span>) {
            <span style="color:#c678dd">if</span> (context != <span style="color:#c678dd">null</span>) {
                <span style="color:#c678dd">try</span> {
                    AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
                    <span style="color:#c678dd">if</span> (!am.isEnabled()) {
                        <span style="color:#929292">//Not need to disable accessibility</span>
                        <span style="color:#c678dd">return</span>;
                    }

                    Method setState = am.getClass().getDeclaredMethod(<span style="color:#98c379">"setState"</span>, <span style="color:#c678dd">int</span>.class);
                    setState.setAccessible(<span style="color:#c678dd">true</span>);
                    setState.invoke(am, <span style="color:#d19a66">0</span>);<span style="color:#929292">/**{<span style="color:#c678dd">@link</span> AccessibilityManager#STATE_FLAG_ACCESSIBILITY_ENABLED}*/</span>
                } <span style="color:#c678dd">catch</span> (Exception ignored) {
                    ignored.printStackTrace();
                }
            }
        }
    }
</code></span></span>

2,android.content.pm.PackageManager $的NameNotFoundException

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code>AndroidRuntimeException: android.content.pm.PackageManager$NameNotFoundException: com.google.android.webview
    at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4604)
    at android.app.ActivityThread.access$1500(ActivityThread.java:154)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1389)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:135)
    at android.app.ActivityThread.main(ActivityThread.java:5302)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:916)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:711)
Caused by: android.util.AndroidRuntimeException: android.content.pm.PackageManager$NameNotFoundException: com.google.android.webview
    at android.webkit.WebViewFactory.getFactoryClass(WebViewFactory.java:174)
    at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:109)
    at android.webkit.WebView.getFactory(WebView.java:2194)
    at android.webkit.WebView.ensureProviderCreated(WebView.java:2189)
    at android.webkit.WebView.setOverScrollMode(WebView.java:2248)
    at android.view.View.<<span style="color:#e06c75">init</span>>(View.java:3588)
    at android.view.View.<<span style="color:#e06c75">init</span>>(View.java:3682)
    at android.view.ViewGroup.<<span style="color:#e06c75">init</span>>(ViewGroup.java:497)
    at android.widget.AbsoluteLayout.<<span style="color:#e06c75">init</span>>(AbsoluteLayout.java:55)
    at android.webkit.WebView.<<span style="color:#e06c75">init</span>>(WebView.java:544)
    at android.webkit.WebView.<<span style="color:#e06c75">init</span>>(WebView.java:489)
    at android.webkit.WebView.<<span style="color:#e06c75">init</span>>(WebView.java:472)
    at android.webkit.WebView.<<span style="color:#e06c75">init</span>>(WebView.java:459)
    at android.webkit.WebView.<<span style="color:#e06c75">init</span>>(WebView.java:449)
</code></span></span>

现象:在创建WebView时崩溃,跟进栈信息,我们需要在setOverScrollMode方法上加异常保护处理

修复方法:

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code><span style="color:#c678dd">try</span> {
    <span style="color:#c678dd">super</span>.setOverScrollMode(mode);
} <span style="color:#c678dd">catch</span> (Throwable e) {
    e.printStackTrace();
}  
</code></span></span>

不过上面捕获的异常范围有点广,在github上上找到一个更全面的修复方法

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code><span style="color:#c678dd">try</span>{
    <span style="color:#c678dd">super</span>.setOverScrollMode(mode);
} <span style="color:#c678dd">catch</span>(Throwable e){
        <span style="color:#e6c07b">String</span> messageCause = e.getCause() == <span style="color:#56b6c2">null</span> ? e.toString() : e.getCause().toString();
<span style="color:#e6c07b">String</span> trace = Log.getStackTraceString(e);
<span style="color:#c678dd">if</span> (trace.contains(<span style="color:#98c379">"android.content.pm.PackageManager$NameNotFoundException"</span>)
  || trace.contains(<span style="color:#98c379">"java.lang.RuntimeException: Cannot load WebView"</span>)
    || trace.contains(<span style="color:#98c379">"android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed"</span>)) {
      e.printStackTrace();
    }<span style="color:#c678dd">else</span>{
      <span style="color:#c678dd">throw</span> e;
    }        
}
</code></span></span>

3,android.webkit.WebViewClassic.clearView

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code>at android.webkit.BrowserFrame.nativeLoadUrl(Native Method)
System.err:     at android.webkit.BrowserFrame.loadUrl(BrowserFrame.java:279)
System.err:     at android.webkit.WebViewCore.loadUrl(WebViewCore.java:2011)
System.err:     at android.webkit.WebViewCore.access<span style="color:#d19a66">$1900</span>(WebViewCore.java:57)
System.err:     at android.webkit.WebViewCore<span style="color:#d19a66">$EventHub</span><span style="color:#d19a66">$1</span>.handleMessage(WebViewCore.java:1303)
System.err:     at android.os.Handler.dispatchMessage(Handler.java:99)
System.err:     at android.os.Looper.loop(Looper.java:137)
System.err:     at android.webkit.WebViewCore<span style="color:#d19a66">$WebCoreThread</span>.run(WebViewCore.java:812)
System.err:     at java.lang.Thread.run(Thread.java:856)
webcoreglue: *** Uncaught exception returned from Java call!
System.err: java.lang.NullPointerException
System.err:     at android.webkit.WebViewClassic.clearView(WebViewClassic.java:2868)
System.err:     at android.webkit.WebViewCore.setupViewport(WebViewCore.java:2497)
System.err:     at android.webkit.WebViewCore.updateViewport(WebViewCore.java:2479)
System.err:     at android.webkit.BrowserFrame.nativeLoadUrl(Native Method)
System.err:     at android.webkit.BrowserFrame.loadUrl(BrowserFrame.java:279)
System.err:     at android.webkit.WebViewCore.loadUrl(WebViewCore.java:2011)
System.err:     at android.webkit.WebViewCore.access<span style="color:#d19a66">$1900</span>(WebViewCore.java:57)
System.err:     at android.webkit.WebViewCore<span style="color:#d19a66">$EventHub</span><span style="color:#d19a66">$1</span>.handleMessage(WebViewCore.java:1303)
System.err:     at android.os.Handler.dispatchMessage(Handler.java:99)
System.err:     at android.os.Looper.loop(Looper.java:137)
</code></span></span>

这个bug是在某些设备上发生的,是在调用webView.destroy()之前调用了loadurl操作发生的,也不是毕现问题,所以只能跟进源码查看,
在清空webview destroy时,调用清理方法,内部可能时机有问题,会出现,WebViewClassic中mWebViewCore对象为null,其内部为处理程序消息机制。

修复方法:

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code><span style="color:#c678dd">public</span> <span style="color:#c678dd">void</span> <span style="color:#61aeee">logdUrl</span>(<span style="color:#c678dd">final</span> String url) {
    <span style="color:#c678dd">try</span> {
        <span style="color:#c678dd">super</span>.loadUrl(url);
    } <span style="color:#c678dd">catch</span> (NullPointerException e) {
        e.printStackTrace();
    }
}
</code></span></span>

八,JSBridge

相信很多都或多或少的了解JsBridge,不管是iOS平台还是Android平台,特别是混合应用,肯定是要用的JsBridge这个机制来建立Native和Web端的通信。

本部分简单阐述下JsBridge原理,以及分析两个实际案例。

JsBridge介绍:

JSBridge我们可以比喻成一座桥或者一根管道,一端是Web一端是Native。我们搭建这个通道的目的就是让Native和Web之间互相调用更为方便统一和简洁
.JSBridge做得好的一个典型就是微信,微信给开发者提供了JSSDK,该SDK中暴露了很多微信原生层的方法,比如支付,定位等。使用起来非常方便。

JsBridge原理:

前面我们分析了WebView如何于JavaScript交互的,JSBridge就是在这些基础之上做扩展使用支持更复杂的功能,三种形式两种原理分析如下:

1,使用addJavascriptInterface

原理:这是Android提供的Js与Native通信的官方解决方案,将java对象注入到Js中直接作为window的某一变量来使用。

2,WebViewClient中shouldOverrideUrlLoading(WebView视图,String url)。

利用方案iframe机制,只要有iframe加载,shouldOverrideUrlLoading方法就会有回调。可以构造一个特殊格式的url,使用shouldOverrideUrlLoading方法拦截url,根据解析url来之行native方法逻辑。

3,利用WebChromeClient中的onJsAlert,onJsConfirm,onJsPrompt提示接口,同样也是拦截操作。

利用js调用window对象的对应的方法,即window.alert,window.confirm,window.prompt,WebChromeClient对象中的三个方法onJsAlert,onJsConfirm,onJsPrompt就会被触发,有了js到native的通道,那么我们就可以制定协议来约束对方。最终我们选择使用提示方法,onJsPrompt()方法的消息参数的值正是Js的方法window.prompt()的消息的值。

汇总:后面两种虽然形式不同,但是原理是相同的,都是对url或者参数做文章,通过制定参数协议,不管是url还是消息,到本机拦截处理.native调用Js只有一种方式,就是使用loadUrl(js),js为在web端定义好的javascript函数。

以上就是所有JsBridge的原理,自己可以写给demo跑一下,下面看几个问题:

1,如何避免JS,Android,iOS相互调用时,需要事先“约定”方法名称和参数?
2,原生调用JS方法,能否类似原生开发一样,使用Callback(block)做为回调方式?
3,JS调用原生能否使用功能获得返回值?

问题来源于网络,基本上都是这几个疑问,下面我们带着疑问去分析三个个方案。

JsBridge案例:

一,H5与Native交互之JSBridge技术

本案例为有赞技术团队博客分享的H5与原生交互之JSBridge技术,并没有最终完全的代码,不过很清楚的分析了IOS和Android的与Java脚本的底层交互原理。

1,实现原理

通过模式方式,使用shouldOverrideUrlLoading方法对URL协议进行解析。

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code><span style="color:#c678dd">var</span> url = <span style="color:#98c379">'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com'</span>;  
<span style="color:#c678dd">var</span> iframe = <span style="color:#e6c07b">document</span>.createElement(<span style="color:#98c379">'iframe'</span>);  
iframe.style.width = <span style="color:#98c379">'1px'</span>;  
iframe.style.height = <span style="color:#98c379">'1px'</span>;  
iframe.style.display = <span style="color:#98c379">'none'</span>;  
iframe.src = url;  
<span style="color:#e6c07b">document</span>.body.appendChild(iframe);  
setTimeout(<span style="color:#c678dd">function</span>() {  
    iframe.remove();
}, <span style="color:#d19a66">100</span>);
</code></span></span>

可以到看有赞技术是通过自定义url协议来作为传输媒介,这样Android就可以拦截这个请求,从而解析出相应的方法和参数

不过此url中的参数是以键值对的方式传递,我是建议使用Json作为传输参数比较好,灵活清楚。

2,库的封装

有赞将Js与Native通讯封装了一个通用的方法,我这里直接复制过来分析下:

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code>YouzanJsBridge = {  
    <span style="color:#d19a66">doCall</span>: <span style="color:#c678dd">function</span>(functionName, data, callback) {
        <span style="color:#c678dd">var</span> _this = <span style="color:#c678dd">this</span>;
        <span style="color:#929292">// 解决连续调用问题</span>
        <span style="color:#c678dd">if</span> (<span style="color:#c678dd">this</span>.lastCallTime && (<span style="color:#e6c07b">Date</span>.now() - <span style="color:#c678dd">this</span>.lastCallTime) < <span style="color:#d19a66">100</span>) {
            setTimeout(<span style="color:#c678dd">function</span>() {
                _this.doCall(functionName, data, callback);
            }, <span style="color:#d19a66">100</span>);
            <span style="color:#c678dd">return</span>;
        }
        <span style="color:#c678dd">this</span>.lastCallTime = <span style="color:#e6c07b">Date</span>.now();

        data = data || {};
        <span style="color:#c678dd">if</span> (callback) {
            $.extend(data, { <span style="color:#d19a66">callback</span>: callback });
        }

        <span style="color:#c678dd">if</span> (UA.isIOS()) {
            $.each(data, <span style="color:#c678dd">function</span>(key, value) {
                <span style="color:#c678dd">if</span> ($.isPlainObject(value) || $.isArray(value)) {
                    data[key] = <span style="color:#e6c07b">JSON</span>.stringify(value);
                }
            });
            <span style="color:#c678dd">var</span> url = Args.addParameter(<span style="color:#98c379">'youzanjs://'</span> + functionName, data);
            <span style="color:#c678dd">var</span> iframe = <span style="color:#e6c07b">document</span>.createElement(<span style="color:#98c379">'iframe'</span>);
            iframe.style.width = <span style="color:#98c379">'1px'</span>;
            iframe.style.height = <span style="color:#98c379">'1px'</span>;
            iframe.style.display = <span style="color:#98c379">'none'</span>;
            iframe.src = url;
            <span style="color:#e6c07b">document</span>.body.appendChild(iframe);
            setTimeout(<span style="color:#c678dd">function</span>() {
                iframe.remove();
            }, <span style="color:#d19a66">100</span>);
        } <span style="color:#c678dd">else</span> <span style="color:#c678dd">if</span> (UA.isAndroid()) {
            <span style="color:#e6c07b">window</span>.androidJS && <span style="color:#e6c07b">window</span>.androidJS[functionName] && <span style="color:#e6c07b">window</span>.androidJS[functionName](<span style="color:#e6c07b">JSON</span>.stringify(data));
        } <span style="color:#c678dd">else</span> {
            <span style="color:#e6c07b">console</span>.error(<span style="color:#98c379">'未获取platform信息,调取api失败'</span>);
        }
    }
}
</code></span></span>

这样不管是和iOS通信还是Android,都只需要调用YouzanJsBridge.doCall()方法即可,讲两个平台的不同屏蔽在封装基础上,这样也有利于Web端代码的整洁和代码兼容。

当然这里的Android平台是没有回调回调的,如果你想实现两端互调的机制,请参考下一个案例,里面会详细介绍这部分。

3,一些优化

将项目通用方法抽象

例如:

1.getData(数据类型,回调,额外)H5从Native APP获取数据
使用场景:H5需要从Native APP获取某些数据的时候,可以调用这个方法。

2.putData(数据类型,数据)H5告诉Native APP一些数据
使用场景:H5告诉Native APP一些数据,可以调用这个方法。

3.gotoWebview(网址,页面,数据)原生APP新开一个Webview窗口,并打开相应网页

4.doAction(action,data)功能上的一些操作

等等其他方法,我相信如果你自己写过native和js调用demo,上面抽象出来的方法并不陌生,所以,如果你的业务没有那么复杂,没有像微信那样,需要提供给数以万计开发者去用去扩展,这种抽象出一些通用方法的方式不是为一种节省成本,快速迭代,方便的方式。

小结:总之万变不离其宗,所有封装或者框架的东西,使用的东西都还是最基本的方法,只是对基础做一个什么样程度扩展,或深或浅,唯一的只要用着舒服就行。

二:JsBridge

如图1所示,使用方法:

注意Android和Web使用方式

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code>webView.registerHandler(<span style="color:#98c379">"submitFromWeb"</span>, <span style="color:#c678dd">new</span> BridgeHandler() {

    @Override
    public <span style="color:#c678dd">void</span> handler(<span style="color:#e6c07b">String</span> data, CallBackFunction <span style="color:#c678dd">function</span>) {
      <span style="color:#c678dd">function</span>.<span style="color:#61aeee">onCallBack</span>(<span style="color:#98c379">"native submitFromWeb 方法, 返回 data"</span>);
    }
  });

  <span style="color:#61aeee">webView</span>.<span style="color:#61aeee">callHandler</span>(<span style="color:#98c379">"functionInJs"</span>, new Gson().<span style="color:#61aeee">toJson</span>(user), <span style="color:#61aeee">new</span> <span style="color:#61aeee">CallBackFunction</span>() {
    @Override
    public <span style="color:#c678dd">void</span> onCallBack(<span style="color:#e6c07b">String</span> data) {
      Log.i(TAG, <span style="color:#98c379">"onCallBack : "</span> + data);
    }
  });

  <span style="color:#e6c07b">window</span>.WebViewJavascriptBridge.callHandler(
      <span style="color:#98c379">'submitFromWeb'</span>
      , {<span style="color:#98c379">'param'</span>: <span style="color:#98c379">'中文测试'</span>}
            , <span style="color:#c678dd">function</span>(responseData) {
    <span style="color:#e6c07b">document</span>.getElementById(<span style="color:#98c379">"show"</span>).innerHTML = <span style="color:#98c379">"send get responseData from Android 端, data = "</span> + responseData
  }
        );

  bridge.registerHandler(<span style="color:#98c379">"functionInJs"</span>, <span style="color:#c678dd">function</span>(data, responseCallback) {
    <span style="color:#e6c07b">document</span>.getElementById(<span style="color:#98c379">"show"</span>).innerHTML = (<span style="color:#98c379">"data from Java: = "</span> + data);
    <span style="color:#c678dd">var</span> responseData = <span style="color:#98c379">"call functionInJs success,return android!"</span>;
    responseCallback(responseData);
  });
</code></span></span>

2,关键类作用:

在讲JsBridge的实现之前,首先要讲下各个文件的作用

  • Message.java:JsBridge中的消息对象,用来封装native与js交互时的json数据,包含:callid,responseid,responseData,handlerName等字段。
  • WebViewJavascriptBridge.js:改JS文件会被注入到各个页面,和本机中封装处理邮件消息逻辑类似,同样提供了初始化,注册处理程序,调用处理程序等方法,之后JS都是通过此文件中的方法和本地统一沟通。
  • WebViewJavascriptBridge.java:native端,Bridge接口类,定义了发送信息的方法,由BridgeWebView来实现,之后调用可以通过webview.send()方式调用。
  • BridgeWebView.java:WebView的子类,实现了WebViewJavascriptBridge接口,并提供了注册,调用Handler等方法,之后都是通过webview.registerHandler()或者webview.callHandler()方式调用js。
  • BridgeWebViewClient.java:WebViewClient的子类,重写了ShouldOverrideUrlLoading,onPageFinish,onPageStart等方法。
  • BridgeHandler.java:作为本地与网络交互的通道.native通过处理程序的名称来找到响应的处理程序来操作,这样才能实现JS回调给本地端,也就是registerHandler时候注册的监听。
  • CallBackFunction.java:native定义的回调函数,Handler处理完成后,用来给Js发送消息,对应处理js中函数。

3,实现原理:

看完这些主要类的作用,在看流程就清晰了。

本地调js:

1,WebView.callHandler( 'handlerName', '{}',回调); 
2,doSend中组装要传输的消息对象,并设置setCallbackId(生成唯一的id是为了方便在js回调回来的时候在android端查找对于的回调)
3,dispatchMessage javascript:WebViewJavascriptBridge._handleMessageFromNative(message);方法消息为上一步的Message对象对应的Json数据
.4,在Js _dispatchMessageFromNative函数中,根据messageJSON json数据中的字段handlerName找到对对应的处理程序方法执行.handler 
= messageHandlers [message.handlerName]; handler这个就是在html中registerHandler注册的回调函数(data,responseCallback)函数
5,之后执行html中responseCallback(responseData); 会触发WebViewJavascriptBridge.js文件中的_doSend函数,_doSend通过messagingIframe.src形式传给android端,shouldOverrideUrlLoading接受,并拦截内容处理。
6,第一次:拦截执行到webView.flushMessageQueue()方法,并调用responseCallbacks.put(jsUrl,returnCallback);,同时调用javascript:WebViewJavascriptBridge._fetchQueue(); 来自查询消息的返回值,并执行一次messagingIframe.src.7 
,第二次:拦截执行到webView.handlerReturnData()方法,并调用上一步注册的CallBackFunction.onCallBack,然后根据responseId即android传过来的消息。 callbackId,找到使用者注册的CallBackFunction回调。

本次的流程就走完了,看几十遍就看懂了哈。

Js - > android:

1,window.WebViewJavascriptBridge.callHandler 
2,执行Js函数_doSend()触发messagingIframe.src 
3,android端拦截,shouldOverrideUrlLoading 
4,第一次:拦截执行到webView.flushMessageQueue()方法,并调用responseCallbacks.put(jsUrl, returnCallback);,同时调用javascript:WebViewJavascriptBridge._fetchQueue(); 来自查询消息的返回值,并执行一次messagingIframe.src.5 
,第二次:拦截执行到webView.handlerReturnData()方法,并调用上一步注册的CallBackFunction.onCallBack - > responseId为空 - > handler.handler(来自) m.getData(),responseFunction) - >外部回调(function.onCallBack(“native submitFromWeb方法,返回数据”);) - > queueMessage - > dispatchMessage - > loadUrl(javascriptCommand) - >回调结束

注意两端的每一次通讯,Android端的shouldOverrideUrlLoading方法都会执行两次,但是携带url不同,这里要注意两次访问是相互配合的,没有第一次的消息查询,也就不会有第二次的数据返回回调。

4,小结

通过URL拦截方式,注入一个本地的js文件,来桥接本地和网络,屏蔽了一些通性工作,在使用上方式相同,好理解,通过两次来回调用实现了可回调函数。

github上也有很多类似的方案,这里就不一见分析了,如果你不是看的这个库,建议好好看看,挺巧妙的机制。

这块逻辑也是看了好久,挺绕的,可以结合打日志和调试来分析。

三:DSBridge-Android

1,使用方法

注意Android和Web使用方式

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code>    webView.callHandler(<span style="color:#98c379">"addValue"</span>,<span style="color:#c678dd">new</span> <span style="color:#e6c07b">Object</span>[]{<span style="color:#d19a66">1</span>,<span style="color:#98c379">"hello"</span>},<span style="color:#c678dd">new</span> CallBackFunction(){
        @Override
        public <span style="color:#c678dd">void</span> onCallBack(<span style="color:#e6c07b">String</span> retValue) {
            Log.d(<span style="color:#98c379">"jsbridge"</span>,<span style="color:#98c379">"call succeed,return value is "</span>+retValue);
        }
    });
   webView.callHandler(<span style="color:#98c379">"test"</span>,<span style="color:#56b6c2">null</span>);

   dsBridge.call(<span style="color:#98c379">"testNever"</span>, {<span style="color:#d19a66">msg</span>: <span style="color:#98c379">"testSyn"</span>});

   dsBridge.call(<span style="color:#98c379">"testNoArgAsyn"</span>, <span style="color:#c678dd">function</span> (v) {
       alert(v);
   });
</code></span></span>

2,关键类作用:

  • DWebView:WebView的子类,提供了注册,调用Handler等方法,之后都是通过webview.callHandler()方式调用js。
  • CompletionHandler:作为本地与网络交互的通道,异步回调的关键操作类。
  • OnReturnValue:js回调android的接口,根据handlerMap储存的id作为handler标标即OnReturnValue实现类。
  • JsApi:需要注册到Js java类。

看完这些主要类的作用,在看流程就清晰了。

3,实现原理:

本质还是使用的系统默认addJavascriptInterface方式,其中做了扩展。

首先需要将这段js注入到网页面

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code><span style="color:#c678dd">function</span> <span style="color:#61aeee">getJsBridge</span>() {
    <span style="color:#e6c07b">window</span>._dsf = <span style="color:#e6c07b">window</span>._dsf || {};
    <span style="color:#c678dd">return</span> {
        <span style="color:#d19a66">call</span>: <span style="color:#c678dd">function</span>(b, a, c) {
            <span style="color:#98c379">"function"</span> == <span style="color:#c678dd">typeof</span> a && (c = a, a = {});
            <span style="color:#c678dd">if</span> (<span style="color:#98c379">"function"</span> == <span style="color:#c678dd">typeof</span> c) {
                <span style="color:#e6c07b">window</span>.dscb = <span style="color:#e6c07b">window</span>.dscb || <span style="color:#d19a66">0</span>;
                <span style="color:#c678dd">var</span> d = <span style="color:#98c379">"dscb"</span> + <span style="color:#e6c07b">window</span>.dscb++;
                <span style="color:#e6c07b">window</span>[d] = c;
                a._dscbstub = d
            }
            a = <span style="color:#e6c07b">JSON</span>.stringify(a || {});
            <span style="color:#c678dd">return</span> <span style="color:#e6c07b">window</span>._dswk ? prompt(<span style="color:#e6c07b">window</span>._dswk + b, a) : <span style="color:#98c379">"function"</span> == <span style="color:#c678dd">typeof</span> _dsbridge ? _dsbridge(b, a) : _dsbridge.call(b, a)
        },
        <span style="color:#d19a66">register</span>: <span style="color:#c678dd">function</span>(b, a) {
            <span style="color:#98c379">"object"</span> == <span style="color:#c678dd">typeof</span> b ? <span style="color:#e6c07b">Object</span>.assign(<span style="color:#e6c07b">window</span>._dsf, b) : <span style="color:#e6c07b">window</span>._dsf[b] = a
        }
    }
}

dsBridge = getJsBridge();
</code></span></span>

native - > js:

1,webview.callHandler(方法,args,处理程序); 
2,拼接Js,并生成唯一的callID,将处理程序保存到map中; 
3,执行js,同时回调java returnValue(),然后回调使用者调用。

js - > android:

1,dsBridge.call(方法,json,函数) - > native call(methodName,args); 
2,通过反射来查找methodName对应的方法,并注册CompletionHandler; 
3,JsApi通过handler.complete()方法,并拼装js方法和参数,再次调用js函数实现回调操作,并删除上一次的回调id。

4,小结:

这个方案比较简单,使用了系统的注入方式,虽然4.2以下存在漏洞,但是它里面只能反射包含@JavascriptInterface注解的方法,所以和4.2以上注入是一样的,也是安全的。

相对案例一,案例二实现方式比较简单粗暴,也比较容易懂。

不过这个库在我的4.2设备上有bug,下面函数执行时找不到dsBridge对象,导致Native调用Js失败。

<span style="color:#2f2f2f"><span style="color:#abb2bf"><code> dsBridge.register(<span style="color:#98c379">'addValue'</span>,<span style="color:#c678dd">function</span>(l,r){
     <span style="color:#c678dd">return</span> l+r;
 })
</code></span></span>

可能和内核执行有关系,我这做了修复,放到功能函数中执行就可以了,如果你也遇到,可以参考我的修复方法DSBridge-Android


九,WebView缓存原理分析和应用

这部分内容可以参考这两篇博文:

写的已经很清楚了,我这里就不赘述了。

主要包含一下内容:

1,WebView的5中缓存类型,以及每个缓存类型工作原理,相同点和不同点,
.2,缓存在手机上的存储
.3,每种缓存机制案例。

如果你想通过过滤来减缓WebView请求网络,可以参考rexxar-android中关于拦截url操作读取本地操作。


十,性能,体验分析与优化

参考美团:WebView性能,体验分析与优化

这部分美团的技术博客已经写的很好了,不仅从性能,内存消耗,体验,安全几个维度,来系统的分析客户端默认WebView的问题,还给出了对应的优化方案。

我的感受:文章中也提到了QQ的混合架构演进,主要的优化方向和内容和下面的混合开源框架VasSonic基本一直,当然都是腾讯东东,应该是有所借鉴的。而且关于WebView的优化也就是那几部分,串行该并行,缓存WebView,客户端代替WebView网络请求,WebView拦截url加载本地资源,还有Web端(cdn神马的,哈哈,不太熟悉)等等几个主要方面,但是能将上数几个方面都完美的结合到一起的市面上很少,VasSonic就做到了。

十一,Hybrid开源框架

1,VasSonic

VasSonic是腾讯出品的一个轻量级的高性能的混合框架,专注于提升页面首屏加载速度,完美支持静态直出页面和动态直出页面,兼容离线包等方案。

我的感受:比豆瓣的框架复杂很多,不是一个量级的,可能和业务也相关吧,毕竟鹅厂用户很多服务也很多啊我也是断断续续看了很久,逻辑还是挺复杂的,不过思路挺清晰,就是能利用上的资源统统要利用,不能让CPU或者网络空等,主要是在WebView和加载之间做一个完美的桥接,让内容不管在什么情况下都能衔接自如。不过,其中并不包含JsBridge部分内容。

VasSonic / wiki wiki写的也特别清楚,值得好好去研究,另外这个库的QQ群也特别活跃,里面有负责维护的同学帮忙解答疑问,值得点赞!

辛辛苦苦开源了,作为平时想提高WebView访问速度的朋友肯定不会错过这么好的内容,强烈建议大家去阅读源码,库更新也很快。

2,rexxar-android

rexxar-android是豆瓣的混合开发框架,包含Web,iOS,Android三端,Android部分主要内容围绕路由表,容器,缓存。其他部分可以阅读豆瓣的混合开发框架 - Rexxar更详细介绍。

我的感觉:rexxar-android在缓存部分还是值得学习的,缓存Cache分为两部分:本地预装,本地文件缓存。其中将每一个需要Native完成的功能抽象成一个widget,通过制定url协议,过滤并解析URL的携带的参数。

有兴趣的可以下载源码学习。

Rexxar Android系列学习其他文章


十二,完整dmeo

Android_WebView


十三,参考

猜你喜欢

转载自blog.csdn.net/MLQ8087/article/details/86164216