Android WebView 全面干货指南

阅读 3025
收藏 149
2017-12-15
原文链接:www.jianshu.com

前言

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


目录

目录

一、简介

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

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

官方 WebView.html
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.


二、基本使用

//配置网络权限
<uses-permission android:name="android.permission.INTERNET"/>

 //布局
<?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:id="@+id/main"
      android:layout_width="match_parent"
      android:layout_height="match_parent">

<com.webview.SafeWebView
    android:id="@+id/web_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
</LinearLayout>

  // BaseWebView 是我自己封装的 WebView 

  //实际使用时请采用 new 的方式
  mWebView = (SafeWebView) findViewById(R.id.web_view);
  mWebView.addJavascriptInterface(new NativeInterface(this), "AndroidNative");
  mWebView.loadUrl("http://www.jianshu.com/u/fa272f63280a");

三、WebView 方法

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

常用方法

  • void loadUrl(String url):加载网络链接 url
  • boolean canGoBack():判断 WebView 当前是否可以返回上一页
  • goBack():回退到上一页
  • boolean canGoForward():判断 WebView 当前是否可以向前一页
  • goForward():回退到前一页
  • onPause():类似 Activity 生命周期,页面进入后台不可见状态
  • pauseTimers():该方法面向全局整个应用程序的webview,它会暂停所有webview的layout,parsing,JavaScript Timer。当程序进入后台时,该方法的调用可以降低CPU功耗。
  • onResume():在调用 onPause()后,可以调用该方法来恢复 WebView 的运行。
  • resumeTimers():恢复pauseTimers时的所有操作。(注:pauseTimers和resumeTimers 方法必须一起使用,否则再使用其它场景下的 WebView 会有问题)
  • destroy():销毁 WebView
  • clearHistory():清除当前 WebView 访问的历史记录。
  • clearCache(boolean includeDiskFiles):清空网页访问留下的缓存数据。需要注意的时,由于缓存是全局的,所以只要是WebView用到的缓存都会被清空,即便其他地方也会使用到。该方法接受一个参数,从命名即可看出作用。若设为false,则只清空内存里的资源缓存,而不清空磁盘里的。
  • reload():重新加载当前请求
  • setLayerType(int layerType, Paint paint):设置硬件加速、软件加速
  • removeAllViews():清除子view。
  • clearSslPreferences():清除ssl信息。
  • clearMatches():清除网页查找的高亮匹配字符。
  • removeJavascriptInterface(String interfaceName):删除interfaceName 对应的注入对象
  • addJavascriptInterface(Object object,String interfaceName):注入 java 对象。
  • setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled):设置垂直方向滚动条。
  • setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled):设置横向滚动条。
  • 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():清除自动完成填充的表单数据。需要注意的是,该方法仅仅清除当前表单域自动完成填充的表单数据,并不会清除WebView存储到本地的数据。

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

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

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

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

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

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

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

一份 WebView 方法使用清单

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

四、常用属性

