Android网页加载控件WebView应知应会

一.什么是WebView?WebView能干啥?

WebView 是一个用来显示 Web 网页的控件,它就是一个微型浏览器,包含一个浏览器该有的基本功能,例如:滚动、缩放、前进、后退下一页、搜索、执行 Js等功能。在Android4.4之前使用的是WebKit的内核,之后使用的是Chrome内核。

二.玩一玩WebView

常用API

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 resultCallback):Api 19 之后可以采用此方法之行 Js。
stopLoading():停止 WebView 当前加载。
clearView():在Android 4.3及其以上系统这个api被丢弃了, 并且这个api大多数情况下会有bug,经常不能清除掉之前的渲染数据。官方建议通过loadUrl(“about:blank”)来实现这个功能,阴雨需要重新加载一个页面自然时间会收到影响。
freeMemory():释放内存,不过貌似不好用。
clearFormData():清除自动完成填充的表单数据。需要注意的是,该方法仅仅清除当前表单域自动完成填充的表单数据,并不会清除WebView存储到本地的数据。

基本使用

基本上是和WebSettings和WebClient、WebChromeClient搭配使用,还有和JS一些交互等

1.添加网络权限

 <uses-permission android:name="android.permission.INTERNET"/>

2.实例化WebView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
    
    <com.seven.webview.SevenWebView
        android:layout_weight="9"
        android:id="@+id/seven_web"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <Button
        android:text="调用js方法"
        android:layout_weight="1"
        android:id="@+id/call_js"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

这里的WebView是单独封装的,配合WebSetting给WebView设置一些参数,比如支持js等

package com.seven.webview;

import android.content.Context;
import android.util.AttributeSet;
import android.webkit.WebSettings;
import android.webkit.WebView;

/**
 * Time:2020/3/13
 * <p>
 * Author:seven
 * <p>
 * Description:
 */
public class SevenWebView extends WebView {
    public SevenWebView(Context context) {
        super(context);
        init(context);
    }



    public SevenWebView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    /**
     *  初始化设置,和WebSettings搭配使用
     * @param context
     */
    private void init(Context context) {

        WebSettings webSettings = getSettings();
        if (webSettings == null) return;
        // 支持 Js 使用
        webSettings.setJavaScriptEnabled(true);
        // 开启DOM缓存,默认状态下是不支持LocalStorage的
        webSettings.setDomStorageEnabled(true);
        // 开启数据库缓存
        webSettings.setDatabaseEnabled(true);
        // 设置 WebView 的缓存模式
        webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
        // 支持缩放
        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);
        //设置背景
        setBackgroundColor(getResources().getColor(android.R.color.transparent));
    }


}

3.设置WebClient

注意重写shouldOverrideUrlLoading这个方法,需要调用webView去加载网页,否则,会有系统浏览器去加载的,理解这句话的意思第一次加载还是用webView,但是网页上如果有链接,就不会拦截了。

package com.seven.webview;

import android.graphics.Bitmap;
import android.net.http.SslError;
import android.util.Log;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;

/**
 * Time:2020/3/13
 * <p>
 * Author:seven
 * <p>
 * Description:
 */
public class SevenWebClient extends WebViewClient {
    private String TAG=getClass().getSimpleName();

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

    /**
     *  系统默认会通过手机浏览器打开网页,
     *  为了能够直接通过WebView显示网页,则必须设置
     *  是否在 WebView 内加载页面
     *
     * @param view
     * @param url
     * @return
     */
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Log.d(TAG, "shouldOverrideUrlLoading: "+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);
        Log.d(TAG, "onPageStarted: "+url);
    }

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

    /**
     * WebView 加载页面资源时会回调,每一个资源产生的一次网络加载,除非本地有当前 url 对应有缓存,否则就会加载。
     *
     * @param view WebView
     * @param url  url
     */
    @Override
    public void onLoadResource(WebView view, String url) {
        super.onLoadResource(view, url);
        Log.d(TAG, "onLoadResource: "+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);
        Log.d(TAG, "onReceivedError: "+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);
        Log.d(TAG, "onReceivedSslError: "+error);
    }
}

4.设置WebChromeClient

onProgressChanged这个方法会回调网页加载的进度,可以做一些进度条什么的,显示友好效果。

package com.seven.webview;

import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.webkit.ConsoleMessage;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;

/**
 * Time:2020/3/13
 * <p>
 * Author:seven
 * <p>
 * Description:
 */
