WebView使用详细介绍

作者: Typist夫少 | 来源:发表于2018-11-19 14:42 被阅读1次

    WebSettings

    对WebView进行配置和管理

    // 如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
    // 若加载的 html 里有JS 在执行动画等操作,会造成资源浪费(CPU、电量)
    // 在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 即可
    webSettings.setJavaScriptEnabled(true);  
    
    // 支持插件
    webSettings.setPluginsEnabled(true); 
    
    // 设置自适应屏幕,两者合用
    webSettings.setUseWideViewPort(true); //将图片调整到适合webview的大小 
    webSettings.setLoadWithOverviewMode(true); // 缩放至屏幕的大小
    
    // 缩放操作
    webSettings.setSupportZoom(true); // 支持缩放,默认为true。是下面那个的前提。
    webSettings.setBuiltInZoomControls(true); // 设置内置的缩放控件。若为false,则该WebView不可缩放
    webSettings.setDisplayZoomControls(false); // 隐藏原生的缩放控件
    
    // 其他细节操作
    webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); // 关闭webview中缓存 
    webSettings.setAllowFileAccess(true); // 设置可以访问文件 
    webSettings.setJavaScriptCanOpenWindowsAutomatically(true); // 支持通过JS打开新窗口 
    webSettings.setLoadsImagesAutomatically(true); // 支持自动加载图片
    webSettings.setDefaultTextEncodingName("utf-8");// 设置编码格式
    

    WebClient

    处理各种通知 & 请求事件

    mWebView.setWebViewClient(new MyWebViewClient());
    
    private class MyWebViewClient extends WebViewClient
    {
      // 复写shouldOverrideUrlLoading()方法,使得打开网页时不调用系统浏览器, 而是在本WebView中显示
      @Override
      public boolean shouldOverrideUrlLoading(WebView view, String url)
      {
    
        // 特定的url调到native 页面进行处理 返回true
        if (LinkHandleUtils.handle(FNWebPageActivity.this, url, true))
        {
          return true;
        }
    
        mCurUrl = url;
        return false;
      }
    
      // 开始载入页面调用的,我们可以设定一个loading的页面,告诉用户程序在等待网络响应。
      @Override
      public void onPageStarted(WebView webView, String s, Bitmap bitmap)
      {
        super.onPageStarted(webView, s, bitmap);
      }
    
      // 在页面加载结束时调用。我们可以关闭loading 条,切换程序动作
      @Override
      public void onPageFinished(WebView webView, String s)
      {
        super.onPageFinished(webView, s);
      }
    
      // 在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次。
      @Override
      public void onLoadResource(WebView webView, String s)
      {
        super.onLoadResource(webView, s);
      }
    
      // 加载页面的服务器出现错误时(如404)调用
      @Override
      public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
      {
        super.onReceivedError(view, errorCode, description, failingUrl);
      }
    
      // 处理https请求
      @Override
      public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError)
      {
        sslErrorHandler.proceed();    // 表示等待证书响应
        // sslErrorHandler.cancel();      // 表示挂起连接,为默认方式
        // sslErrorHandler.handleMessage(null);    // 可做其他处理
      }
    }
    

    WebChromeClient

    辅助 WebView 处理 Javascript 的对话框,网站图标,网站标题等等。

    setWebChromeClient(new MyWebChromeClient());
    
    private class MyWebChromeClient extends WebChromeClient
    {
      // 获得网页的加载进度并显示
      @Override
      public void onProgressChanged(com.tencent.smtt.sdk.WebView webView, int newProgress)
      {
        if (newProgress <= 100 && mProgressBar != null)
        {
          if (GONE == mProgressBar.getVisibility())
          {
            mProgressBar.setVisibility(VISIBLE);
          }
          startProgressAnimation(newProgress);
        }
        super.onProgressChanged(webView, newProgress);
      }
    
      // 获取Web页中的标题
      @Override
      public void onReceivedTitle(WebView webView, String title)
      {
        super.onReceivedTitle(webView, title);
        if (mCallback != null && StringUtils.isNotBlank(title))
        {
          mCallback.setTitle(title);
        }
      }
    
      // 支持javascript的警告框
      @Override
      public boolean onJsAlert(WebView webView, String url, String message, final JsResult result)
      {
        new AlertDialog.Builder(getContext())
                .setTitle("JsAlert")
                .setMessage(message)
                .setPositiveButton("OK", new DialogInterface.OnClickListener()
                {
                  @Override
                  public void onClick(DialogInterface dialog, int which)
                  {
                    result.confirm();
                  }
                })
                .setCancelable(false)
                .show();
        return true;
      }
    
      // 支持javascript的确认框
      @Override
      public boolean onJsConfirm(WebView webView, String url, String message, final JsResult jsResult)
      {
        new AlertDialog.Builder(getContext())
                .setTitle("JsConfirm")
                .setMessage(message)
                .setPositiveButton("OK", new DialogInterface.OnClickListener()
                {
                  @Override
                  public void onClick(DialogInterface dialog, int which)
                  {
                    jsResult.confirm();
                  }
                })
                .setNegativeButton("Cancel", new DialogInterface.OnClickListener()
                {
                  @Override
                  public void onClick(DialogInterface dialog, int which)
                  {
                    jsResult.cancel();
                  }
                })
                .setCancelable(false)
                .show();
        // 返回布尔值:判断点击时确认还是取消
        // true表示点击了确认;false表示点击了取消;
        return true;
      }
    
      // 支持javascript输入框
      @Override
      public boolean onJsPrompt(WebView webView, String url, String message, String defaultValue, final JsPromptResult result)
      {
        return super.onJsPrompt(webView, s, s1, s2, jsPromptResult);
      }
    }
    

    WebView与原生代码的交互

    Java->JS

    loadUrl

    // mJSMethodName对应js方法名
    // result对应js方法参数
    mWebView.loadUrl("javascript:" + mJSMethodName + "(\" " + param + "\")");
    

    对应的html文件如下:

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
          // JS代码
          <script>
            // Android需要调用的方法
            function mJSMethodName()
            {
              alert("Android调用了JS的mJSMethodName方法");
            }
          </script>
        </head>
    </html>
    

    特别注意:JS代码调用一定要在 onPageFinished() 回调之后才能调用,否则不会调用。

    evaluateJavascript

    1. 该方法的执行不会使页面刷新,而第一种方法(loadUrl )的执行则会。所以该方法比第一种方法效率更高。
    2. Android 4.4 后才可使用
    mWebView.evaluateJavascript("javascript:" + mJSMethodName + "(\" " + param + "\")", new ValueCallback<String>() 
      {
        @Override
        public void onReceiveValue(String result) 
        {
          // result为js方法返回结果
        }
      });
    

    注:上面两种方法各有优劣,建议根据Android版本混合使用

    // 因为该方法在 Android 4.4 版本才可使用,所以使用时需进行版本判断
    if (Build.VERSION.SDK_INT < 18) 
    {
      mWebView.loadUrl("javascript:" + mJSMethodName + "(\" " + param + "\")");
    } else 
    {
      mWebView.evaluateJavascript("javascript:" + mJSMethodName + "(\" " + param + "\")", new ValueCallback<String>() 
      {
        @Override
        public void onReceiveValue(String result) 
        {
          // result为js方法返回结果
        }
      });
    }
    

    JS->Java

    通过 WebView 的 addJavascriptInterface() 方法

    这种方法是我们最常用的方法,使用方法如下:

    // 添加映射对象以及命名空间
    mWebView.addJavascriptInterface(new MyJsInteration(), "android");
    
    private class MyJsInteration 
    {
      @JavascriptInterface
      public void hello(String msg) 
      {
      }
    }
    

    上面的java代码对应的js代码是:

    // 注意android是上面定义的命名空间
    window.android.hello(message)
    

    通过WebViewClient 的shouldOverrideUrlLoading()方法回调

    这个我们已经在上面的代码里写过了,比如你可以自己维护一些特殊的URL以及处理这些URL的 Activity,然后复写 shouldOverrideUrlLoading(),在该方法中拦截特定URL转到特定的Activity进行处理。也能达到JS->Java的目的。并且这种形式也是比较常见的处理方式。

    // 复写shouldOverrideUrlLoading()方法,使得打开网页时不调用系统浏览器, 而是在本WebView中显示
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) 
    {
      // 特定的url调到native 页面进行处理 返回true
      if (LinkHandleUtils.handle(MyWebPageActivity.this, url, true)) 
      {
        return true;
      }
      mCurUrl = url;
      return false;
    }
    

    通过WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调

    这种方法跟上面的没有本质差异,也是在回调函数中进行Java代码操作,目前我在项目中用到的地方较少,主要用来做一些比较特殊的功能,例如检测到Alert弹框中的内容符合条件进行Java代码。

    JS->Java方法总结

    三种方法优劣比较:

    1. 通过WebView的addJavascriptInterface()方法比较简单,并且也更为常见,不过其存在不小的安全隐患。
    2. 通过WebViewClient 的shouldOverrideUrlLoading()方法回调这个使用起来也比较简单,也不存在方式1的安全隐患,不过JS获取Android方法的返回值复杂。

    如果JS想要得到Android方法的返回值,只能通过 WebView 的 loadUrl ()去执行 JS 方法把返回值传递回去

    WebView的文件上传

    当在网页里有文件上传组件时,我们惊奇的发现Android端这个文件上传组件并没有起作用。原因何在呢?因为Android 中的 WebView是不能直接打开文件选择弹框的。
    接下来我讲简单提供一下解决方案,先说一下思路:

    1. 接收WebView打开文件选择器的通知,收到通知后,打开文件选择器等待用户选择需要上传的文件
    2. 在onActivityResult中得到用户选择的文件的Uri
    3. 然后把Uri传递给Html5
      这样就完成了一次H5选择文件的过程,下面我把代码贴出来看一下.
    4. 当H5在调用上传文件的Api的时候,WebView 会回调 openFileChooser和onShowFileChooser 方法来通知我们,那我们就得重写了

    需要注意的是openFileChooser在不同的Android版本上是形参不同的

    private class MyWebChromeClient extends WebChromeClient
    {
      // 支持文件选择上传
      @Override
      public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> valueCallback, FileChooserParams fileChooserParams)
      {
        return super.onShowFileChooser(webView, valueCallback, fileChooserParams);
      }
    
      // Android > 4.1.1 调用这个方法
      public void openFileChooser(ValueCallback<Uri> uploadMsg,
                                  String acceptType, String capture)
      {
        if (mFileUploadSupportListener == null)
          return;
        // 调用传入的接口进行回调
        mFileUploadSupportListener.call(uploadMsg);
      }
    
    
      // 3.0 + 调用这个方法
      public void openFileChooser(ValueCallback<Uri> uploadMsg,
                                  String acceptType)
      {
        if (mFileUploadSupportListener == null)
          return;
        mFileUploadSupportListener.call(uploadMsg);
      }
    
      // Android < 3.0 调用这个方法
      public void openFileChooser(ValueCallback<Uri> uploadMsg)
      {
        if (mFileUploadSupportListener == null)
          return;
        mFileUploadSupportListener.call(uploadMsg);
      }
    }
    
    1. 注入接口
    // 注入接口
    mWebView.setFileUploadSupportListener(new IFileUploadSupportListener()
    {
      @Override
      public void call(ValueCallback<Uri> valueCallback)
      {
        mUploadMessage = valueCallback;
        chooseFile();
      }
    });
    
    // 选择文件
    private void chooseFile()
    {
      PhotoPicker.builder()         
              .setPhotoCount(1)
              .setShowCamera(true)
              .setShowGif(true)
              .setPreviewEnabled(false)
              .start(MyWebPageActivity.this, PhotoPicker.REQUEST_CODE);
    }
    
    1. 进行回传
    if (null == mUploadMessage)
    {
      return;
    }
    if (resultCode == RESULT_OK && requestCode == PhotoPicker.REQUEST_CODE)
    {
      ArrayList<String> photos = data.getStringArrayListExtra(PhotoPicker.KEY_SELECTED_PHOTOS);
    
      Uri result = Uri.parse(photos.get(0));
      mUploadMessage.onReceiveValue(result);
      mUploadMessage = null;
    } else
    {
      mUploadMessage.onReceiveValue(null);
    }
    

    WebView的优化

    WebView的addJavascriptInterface()方法的安全隐患

    上面已经稍微说了一下,该方法只能在Android4.4以上安全使用,那么我们来看一下Android 系统占比,Google公布的数据:截止 2018 .6 .28 ,Android4.4 之下占有约5.7%,具体占比如下图

    Android版本分布
    解决方案请参考Android:你不知道的 WebView 使用漏洞

    WebView的内存泄露

    WebView的内存泄露问题已经是个老生常谈的问题了,现在只要用到WebView的开发者都得注意到这个问题。
    现在流行的有以下两种解决方案:

    独立进程法

    独立进程法顾名思义是让包含 WebView 的 Acitivy 以android:process=":web" 的形式指定单独进程,然后在需要退出的时候使用System.exit(0) 结束整个进程,内存自然回收了。该方法简单暴力,并有以下优点:

    1. 每个独立的进程都能分配独立的内存,这样的话,你的app可以获得双倍的内存,其中一半给Webview吃。增大Webview获得的内存,变相的减小内存泄露产生OOM的概率。
    2. 在适当时机直接杀掉Webview独立进程,什么内存泄露,内存占用巨大的问题都见鬼去吧。要问什么时机?比如退出app时,检测到没有Webview页面时。
    3. Webview发生崩溃时不会导致app闪退,就像第二点说的,因为Webview是在独立进程中,如果发生崩溃,主进程还安然无事,app还在运行中,没有闪退,不闪的才是健康的。

    源码解决法

    这个方法就是RTFSC(Read The Fucking Source Code),从LeakCannary分析得出内存泄露在 org.chromium.android_webview.AwContents 类

    //org.chromium.android_webview.AwContents 类的onAttachedToWindow() 和  onDetachedFromWindow()方法
    @Override
    public void onAttachedToWindow() {
        if (isDestroyed()) return;
        if (mIsAttachedToWindow) {
            Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
            return;
        }
        mIsAttachedToWindow = true;
    
        mContentViewCore.onAttachedToWindow();
        nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(),
                mContainerView.getHeight());
        updateHardwareAcceleratedFeaturesToggle();
    
        if (mComponentCallbacks != null) return;
        mComponentCallbacks = new AwComponentCallbacks();
        mContext.registerComponentCallbacks(mComponentCallbacks);
    }
    
    @Override
    public void onDetachedFromWindow() {
        if (isDestroyed()) return;//注意这里
        if (!mIsAttachedToWindow) {
            Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
            return;
        }
        mIsAttachedToWindow = false;
        hideAutofillPopup();
        nativeOnDetachedFromWindow(mNativeAwContents);
    
        mContentViewCore.onDetachedFromWindow();
        updateHardwareAcceleratedFeaturesToggle();
    
        if (mComponentCallbacks != null) {
            mContext.unregisterComponentCallbacks(mComponentCallbacks);
            mComponentCallbacks = null;
        }
    
        mScrollAccessibilityHelper.removePostedCallbacks();
    }
    

    一般情况下,我们的activity退出的时候,都会主动调用 WebView.destroy() 方法,经过分析,destroy()的执行时间在onDetachedFromWindow之前,所以就会导致不能正常进行unregister(),从而造成内存泄露。

    知道原因了,那么解决办法也就来了。
    在Activity的onDestroy里方法里如下代码

    @Override
    protected void onDestroy() 
    {
      if (mWebView != null) 
      {
        try 
        {
          ViewGroup parent = (ViewGroup) mWebView.getParent();
          if (parent != null) 
          {
            parent.removeView(mWebView);
          }
          mWebView.removeAllViews();
          mWebView.destroy();
          } catch (Exception e)     
          {
            e.printStackTrace();
          }
        }
      }
      super.onDestroy();
    }
    

    相关文章

      网友评论

        本文标题:WebView使用详细介绍

        本文链接:https://www.haomeiwen.com/subject/ognofqtx.html