主要包含三部分: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):Sets the text zoom of the page in percent. The default is 100。
  • setAllowFileAccess(boolean allow):是否允许加载本地 html 文件/false。
  • setDatabaseEnabled(boolean flag):是否开启数据库缓存
  • setDomStorageEnabled(boolean flag):是否开启DOM缓存。
  • setUserAgentString(String ua):设置 UserAgent 属性。
  • setLoadsImagesAutomatically(boolean flag):支持自动加载图片
  • setAllowFileAccessFromFileURLs(boolean flag::允许通过 file url 加载的 Javascript 读取其他的本地文件,Android 4.1 之前默认是true,在 Android 4.1 及以后默认是false,也就是禁止。
  • setAllowUniversalAccessFromFileURLs(boolean flag):允许通过 file url 加载的 Javascript 可以访问其他的源,包括其他的文件和 http,https 等其他的源,Android 4.1 之前默认是true,在 Android 4.1 及以后默认是false,也就是禁止如果此设置是允许,则 setAllowFileAccessFromFileURLs 不起做用。
  • boolean getLoadsImagesAutomatically():是否支持自动加载图片。
一份使用清单
    WebSettings webSettings = mWebView.getSettings();
    if (webSettings == null) return;
    // 支持 Js 使用
    webSettings.setJavaScriptEnabled(true);
    // 开启DOM缓存
    webSettings.setDomStorageEnabled(true);
    // 开启数据库缓存
    webSettings.setDatabaseEnabled(true);
    // 支持自动加载图片
    webSettings.setLoadsImagesAutomatically(hasKitkat());
    // 设置 WebView 的缓存模式
    webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
    // 支持启用缓存模式
    webSettings.setAppCacheEnabled(true);
    // 设置 AppCache 最大缓存值(现在官方已经不提倡使用,已废弃)
    webSettings.setAppCacheMaxSize(8 * 1024 * 1024);
    // Android 私有缓存存储,如果你不调用setAppCachePath方法,WebView将不会产生这个目录
    webSettings.setAppCachePath(getCacheDir().getAbsolutePath());
    // 数据库路径
    if (!hasKitkat()) {
        webSettings.setDatabasePath(getDatabasePath("html").getPath());
    }
    // 关闭密码保存提醒功能
    webSettings.setSavePassword(false);
    // 支持缩放
    webSettings.setSupportZoom(true);
    // 设置 UserAgent 属性
    webSettings.setUserAgentString("");
    // 允许加载本地 html 文件/false
    webSettings.setAllowFileAccess(true);
    // 允许通过 file url 加载的 Javascript 读取其他的本地文件,Android 4.1 之前默认是true,在 Android 4.1 及以后默认是false,也就是禁止
    webSettings.setAllowFileAccessFromFileURLs(false);
    // 允许通过 file url 加载的 Javascript 可以访问其他的源,包括其他的文件和 http,https 等其他的源,
    // Android 4.1 之前默认是true,在 Android 4.1 及以后默认是false,也就是禁止
    // 如果此设置是允许,则 setAllowFileAccessFromFileURLs 不起做用
    webSettings.setAllowUniversalAccessFromFileURLs(false);

WebViewClient

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

    /**
     * 当WebView得页面Scale值发生改变时回调
     */
    @Override
    public void onScaleChanged(WebView view, float oldScale, float newScale) {
        super.onScaleChanged(view, oldScale, newScale);
    }

    /**
     * 是否在 WebView 内加载页面
     *
     * @param view
     * @param url
     * @return
     */
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        view.loadUrl(url);
        return true;
    }

    /**
     * WebView 开始加载页面时回调,一次Frame加载对应一次回调
     *
     * @param view
     * @param url
     * @param favicon
     */
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);
    }

    /**
     * WebView 完成加载页面时回调,一次Frame加载对应一次回调
     *
     * @param view
     * @param url
     */
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
    }

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

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

    /**
     * WebView 访问 url 出错
     *
     * @param view
     * @param request
     * @param error
     */
    @Override
    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
        super.onReceivedError(view, request, error);
    }

    /**
     * WebView ssl 访问证书出错,handler.cancel()取消加载,handler.proceed()对然错误也继续加载
     *
     * @param view
     * @param handler
     * @param error
     */
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        super.onReceivedSslError(view, handler, error);
    }
}

WebChromeClient

1、常用方法
  • onConsoleMessage(String message, int lineNumber,String sourceID):输出 Web 端日志。
  • onProgressChanged(WebView view, int newProgress):当前 WebView 加载网页进度。
  • onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result):处理 JS 中的 Prompt对话框
  • onJsAlert(WebView view, String url, String message, JsResult result): Js 中调用 alert() 函数,产生的对话框。
  • onReceivedTitle(WebView view, String title):接收web页面的 Title。
  • onReceivedIcon(WebView view, Bitmap icon):接收web页面的icon。
