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交互以及一些小坑。希望能對你有所幫助!如果覺得對你有用,我也覺得很高興,畢竟技術是拿來分享的。

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