public class SevenWebChromeClient extends WebChromeClient {

    private String TAG=getClass().getSimpleName();
    private Context mContenxt;

    public SevenWebChromeClient(Context context) {

        this.mContenxt = context;
    }

    /**
     *  Js的日志打印
     * @param consoleMessage
     * @return
     */
    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
        Log.d(TAG, "onConsoleMessage: "+consoleMessage.message());
        return super.onConsoleMessage(consoleMessage);
    }

    /**
     * 当前 WebView 加载网页进度
     *
     * @param view
     * @param newProgress
     */
    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        super.onProgressChanged(view, newProgress);
        Log.d(TAG, "onProgressChanged: "+newProgress);

    }

    /**
     * Js 中调用 alert() 函数,产生的对话框
     *
     * @param view
     * @param url
     * @param message
     * @param result
     * @return
     */
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
        Log.d(TAG, "onJsAlert: "+message);
        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) {
        Log.d(TAG, "onJsConfirm: "+message);
        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) {

        Log.d(TAG, "onJsPrompt: "+message);
        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);
        Log.d(TAG, "onReceivedTitle: "+title);
    }

}

5.如何和JS交互

在我自定义的WebView的时候,就已经设置过支持javascript了,所以这里只要实例化一个java对象,并通过addJavascriptInterface这个方法将java对象注入进去。
实力恶化一个java对象,被js调用的方法上需要加上@JavascriptInterface这个注解

package com.seven.webview;

import android.content.Context;
import android.webkit.JavascriptInterface;
import android.widget.Toast;

/**
 * Time:2020/3/13
 * <p>
 * Author:seven
 * <p>
 * Description:
 */
public class NativeJsInterface {

    private Context mContext;

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

    @JavascriptInterface
    public void helloAndroid() {
        Toast.makeText(mContext, "Hello Android!", Toast.LENGTH_SHORT).show();
    }


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

}

package com.seven.webview;

import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class MainActivity extends AppCompatActivity {

    private String TAG=getClass().getSimpleName();

    @BindView(R.id.seven_web)
    WebView webView;

    private WebViewClient webViewClient;

    private WebChromeClient webChromeClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        loadUrl();
        supportJs();

    }
    /**
     * 加载网页
     */
    private void loadUrl() {
        webViewClient = new SevenWebClient();
        webChromeClient = new SevenWebChromeClient(this);
        webView.setWebViewClient(webViewClient);
        webView.setWebChromeClient(webChromeClient);
        webView.loadUrl("file:///android_asset/html/ts.html");
    }

    /**
     * 扩展JS对象,页面可直接使用NativeObject对象调用Android提供的方法
     */
    private void supportJs() {
        NativeJsInterface nativeJsInterface = new NativeJsInterface(this);
        webView.addJavascriptInterface(nativeJsInterface, "NativeObject");

    }

    /**
     * 调用JS的方法
     */

    @OnClick(R.id.call_js)
    public void onViewClicked() {
        String jsMethod="javascript:androidCallJs()";
        if(Build.VERSION.SDK_INT< Build.VERSION_CODES.KITKAT){
            webView.loadUrl(jsMethod);
        }else {
              webView.evaluateJavascript(jsMethod, new ValueCallback<String>() {
                  @Override
                  public void onReceiveValue(String value) {
                      Log.d(TAG, "onReceiveValue: "+value);
                  }
              });
        }
    }
}

6.写一个简单的网页

</html>


<html>
<head>
    <meta charset="utf-8">
    <title>测试页</title>
    <script>
        function callAndroid(){
            NativeObject.helloAndroid();
        }

        function getAndroid(){
            NativeObject.getAndroid();
        }

       function androidCallJs(){
        alert('js方法被执行!!!')
       }
       function jumpApp(){

       }
    </script>

</head>
<body background="images/ts.png">



<center>
    <button type="button" id="button" onclick="callAndroid()">helloAndroid</button>
</center>
<center>
    <button type="button" id="button2" onclick="getAndroid()">getAndroid</button>
</center>

</center> <a href="scheme://host/path?query">重新打开自己</a></center>

</body>
</html>


通过以上6步就可以完成基本的加载网页,支持java和js互相调用的功能了。

网页通过Scheme方式跳转其他应用

1.scheme是什么?