2、一份使用清单
public class SafeWebChromeClient extends WebChromeClient {

    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
        return super.onConsoleMessage(consoleMessage);
    }

    /**
     * 当前 WebView 加载网页进度
     *
     * @param view
     * @param newProgress
     */
    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        super.onProgressChanged(view, newProgress);
    }

    /**
     * Js 中调用 alert() 函数,产生的对话框
     *
     * @param view
     * @param url
     * @param message
     * @param result
     * @return
     */
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
        return super.onJsAlert(view, url, message, result);
    }

    /**
     * 处理 Js 中的 Confirm 对话框
     *
     * @param view
     * @param url
     * @param message
     * @param result
     * @return
     */
    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
        return super.onJsConfirm(view, url, message, result);
    }

    /**
     * 处理 JS 中的 Prompt对话框
     *
     * @param view
     * @param url
     * @param message
     * @param defaultValue
     * @param result
     * @return
     */
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        return super.onJsPrompt(view, url, message, defaultValue, result);
    }

    /**
     * 接收web页面的icon
     *
     * @param view
     * @param icon
     */
    @Override
    public void onReceivedIcon(WebView view, Bitmap icon) {
        super.onReceivedIcon(view, icon);
    }

    /**
     * 接收web页面的 Title
     *
     * @param view
     * @param title
     */
    @Override
    public void onReceivedTitle(WebView view, String title) {
        super.onReceivedTitle(view, title);
    }

}

五、于 JavaScript 交互

介绍

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

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

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

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

使用清单:

//开启Js可用
mWebView.getSettings().setJavaScriptEnabled(true);

// 创建要注入的 Java 类
public class NativeInterface {

    private Context mContext;

    public NativeInterface(Context context) {
        mContext = context;
    }

    @JavascriptInterface
    public void hello() {
        Toast.makeText(mContext, "hello", Toast.LENGTH_SHORT).show();
    }

    @JavascriptInterface
    public void hello(String params) {
        Toast.makeText(mContext, params, Toast.LENGTH_SHORT).show();
    }

    @JavascriptInterface
    public String getAndroid() {
        Toast.makeText(mContext, "getAndroid", Toast.LENGTH_SHORT).show();
        return "Android data";
    }

}

// WebView 注入即可
mWebView.addJavascriptInterface(new NativeInterface(this), "AndroidNative");

//Js编写
<script>
    function callHello(){
        AndroidNative.hello();
    }

    function callHello1(){
        AndroidNative.hello('hello Android');
    }

    function callAndroid(){
        var temp = AndroidNative.getAndroid();
        console.log(temp);
        alert(temp);
    }  

</script>

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 Object 可能被利用,实现远程任意代码;
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 方法将这两个对象删除。

       super.removeJavascriptInterface("searchBoxJavaBridge_");
       super.removeJavascriptInterface("accessibility");
       super.removeJavascriptInterface("accessibilityTraversal");

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

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

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

setAllowFileAccess(true);//设置为 false 将不能加载本地 html 文件
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
if (url.startsWith("file://") {
    setJavaScriptEnabled(false);
} else {
    setJavaScriptEnabled(true);
}

安全修复案例

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


七、一些坑

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

1、android.webkit.AccessibilityInjector$TextToSpeechWrapper

java.lang.NullPointerException
    at android.webkit.AccessibilityInjector$TextToSpeechWrapper$1.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$TextToSpeechWrapper.<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$MethodAndArgsCaller.run(ZygoteInit.java:833)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:600)
    at dalvik.system.NativeStart.main(Native Method)

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

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

public static void disableAccessibility(Context context) {
        if (Build.VERSION.SDK_INT == 17/*4.2 (Build.VERSION_CODES.JELLY_BEAN_MR1)*/) {
            if (context != null) {
                try {
                    AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
                    if (!am.isEnabled()) {
                        //Not need to disable accessibility
                        return;
                    }

                    Method setState = am.getClass().getDeclaredMethod("setState", int.class);
                    setState.setAccessible(true);
                    setState.invoke(am, 0);/**{@link AccessibilityManager#STATE_FLAG_ACCESSIBILITY_ENABLED}*/
                } catch (Exception ignored) {
                    ignored.printStackTrace();
                }
            }
        }
    }

