WebView使用详细介绍

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();
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容

  • 昨天我在家里呆了一天,这一天呆的实在是太难受了。家中阴冷的氛围让我想逃,我想逃离那个没有温度,只有抑郁的家,那...
    山转转阅读 393评论 0 3
  • 没落的富豪,穷苦的卑微,志向算什么。 伤心的泪眼,冰冷的残壳,无奈的贱命,苟活于凡世。 人为什么要吃饭?为什么要活...
    照亮Br阅读 176评论 1 0
  • 像每一个晚饭后的时光一样,刘东又跟苏妍有一搭没一搭的聊着,说实话,刘东并不是一个很好的聊天对象,开头第一个...
    瑾沐阅读 1,411评论 0 0
  • 一直都是夏天的时候出游,难得这次冬天出来,假期短,于是就选择了不远的重庆,体验一番雾都的冬。 买的下午6点15的机...
    禅木老师阅读 284评论 0 0