android中的scheme是一种页面内跳转协议,是一种非常好的实现机制,通过定义自己的scheme协议,可以非常方便跳转app中的各个页面;通过scheme协议,服务器可以定制化告诉App跳转那个页面,可以通过通知栏消息定制化跳转页面,可以通过H5页面跳转页面等。schema也是【隐示启动】中的一种,在data属性下的,其余内容查看Android Intent的隐示启动(启动其余APP界面并传递数据)
Scheme的格式:客户端自定义的 URL 作为从一个应用调用另一个的基础,遵循 RFC 1808 (Relative Uniform Resource Locators) 标准。这跟我们常见的网页内容 URL 格式一样。一个普通的 URL 分为几个部分,scheme、host、relativePath、query。

2.网页如果跳转app?

1)首先,你自定义一个scheme,我在上面的页面代码中已经写过了。
2)如果在webViewclient拦截了,在webView中使用shouldOverrideUrlLoading中去拦截

    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Log.d(TAG, "shouldOverrideUrlLoading: "+url);

        if (url.startsWith("scheme")) {
            Log.d(TAG, "shouldOverrideUrlLoading: 处理自定义scheme-->" + url);
            try {
                // 以下固定写法
                final Intent intent = new Intent(Intent.ACTION_VIEW,
                        Uri.parse(url));
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                        | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                mContext.startActivity(intent);
            } catch (Exception e) {
                // 防止没有安装的情况
                e.printStackTrace();
                Toast.makeText(mContext,"您所打开的第三方App未安装!",Toast.LENGTH_SHORT).show();
            }
            return true;
        }
        view.loadUrl(url);
        return true;
    }

3)如果没有拦截,准确的来说是没有设置过webViewClient,可以直接用页面的方法直接打开。Android清单文件如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.seven.webview">

    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>

                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="scheme"/>
            </intent-filter>
        </activity>

    </application>

</manifest>

3.推荐做法

一般的做法都是拦截,在拦截出去启动app。不过也可以单独提供native方法给页面使用,让页面去跳转app。实现的方法可以有多种。

三.一些小坑

1.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() 执行。

2.扩展的java方法是在子线程中执行的,更新UI需要切换至主线程,否则会报错!

3.跨域的小坑,记得设置setAllowFileAccessFromFileURLs(true)这个方法

4.页面和native保存账号的坑
原生和页面嵌套时,比如登录时,如何同步账号的问题,一般的解法都是使用cookieManage这个来保存原生登录时的cookie,以确保在网页操作时不至于需要重复登录。

5.文件选择,重写WebChromeClient的这几个方法

 // For Android < 3.0
    public void openFileChooser(ValueCallback<Uri> valueCallback) {
        if(null!=valueCall){
            valueCall.onValueSelect(valueCallback);
        }
        openImageChooserActivity();
    }

    // For Android  >= 3.0
    public void openFileChooser(ValueCallback valueCallback, String acceptType) {
        if(null!=valueCall){
            valueCall.onValueSelect(valueCallback);
        }
        openImageChooserActivity();
    }

    //For Android  >= 4.1
    public void openFileChooser(ValueCallback<Uri> valueCallback, String acceptType,  String capture) {
//        uploadMessage = valueCallback;
        if(null!=valueCall){
            valueCall.onValueSelect(valueCallback);
        }
        openImageChooserActivity();
    }

    // For Android >= 5.0
    @Override
    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
        if(null!=valueCall){
            valueCall.onValuesSelectAboveL(filePathCallback);
        }

        openImageChooserActivity();
        return true;
    }
    private void openImageChooserActivity() {
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        i.setType("image/*");
        mContenxt.startActivityForResult(Intent.createChooser(i, "Image Chooser"),     FILE_CHOOSER_RESULT_CODE);
    }

四.发散学习

安全漏洞·:Android 4.2 以下不要在使用 JavascriptInterface方式,4.2 以上需要添加注解 @JavascriptInterface 才能调用。(这部分和JsBrige 有关,更详细的内容后面会介绍)
当系统辅助功能服务被开启时,在 Android 4.4 以下的系统中,由系统提供的 WebView 组件都默认导出 ”accessibility” 和 ”accessibilityTraversal” 这两个接口,这两个接口同样存在远程任意代码执行的威胁,同样的需要通过 removeJavascriptInterface 方法将这两个对象删除。

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

webView缓存问题
参考:WebView缓存原理分析和应用
如何优化加载速度
参考:WebView性能、体验分析与优化

五.总结

本文整理了一些webView常见用法、和js交互以及一些小坑。希望能对你有所帮助!如果觉得对你有用,我也觉得很高兴,毕竟技术是拿来分享的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章