2、android.content.pm.PackageManager$NameNotFoundException

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.<init>(View.java:3588)
    at android.view.View.<init>(View.java:3682)
    at android.view.ViewGroup.<init>(ViewGroup.java:497)
    at android.widget.AbsoluteLayout.<init>(AbsoluteLayout.java:55)
    at android.webkit.WebView.<init>(WebView.java:544)
    at android.webkit.WebView.<init>(WebView.java:489)
    at android.webkit.WebView.<init>(WebView.java:472)
    at android.webkit.WebView.<init>(WebView.java:459)
    at android.webkit.WebView.<init>(WebView.java:449)

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

修复方法:

try {
    super.setOverScrollMode(mode);
} catch (Throwable e) {
    e.printStackTrace();
}  

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

try{
    super.setOverScrollMode(mode);
} catch(Throwable e){
        String messageCause = e.getCause() == null ? e.toString() : e.getCause().toString();
String trace = Log.getStackTraceString(e);
if (trace.contains("android.content.pm.PackageManager$NameNotFoundException")
  || trace.contains("java.lang.RuntimeException: Cannot load WebView")
    || trace.contains("android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed")) {
      e.printStackTrace();
    }else{
      throw e;
    }        
}

3、android.webkit.WebViewClassic.clearView

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$1900(WebViewCore.java:57)
System.err:     at android.webkit.WebViewCore$EventHub$1.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$WebCoreThread.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$1900(WebViewCore.java:57)
System.err:     at android.webkit.WebViewCore$EventHub$1.handleMessage(WebViewCore.java:1303)
System.err:     at android.os.Handler.dispatchMessage(Handler.java:99)
System.err:     at android.os.Looper.loop(Looper.java:137)

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

修复方法:

public void logdUrl(final String url) {
    try {
        super.loadUrl(url);
    } catch (NullPointerException e) {
        e.printStackTrace();
    }
}

八、JSBridge

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

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

JsBridge 介绍:

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

JsBridge 原理:

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

1、使用 addJavascriptInterface

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

2、WebViewClient 中 shouldOverrideUrlLoading (WebView view, String url)。

利用 scheme 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的通道,那么我们就可以制定协议来约束对方。最终我们选择使用 prompt 方法,onJsPrompt()方法的message参数的值正是Js的方法window.prompt()的message的值。

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

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

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

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

JsBridge 案例:

一、 H5与Native交互之JSBridge技术

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

1、实现原理

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

var url = 'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';  
var iframe = document.createElement('iframe');  
iframe.style.width = '1px';  
iframe.style.height = '1px';  
iframe.style.display = 'none';  
iframe.src = url;  
document.body.appendChild(iframe);  
setTimeout(function() {  
    iframe.remove();
}, 100);

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

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

2、库的封装

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

