Android WebView 总结

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

一、简介

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

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

二、基本使用

//配置网络权限
<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的携带的参数。



作者:無名小子的杂货铺
链接:https://www.jianshu.com/p/fd61e8f4049e
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

猜你喜欢

转载自blog.csdn.net/weixin_41205419/article/details/79974660