YouzanJsBridge = {  
    doCall: function(functionName, data, callback) {
        var _this = this;
        // 解决连续调用问题
        if (this.lastCallTime && (Date.now() - this.lastCallTime) < 100) {
            setTimeout(function() {
                _this.doCall(functionName, data, callback);
            }, 100);
            return;
        }
        this.lastCallTime = Date.now();

        data = data || {};
        if (callback) {
            $.extend(data, { callback: callback });
        }

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

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

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

3、一些优化

将项目通用方法抽象

例如:

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

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

3.gotoWebview(url, page, data) Native APP新开一个Webview窗口,并打开相应网页

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

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

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

二:JsBridge

1、使用方法:

注意Android 和 Web 使用方式

webView.registerHandler("submitFromWeb", new BridgeHandler() {

    @Override
    public void handler(String data, CallBackFunction function) {
      function.onCallBack("native submitFromWeb 方法, 返回 data");
    }
  });

  webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() {
    @Override
    public void onCallBack(String data) {
      Log.i(TAG, "onCallBack : " + data);
    }
  });

  window.WebViewJavascriptBridge.callHandler(
      'submitFromWeb'
      , {'param': '中文测试'}
            , function(responseData) {
    document.getElementById("show").innerHTML = "send get responseData from Android 端, data = " + responseData
  }
        );

  bridge.registerHandler("functionInJs", function(data, responseCallback) {
    document.getElementById("show").innerHTML = ("data from Java: = " + data);
    var responseData = "call functionInJs success,return android!";
    responseCallback(responseData);
  });

2、关键类作用:

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

  • Message.java:JsBridge 中的消息对象,用来封装native与js交互时的json数据,包含:callid、responseid、responseData、handlerName等字段。
  • WebViewJavascriptBridge.js:改js文件会被注入到各个页面,和native中封装处理Message消息逻辑类似,同样提供了初始化、注册Handler、调用Handler等方法,之后js都是通过此文件中的方法和native统一沟通。
  • 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与web交互的通道。native通过Handler的名称来找到响应的Handler来操作,这样才能实现js回调给native端,也就是registerHandler时候注册的监听。
  • CallBackFunction.java:native定义的回调函数, Handler处理完成后,用来给Js发送消息,对应处理js中function中。

3、实现原理:

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

native 调 js:

1、WebView.callHandler('handlerName','{}',callBack);
2、doSend 中组装 要传输的 Message 对象,并设置setCallbackId(生成唯一的id是为了方便在js回调回来的时候在android端查找对于的 callback)
3、dispatchMessage javascript:WebViewJavascriptBridge._handleMessageFromNative(message);方法 message 为上一步的Message 对象对应的 Json 数据。
4、在 Js _dispatchMessageFromNative 函数中,根据 messageJSON json 数据中的字段 handlerName 找到对对应的 handler 方法执行。
handler = messageHandlers[message.handlerName];handler 这个就是在 html 中 registerHandler 注册的回调function(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 传过来的 message.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 方法, 返回 data");)-> queueMessage -> dispatchMessage ->loadUrl(javascriptCommand) -> 回调结束

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

4、小结

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

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

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

三:DSBridge-Android

1、使用方法

注意Android 和 Web 使用方式

    webView.callHandler("addValue",new Object[]{1,"hello"},new CallBackFunction(){
        @Override
        public void onCallBack(String retValue) {
            Log.d("jsbridge","call succeed,return value is "+retValue);
        }
    });
   webView.callHandler("test",null);

   dsBridge.call("testNever", {msg: "testSyn"});

   dsBridge.call("testNoArgAsyn", function (v) {
       alert(v);
   });

2、关键类作用:

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

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

3、实现原理:

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

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

function getJsBridge() {
    window._dsf = window._dsf || {};
    return {
        call: function(b, a, c) {
            "function" == typeof a && (c = a, a = {});
            if ("function" == typeof c) {
                window.dscb = window.dscb || 0;
                var d = "dscb" + window.dscb++;
                window[d] = c;
                a._dscbstub = d
            }
            a = JSON.stringify(a || {});
            return window._dswk ? prompt(window._dswk + b, a) : "function" == typeof _dsbridge ? _dsbridge(b, a) : _dsbridge.call(b, a)
        },
        register: function(b, a) {
            "object" == typeof b ? Object.assign(window._dsf, b) : window._dsf[b] = a
        }
    }
}

dsBridge = getJsBridge();

native -> js:

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

js -> android:

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

4、小结:

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

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

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

 dsBridge.register('addValue',function(l,r){
     return l+r;
 })

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


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

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

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

主要包含一下内容:

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

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


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

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

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

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

十一、Hybrid 开源框架

1、VasSonic

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

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

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

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

2、rexxar-android

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

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

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

Rexxar Android 系列学习其他文章


十二、完整dmeo

Android_WebView

十三、参考


评论