深度學習js與安卓的互動以及WebView的那些坑

AWeiLoveAndroid發表於2018-01-23

前言:隨著市場需求的不斷變化,原生安卓已經無法滿足客戶的需要了,現在很多app都在使用Android和h5的互動實現某些功能,比如商品詳情頁,文章詳情頁面,商品點評頁面,還有某些複雜的展示頁面等等,設定登陸頁面都有可能是和js互動做到的。通過互動可以很快速的達到效果,原生的安卓去做的話就會很麻煩。今天我就簡單講一下使用WebView做到js程式碼和安卓的互動,通過一個小demo教你學會js和Android的互動。

##  首先來看看這篇部落格要講解內容的大綱(這個圖是我自己畫的,網上找不到的)

WebView詳解.png

⇒ 一、WebView的基本使用

WebView是一個基於webkit引擎、展現web頁面的控制元件。Webview在低版本和高版本採用了不同的webkit版本核心,4.4後直接使用了Chrome。   WebView控制元件功能強大,除了具有一般View的屬性和設定外,還可以對url請求、頁面載入**(直接使用html檔案**(網路上或本地assets中)作佈局**)、渲染Wb頁面、頁面互動(和js互動)**進行強大的處理。

(一)常用方法

  • (1) WebView的狀態
//啟用WebView為活躍狀態,能正常執行網頁的響應
webView.onResume() ;

//當頁面被失去焦點被切換到後臺不可見狀態,需要執行onPause
//通過onPause動作通知核心暫停所有的動作,比如DOM的解析、plugin的執行、JavaScript執行。
webView.onPause();

//當應用程式(存在webview)被切換到後臺時,這個方法不僅僅針對當前的webview而是全域性的全應用程式的webview
//它會暫停所有webview的layout,parsing,javascripttimer。降低CPU功耗。
webView.pauseTimers()

//恢復pauseTimers狀態
webView.resumeTimers();

//銷燬Webview
//在關閉了Activity時,如果Webview的音樂或視訊,還在播放。就必須銷燬Webview
//但是注意:webview呼叫destory時,webview仍繫結在Activity上
//這是由於自定義webview構建時傳入了該Activity的context物件
//因此需要先從父容器中移除webview,然後再銷燬webview:
rootLayout.removeView(webView); 
webView.destroy();
複製程式碼
  • (2) 關於前進 / 後退網頁
//是否可以後退
Webview.canGoBack() 
//後退網頁
Webview.goBack()

//是否可以前進                     
Webview.canGoForward()
//前進網頁
Webview.goForward()

//以當前的index為起始點前進或者後退到歷史記錄中指定的steps
//如果steps為負數則為後退,正數則為前進
Webview.goBackOrForward(intsteps) 
複製程式碼

常見用法:Back鍵控制網頁後退

問題:在不做任何處理前提下 ,瀏覽網頁時點選系統的“Back”鍵,整個 Browser 會呼叫 finish()而結束自身
目標:點選返回後,是網頁回退而不是推出瀏覽器

解決方案:在當前Activity中處理並消費掉該 Back 事件
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if ((keyCode == KEYCODE_BACK) && mWebView.canGoBack()) { 
    mWebView.goBack();
    return true;
    }
    return super.onKeyDown(keyCode, event);
}
複製程式碼
  • (3) 清除快取資料
//清除網頁訪問留下的快取
//由於核心快取是全域性的因此這個方法不僅僅針對webview而是針對整個應用程式.
Webview.clearCache(true);

//清除當前webview訪問的歷史記錄
//只會webview訪問歷史記錄裡的所有記錄除了當前訪問記錄
Webview.clearHistory();

//這個api僅僅清除自動完成填充的表單資料,並不會清除WebView儲存到本地的資料
Webview.clearFormData();
複製程式碼

(二)常用類

  • (1) WebSettings類(主要作用是:對WebView進行配置和管理)
//生成一個WebView元件(兩種方式)
//方式1:直接在在Activity中生成
WebView webView = new WebView(this)
//方法2:在Activity的layout檔案裡新增webview控制元件:
WebView webview = (WebView) findViewById(R.id.webView1);

//宣告WebSettings子類
WebSettings webSettings = webView.getSettings();

//如果訪問的頁面中要與Javascript互動,則webview必須設定支援Javascript
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");//設定編碼格式
複製程式碼

//設定WebView快取(當載入 html 頁面時,WebView會在/data/data/包名目錄下生成 database 與 cache 兩個資料夾,請求的 URL記錄儲存在 WebViewCache.db,而 URL的內容是儲存在 WebViewCache 資料夾下)

//優先使用快取: 
WebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); 
    //快取模式如下:
    //LOAD_CACHE_ONLY: 不使用網路,只讀取本地快取資料
    //LOAD_DEFAULT: (預設)根據cache-control決定是否從網路上取資料。
    //LOAD_NO_CACHE: 不使用快取,只從網路獲取資料.
    //LOAD_CACHE_ELSE_NETWORK,只要本地有,無論是否過期,或者no-cache,都使用快取中的資料。

//不使用快取: 
WebView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
複製程式碼

//結合使用(離線載入)(注意:每個 Application 只呼叫一次 WebSettings.setAppCachePath(),WebSettings.setAppCacheMaxSize())

if (NetStatusUtil.isConnected(getApplicationContext())) {//判斷網路是否連線
    webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//根據cache-control決定是否從網路上取資料。
} else {
    webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//沒網,則從本地獲取,即離線載入
}

webSettings.setDomStorageEnabled(true); // 開啟 DOM storage API 功能
webSettings.setDatabaseEnabled(true);   //開啟 database storage API 功能
webSettings.setAppCacheEnabled(true);//開啟 Application Caches 功能

String cacheDirPath = getFilesDir().getAbsolutePath() + APP_CACAHE_DIRNAME;
webSettings.setAppCachePath(cacheDirPath); //設定  Application Caches 快取目錄
複製程式碼
  • (2) WebViewClient類(主要作用是:處理各種通知 & 請求事件)
//步驟1. 定義Webview元件
Webview webview = (WebView) findViewById(R.id.webView1);

//步驟2. 選擇載入方式
//方式a. 載入一個網頁:
webView.loadUrl("http://www.google.com/");
//方式b:載入apk包中的html頁面
webView.loadUrl("file:///android_asset/test.html");
//方式c:載入手機本地的html頁面
webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");

//步驟3. 複寫shouldOverrideUrlLoading()方法,
webView.setWebViewClient(new WebViewClient(){
  @Override
  public boolean shouldOverrideUrlLoading(WebView view, String url) {
    //使得開啟網頁時不呼叫系統瀏覽器, 而是在本WebView中顯示
    view.loadUrl(url);
    return true;
  }

  @Override
  public void  onPageStarted(WebView view, String url, Bitmap favicon) {
    //設定載入開始的操作
  }

  @Override
  public void onPageFinished(WebView view, String url) {
    //設定載入結束的操作
  }

  @Override
  public boolean onLoadResource(WebView view, String url) {
    //設定載入資源的操作
  }

  @Override
  public void onReceivedError(WebView view, int errorCode, String description, String failingUrl){
    switch(errorCode){
    //該方法傳回了錯誤碼,根據錯誤型別可以進行不同的錯誤分類處理 
      case HttpStatus.SC_NOT_FOUND:
        view.loadUrl("file:///android_assets/error_handle.html");
        break;
    }
  }

  @Override    
  public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {//處理https請  
    handler.proceed();    //表示等待證照響應
    // handler.cancel();      //表示掛起連線,為預設方式
    // handler.handleMessage(null);    //可做其他處理
  }    
});
複製程式碼
  • (3) WebChromeClient類( 作用:輔助 WebView 處理 Javascript 的對話方塊,網站圖示,網站標題等等。)
  webview.setWebChromeClient(new WebChromeClient(){
      @Override
      public void onProgressChanged(WebView view, int newProgress) {
          if (newProgress < 100) {
          String progress = newProgress + "%";
          progress.setText(progress);
          }else{
          // to do something...
          }
      }

      @Override
      public void onReceivedTitle(WebView view, String title) {
         titleview.setText(title);
      }      
  });
複製程式碼
  • 一個demo示範一下以上幾個類的用法:

activity_main.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"

    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.carson_ho.webview_demo.MainActivity">


   <!-- 獲取網站的標題-->
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""/>

    <!--開始載入提示-->
    <TextView
        android:id="@+id/text_beginLoading"
        android:layout_below="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""/>

    <!--獲取載入進度-->
    <TextView
        android:layout_below="@+id/text_beginLoading"
        android:id="@+id/text_Loading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""/>

    <!--結束載入提示-->
    <TextView
        android:layout_below="@+id/text_Loading"
        android:id="@+id/text_endLoading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""/>

    <!--顯示網頁區域-->
    <WebView
        android:id="@+id/webView1"
        android:layout_below="@+id/text_endLoading"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_marginTop="10dp" />
</RelativeLayout>
複製程式碼

java如下:

import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.KeyEvent;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.TextView;


public class MainActivity extends AppCompatActivity {
    WebView mWebview;
    WebSettings mWebSettings;
    TextView beginLoading,endLoading,loading,mtitle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebview = (WebView) findViewById(R.id.webView1);
        beginLoading = (TextView) findViewById(R.id.text_beginLoading);
        endLoading = (TextView) findViewById(R.id.text_endLoading);
        loading = (TextView) findViewById(R.id.text_Loading);
        mtitle = (TextView) findViewById(R.id.title);
        mWebSettings = mWebview.getSettings();
        mWebview.loadUrl("http://www.baidu.com/");
        //設定不用系統瀏覽器開啟,直接顯示在當前Webview
        mWebview.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }
        });

        //設定WebChromeClient類
        mWebview.setWebChromeClient(new WebChromeClient() {
            //獲取網站標題
            @Override
            public void onReceivedTitle(WebView view, String title) {
                System.out.println("標題在這裡");
                mtitle.setText(title);
            }

            //獲取載入進度
            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                if (newProgress < 100) {
                    String progress = newProgress + "%";
                    loading.setText(progress);
                } else if (newProgress == 100) {
                    String progress = newProgress + "%";
                    loading.setText(progress);
                }
            }
        });

        //設定WebViewClient類
        mWebview.setWebViewClient(new WebViewClient() {
            //設定載入前的函式
            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                System.out.println("開始載入了");
                beginLoading.setText("開始載入了");
            }

            //設定結束載入函式
            @Override
            public void onPageFinished(WebView view, String url) {
                endLoading.setText("結束載入了");
            }
        });
    }

    //點選返回上一頁面而不是退出瀏覽器
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && mWebview.canGoBack()) {
            mWebview.goBack();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    //銷燬Webview
    @Override
    protected void onDestroy() {
        if (mWebview != null) {
            mWebview.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
            mWebview.clearHistory();
            ((ViewGroup) mWebview.getParent()).removeView(mWebview);
            mWebview.destroy();
            mWebview = null;
        }
        super.onDestroy();
    }
}
複製程式碼

⇒ 二、WebView的記憶體洩漏怎麼辦?

1.不在xml中定義 Webview ,而是在需要的時候在Activity中建立,並且Context使用 getApplicationgContext()

    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 
                                         ViewGroup.LayoutParams.MATCH_PARENT);
    mWebView = new WebView(getApplicationContext());
    mWebView.setLayoutParams(params);
    mLayout.addView(mWebView);
複製程式碼

2.在 Activity 銷燬( WebView )的時候,先讓 WebView 載入null內容,然後移除 WebView,再銷燬 WebView,最後置空。

@Override
protected void onDestroy() {
    if (mWebView != null) {
        mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
        mWebView.clearHistory();

        ((ViewGroup) mWebView.getParent()).removeView(mWebView);
        mWebView.destroy();
        mWebView = null;
    }
    super.onDestroy();
}
複製程式碼

⇒ 三、WebView的使用漏洞 及其修復方式

WebView中,主要漏洞有三類:

1.任意程式碼執行漏洞
2.密碼明文儲存漏洞
3.域控制不嚴格漏洞
複製程式碼

(一)任意程式碼執行漏洞

  • (1)addJavascriptInterface 介面引起遠端程式碼執行漏洞

1. 漏洞產生原因:

    js呼叫Android的其中一個方式是通過addJavascriptInterface介面進行物件對映:

    webView.addJavascriptInterface(new JSObject(), "myObj");
    // 引數1:Android的本地物件
    // 引數2:JS的物件
    // 通過物件對映將Android中的本地物件和JS中的物件進行關聯,從而實現JS呼叫Android的物件和方法
    所以,漏洞產生原因是:當JS拿到android這個物件後,就可以呼叫這個Android物件中所有的方法,包括系統類(Java.lang.Runtime 類),
從而進行任意程式碼執行。(比如**我們可以執行命令獲取本地裝置的SD卡中的檔案等資訊從而造成資訊洩露**)
複製程式碼

具體獲取系統類的描述:(結合 Java 反射機制)

    1. Android中的物件有一公共的方法:getClass() ;
    1. 該方法可以獲取到當前類 型別Class
    1. 該類有一關鍵的方法: Class.forName;
    1. 該方法可以載入一個類(可載入 java.lang.Runtime 類)
    1. 而該類是可以執行本地命令的

以下是攻擊的Js核心程式碼:

function execute(cmdArgs)  {  
    // 步驟1:遍歷 window 物件
    // 目的是為了找到包含 getClass ()的物件
    // 因為Android對映的JS物件也在window中,所以肯定會遍歷到
    for (var obj in window) {  
        if ("getClass" in window[obj]) {  
            // 步驟2:利用反射呼叫forName()得到Runtime類物件
            alert(obj);          
            return  window[obj].getClass().forName("java.lang.Runtime")  
            // 步驟3:以後,就可以呼叫靜態方法來執行一些命令,比如訪問檔案的命令
            getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);  
            // 從執行命令後返回的輸入流中得到字串,有很嚴重暴露隱私的危險。
            // 如執行完訪問檔案的命令之後,就可以得到檔名的資訊了。
        }  
    }  
} 
複製程式碼

當一些 APP 通過掃描二維碼開啟一個外部網頁時,攻擊者就可以執行這段 js 程式碼進行漏洞攻擊。在微信盛行、掃一掃行為普及的情況下,該漏洞的危險性非常大

2.解決方法

Android 4.2版本之後:Google 在Android 4.2 版本中規定對被呼叫的函式以 @JavascriptInterface進行註解從而避免漏洞攻擊

Android 4.2版本之前:採用攔截prompt()進行漏洞修復。 具體步驟如下:

1.繼承 WebView ,重寫 addJavascriptInterface 方法,然後在內部自己維護一個物件對映關係的 Map ( 將需要新增的 JS 介面放入該Map中 )
2.每次當 WebView 載入頁面前載入一段本地的 JS 程式碼,原理是:
    1) 讓JS呼叫一Javascript方法:該方法是通過呼叫prompt()把JS中的資訊(含特定標識,方法名稱等)傳遞到Android端;
    2) 在Android的onJsPrompt()中 ,解析傳遞過來的資訊,再通過反射機制呼叫Java物件的方法,這樣實現安全的JS呼叫Android程式碼。
關於Android返回給JS的值:可通過prompt()把Java中方法的處理結果返回到Js中
複製程式碼

具體需要載入的JS程式碼如下:

 javascript:(function JsAddJavascriptInterface_(){  
    // window.jsInterface 表示在window上宣告瞭一個Js物件
    // jsInterface = 註冊的物件名
    // 它註冊了兩個方法,onButtonClick(arg0)和onImageClick(arg0, arg1, arg2)
    // 如果有返回值,就新增上return
    if (typeof(window.jsInterface)!='undefined') {      
        console.log('window.jsInterface_js_interface_name is exist!!');}   
    else {  
        window.jsInterface = {     
            // 宣告方法形式:方法名: function(引數)
            onButtonClick:function(arg0) {   
            // prompt()返回約定的字串
            // 該字串可自己定義
            // 包含特定的識別符號MyApp和 JSON 字串(方法名,引數,物件名等)    
                return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onButtonClick',args:[arg0]}));  
            },  
            onImageClick:function(arg0,arg1,arg2) {   
                return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onImageClick',
args:[arg0,arg1,arg2]}));  
            },  
        };  
    }  
}  
)()
// 當JS呼叫 onButtonClick() 或 onImageClick() 時,就會回撥到Android中的 onJsPrompt ()
// 我們解析出方法名,引數,物件名
// 再通過反射機制呼叫Java物件的方法
複製程式碼

關於採用攔截prompt()進行漏洞修復需要注意的兩點細節:

細節1:載入上述JS程式碼的時機

由於當 WebView 跳轉到下一個頁面時,之前載入的 JS 可能已經失效,所以,通常需要在以下方法中載入js:
    onLoadResource();
    doUpdateVisitedHistory();
    onPageStarted();
    onPageFinished();
    onReceivedTitle();
    onProgressChanged();
複製程式碼

細節2:需要過濾掉 Object 類的方法

由於最終是通過反射得到Android指定物件的方法,所以同時也會得到基類的其他方法(最頂層的基類是 Object類)
為了不把 getClass()等方法注入到 JS 中,我們需要把 Object 的共有方法過濾掉,需要過濾的方法列表如下:
    getClass()
    hashCode()
    notify()
    notifyAl()
    equals()
    toString()
    wait()
複製程式碼
  • (2)searchBoxJavaBridge_介面引起遠端程式碼執行漏洞

1. 產生原因

1) 在Android 3.0以下,Android系統會預設通過searchBoxJavaBridge_的Js介面給 WebView 新增一個JS對映物件:
searchBoxJavaBridge_物件
2) 該介面可能被利用,實現遠端任意程式碼。
複製程式碼

2. 解決方法

刪除searchBoxJavaBridge_介面
// 通過呼叫該方法刪除介面removeJavascriptInterface();
複製程式碼
  • (3)accessibility和 accessibilityTraversal介面引起遠端程式碼執行漏洞

1. 產生原因

1) 在Android 3.0以下,Android系統會預設通過searchBoxJavaBridge_的Js介面給 WebView 新增一個JS對映物件:
searchBoxJavaBridge_物件
2) 該介面可能被利用,實現遠端任意程式碼。
複製程式碼

2. 解決方法

刪除searchBoxJavaBridge_介面
// 通過呼叫該方法刪除介面removeJavascriptInterface();
複製程式碼

(二)密碼明文儲存漏洞

  • (1)問題分析

    //WebView預設開啟密碼儲存功能 : mWebView.setSavePassword(true) 開啟後,在使用者輸入密碼時,會彈出提示框:詢問使用者是否儲存密碼; 如果選擇”是”,密碼會被明文保到 /data/data/com.package.name/databases/webview.db 中,這樣就有被盜取密碼的危險

  • (2)解決方案

    //關閉密碼儲存提醒 WebSettings.setSavePassword(false)

(三)域控制不嚴格漏洞

先看Android裡的WebViewActivity.java:

public class WebViewActivity extends Activity {
    private WebView webView;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_webview);
        webView = (WebView) findViewById(R.id.webView);

        //webView.getSettings().setAllowFileAccess(false);                    (1)
        //webView.getSettings().setAllowFileAccessFromFileURLs(true);         (2)
        //webView.getSettings().setAllowUniversalAccessFromFileURLs(true);    (3)
        Intent i = getIntent();
        String url = i.getData().toString(); //url = file:///data/local/tmp/attack.html 
        webView.loadUrl(url);
    }
 }

/**Mainifest.xml**/
// 將該 WebViewActivity 在Mainifest.xml設定exported屬性
// 表示:當前Activity是否可以被另一個Application的元件啟動
android:exported="true"
複製程式碼
  • (1)問題分析

上述demo中:即 A 應用可以通過 B 應用匯出的 Activity 讓 B 應用載入一個惡意的 file 協議的 url,從而可以獲取 B 應用的內部私有檔案,從而帶來資料洩露威脅

**具體:**當其他應用啟動此 Activity 時, intent 中的 data 直接被當作 url 來載入(假定傳進來的 url 為 file:///data/local/tmp/attack.html ),其他 APP 通過使用顯式 ComponentName 或者其他類似方式就可以很輕鬆的啟動該 WebViewActivity 並載入惡意url。

下面我們著重分析WebView中getSettings類的方法對 WebView 安全性的影響:

setAllowFileAccess()
setAllowFileAccessFromFileURLs()
setAllowUniversalAccessFromFileURLs()
複製程式碼
  • (2) setAllowFileAccess()

    // 設定是否允許 WebView 使用 File 協議,預設設定為true,即允許在 File 域下執行任意 JavaScript 程式碼 webView.getSettings().setAllowFileAccess(true);

但是同時也限制了 WebView 的功能,使其不能載入本地的 html 檔案,( 移動版的 Chrome 預設禁止載入 file 協議的檔案 ) ,如下圖:

深度學習js與安卓的互動以及WebView的那些坑

解決方案:

1) 對於不需要使用 file 協議的應用,禁用 file 協議;
  setAllowFileAccess(false); 
  
2) 對於需要使用 file 協議的應用,禁止 file 協議載入 JavaScript。
  setAllowFileAccess(true); 
  // 禁止 file 協議載入 JavaScript
  if (url.startsWith("file://") {
      setJavaScriptEnabled(false);
  } else {
      setJavaScriptEnabled(true);
  }
複製程式碼
  • (3)setAllowFileAccessFromFileURLs()

設定是否允許通過 file url 載入的 Js程式碼讀取其他的本地檔案 , 在Android 4.1前預設允許 , 在Android 4.1後預設禁止

webView.getSettings().setAllowFileAccessFromFileURLs(true);
複製程式碼

當AllowFileAccessFromFileURLs()設定為 true 時,攻擊者的JS程式碼為 ( 通過該程式碼可成功讀取 /etc/hosts 的內容資料 ) :

<script>
    function loadXMLDoc(){
        var arm = "file:///etc/hosts";
        var xmlhttp;
        if (window.XMLHttpRequest){
            xmlhttp=new XMLHttpRequest();
        }
        xmlhttp.onreadystatechange=function(){
            //alert("status is"+xmlhttp.status);
            if (xmlhttp.readyState==4){
                  console.log(xmlhttp.responseText);
            }
        }
        xmlhttp.open("GET",arm);
        xmlhttp.send(null);
    }
    loadXMLDoc();
</script>
複製程式碼

解決方案:

設定setAllowFileAccessFromFileURLs(false);
複製程式碼

當設定成為 false 時,上述JS的攻擊程式碼執行會導致錯誤,表示瀏覽器禁止從 file url 中的 JavaScript 讀取其它本地檔案。

  • (4) setAllowUniversalAccessFromFileURLs()

設定是否允許通過 file url 載入的 Javascript 可以訪問其他的源(包括http、https等源),在Android 4.1前預設允許(setAllowFileAccessFromFileURLs()不起作用),在Android 4.1後預設禁止

webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
複製程式碼

當AllowFileAccessFromFileURLs()被設定成true時,攻擊者的JS程式碼是:

// 通過該程式碼可成功讀取 http://www.so.com 的內容
<script>
    function loadXMLDoc(){
        var arm = "http://www.so.com";
        var xmlhttp;
        if (window.XMLHttpRequest){
            xmlhttp=new XMLHttpRequest();
        }
        xmlhttp.onreadystatechange=function(){
            //alert("status is"+xmlhttp.status);
            if (xmlhttp.readyState==4){
                 console.log(xmlhttp.responseText);
            }
        }
        xmlhttp.open("GET",arm);
        xmlhttp.send(null);
    }
    loadXMLDoc();
</script>
複製程式碼

解決方案:

設定setAllowUniversalAccessFromFileURLs(false);
複製程式碼
  • (5) setJavaScriptEnabled()

設定是否允許 WebView 使用 JavaScript(預設是不允許),但很多應用(包括移動瀏覽器)為了讓 WebView 執行 http 協議中的 JavaScript,都會主動設定為true,不區別對待是非常危險的,如下程式碼所示:

webView.getSettings().setJavaScriptEnabled(true);  
複製程式碼

即使把setAllowFileAccessFromFileURLs()和setAllowUniversalAccessFromFileURLs()都設定為 false,通過 file URL 載入的 javascript仍然有方法訪問其他的本地檔案:符號連結跨源攻擊(前提是允許 file URL 執行 javascript,即webView.getSettings().setJavaScriptEnabled(true);)

原因分析:

這一攻擊能奏效的原因是:通過 javascript 的延時執行和將當前檔案替換成指向其它檔案的軟連結就可以讀取到被符號連結所指的檔案。
複製程式碼

具體攻擊步驟:(在該命令執行前 xx.html 是不存在的;執行完這條命令之後,就生成了這個檔案,並且將 Cookie 檔案連結到了 xx.html 上。) 1. 把惡意的 js 程式碼輸出到攻擊應用的目錄下,隨機命名為 xx.html,修改該目錄的許可權; 2. 修改後休眠 1s,讓檔案操作完成; 3. 完成後通過系統的 Chrome 應用去開啟該 xx.html 檔案 4. 等待 4s 讓 Chrome 載入完成該 html,最後將該 html 刪除,並且使用 ln -s 命令為 Chrome 的 Cookie 檔案建立軟連線, 於是就可通過連結來訪問 Chrome 的 Cookie


注意事項:   Google 沒有進行修復,只是讓Chrome 最新版本預設禁用 file 協議,所以這一漏洞在最新版的 Chrome 中並不存在。   但是,在日常大量使用 WebView 的App和瀏覽器,都有可能受到此漏洞的影響。通過利用此漏洞,容易出現資料洩露的危險   如果是 file 協議,禁用 javascript 可以很大程度上減小跨源漏洞對 WebView 的威脅。   但並不能完全杜絕跨原始檔洩露。例:應用實現了下載功能,對於無法載入的頁面,會自動下載到 sd 卡中;由於 sd 卡中的檔案所有應用都可以訪問,於是可以通過構造一個 file URL 指向被攻擊應用的私有檔案,然後用此 URL 啟動被攻擊應用的 WebActivity,這樣由於該 WebActivity 無法載入該檔案,就會將該檔案下載到 sd 卡下面,然後就可以從 sd 卡上讀取這個檔案了

  • (6) 最終解決方案

1)對於不需要使用 file 協議的應用,禁用 file 協議;

// 禁用 file 協議;
setAllowFileAccess(false); 
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
複製程式碼

2)對於需要使用 file 協議的應用,禁止 file 協議載入 JavaScript。

// 需要使用 file 協議
setAllowFileAccess(true); 
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);

// 禁止 file 協議載入 JavaScript
if (url.startsWith("file://") {
    setJavaScriptEnabled(false);
} else {
    setJavaScriptEnabled(true);
}
複製程式碼

⇒ 四、安卓通過WebView和js互動

Android與js通過WebView互相呼叫方法,二者溝通的橋樑是WebView,實際上是:

  • Android去呼叫JS的程式碼
  • JS去呼叫Android的程式碼

對於 Android呼叫JS程式碼 的方法有2種: 1. 通過WebView的loadUrl() 2. 通過WebView的evaluateJavascript()

對於 JS呼叫Android程式碼 的方法有3種: 1. 通過WebView的addJavascriptInterface()進行物件對映 2. 通過 WebViewClient 的shouldOverrideUrlLoading ()方法回撥攔截 url 3. 通過 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回撥攔截JS對話方塊alert()、confirm()、prompt() 訊息

⇒ 五、WebView 的快取機制 & 資源預載入方案

⇒ 六、WebView的那些坑(我自己整理收集的)

(1) 為什麼Webview開啟一個頁面,播放一段音樂,退出Activity時音樂還在後臺播放?

◆◆ 解決方案 1:

//銷燬Webview
@Override
protected void onDestroy() {
    if (mWebview != null) {
        mWebview.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
        mWebview.clearHistory();
        ((ViewGroup) mWebview.getParent()).removeView(mWebview);
        mWebview.destroy();
        mWebview = null;
    }
    super.onDestroy();
}
複製程式碼

還有別問我為什麼要移除,等你Error: WebView.destroy() called while still attached!之後你就知道了。

◆◆ 解決方案 2:

@Override
protected void onPause() {
   h5_webview.onPause();
   h5_webview.pauseTimers();
   super.onPause();
}
@Override
protected void onResume() {
   h5_webview.onResume();
   h5_webview.resumeTimers();
   super.onResume();
}
複製程式碼

Webview的onPause()方法官網是這麼解釋的:

   Does a best-effort attempt to pause any processing that can be paused safely, such as animations
and geolocation. Note that this call does not pause JavaScript. To pause JavaScript globally, use 
pauseTimers(). To resume WebView, call onResume().  
【翻譯:】通知核心嘗試停止所有處理,如動畫和地理位置,但是不能停止Js,如果想全域性停止Js,
可以呼叫pauseTimers()全域性停止Js,呼叫onResume()恢復。
複製程式碼

(2) 怎麼用網頁的標題來設定自己的標題欄?

◆◆ 解決方案:

WebChromeClient mWebChromeClient = new WebChromeClient() {    
    @Override    
    public void onReceivedTitle(WebView view, String title) {    
        super.onReceivedTitle(view, title);    
        txtTitle.setText(title);    
    }    
};  
mWedView.setWebChromeClient(mWebChromeClient());
複製程式碼

★★ 注意事項:

●   1.可能當前頁面沒有標題,獲取到的是null,那麼你可以在跳轉到該Activity的時候自己帶一個標題,或者有一個預設標題。
●   2.在一些機型上面,Webview.goBack()後,這個方法不一定會呼叫,所以標題還是之前頁面的標題。那麼
你就需要用一個ArrayList來保持載入過的url,一個HashMap儲存url及對應的title.然後就是用WebView.canGoBack()來做判斷處理了。
複製程式碼

(3) 為什麼打包之後JS呼叫失敗(或者WebView與JavaScript相互呼叫時,如果是debug沒有配置混淆時,呼叫時沒問題的,但是當設定混淆後發現無法正常呼叫了)?

◆◆ 解決方案:在proguard-rules.pro中新增混淆。

-keepattributes *Annotation*  
-keepattributes *JavascriptInterface*
-keep public class org.mq.study.webview.DemoJavaScriptInterface{
   public <methods>;
}
#假如是內部類,混淆如下:
-keepattributes *JavascriptInterface*
-keep public class org.mq.study.webview.webview.DemoJavaScriptInterface$InnerClass{
    public <methods>;
}

其中org.mq.study.webview.DemoJavaScriptInterface 是不需要混淆的類名
複製程式碼

(4) 5.0 以後的WebView載入的連結為Https開頭,但是連結裡面的內容,比如圖片為Http連結,這時候,圖片就會載入不出來,怎麼解決?

★★ 原因分析:原因是Android 5.0上Webview預設不允許載入Http與Https混合內容:

◆◆ 解決方案:

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 
    //兩者都可以
    webSetting.setMixedContentMode(webSetting.getMixedContentMode());
    //mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
複製程式碼

★★ 引數說明:

●   MIXED_CONTENT_ALWAYS_ALLOW 允許從任何來源載入內容,即使起源是不安全的;
●   MIXED_CONTENT_NEVER_ALLOW 不允許Https載入Http的內容,即不允許從安全的起源去載入一個不安全的
    資源;
●   MIXED_CONTENT_COMPLTIBILITY_MODE 當涉及到混合式內容時,WebView會嘗試去相容最新Web瀏覽器的
    風格;
複製程式碼

另外:在認證證照不被Android所接受的情況下,我們可以通過設定重寫WebViewClient的onReceivedSslError方法在其中設定接受所有網站的證照來解決,具體程式碼如下:

webView.setWebViewClient(new WebViewClient() {
        @Override
        public void onReceivedSslError(WebView view,
                SslErrorHandler handler, SslError error) {
            //super.onReceivedSslError(view, handler, error);注意一定要去除這行程式碼,否則設定無效。
            // handler.cancel();// Android預設的處理方式
            handler.proceed();// 接受所有網站的證照
            // handleMessage(Message msg);// 進行其他處理
        }
});
複製程式碼

(5) WebView呼叫手機系統相簿來上傳圖片,開發過程中發現在很多機器上無法正常喚起系統相簿來選擇圖片。怎麼解決?

★★ 原因分析:因為Google攻城獅們對setWebChromeClient的回撥方法openFileChooser做了多次修改,5.0以下openFileChooser有幾種過載方法,在5.0以上將回撥方法該為了onShowFileChooser。

◆◆ 解決方案:為了相容各個版本,我們需要對openFileChooser()進行過載,同時針對5.0及以上重寫onShowFileChooser()方法:

上一段示例程式碼,給大家看看:

public class MainActivity extends AppCompatActivity {

private ValueCallback<Uri> uploadMessage;
private ValueCallback<Uri[]> uploadMessageAboveL;
private final static int FILE_CHOOSER_RESULT_CODE = 10000;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    WebView webview = (WebView) findViewById(R.id.web_view);
    assert webview != null;
    WebSettings settings = webview.getSettings();
    settings.setUseWideViewPort(true);
    settings.setLoadWithOverviewMode(true);
    settings.setJavaScriptEnabled(true);
    webview.setWebChromeClient(new WebChromeClient() {

        //  android 3.0以下:用的這個方法
        public void openFileChooser(ValueCallback<Uri> valueCallback) {
            uploadMessage = valueCallback;
            openImageChooserActivity();
        }

        // android 3.0以上,android4.0以下:用的這個方法
        public void openFileChooser(ValueCallback valueCallback, String acceptType) {
            uploadMessage = valueCallback;
            openImageChooserActivity();
        }

        //android 4.0 - android 4.3  安卓4.4.4也用的這個方法
        public void openFileChooser(ValueCallback<Uri> valueCallback, String acceptType, 
                        String capture) {
            uploadMessage = valueCallback;
            openImageChooserActivity();
        }

        //android4.4 無方法。。。

        // Android 5.0及以上用的這個方法
        @Override
        public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> 
                 filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
            uploadMessageAboveL = filePathCallback;
            openImageChooserActivity();
            return true;
        }
    });
    String targetUrl = "file:///android_asset/up.html";
    webview.loadUrl(targetUrl);
}

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

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == FILE_CHOOSER_RESULT_CODE) {
        if (null == uploadMessage && null == uploadMessageAboveL) return;
        Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
        if (uploadMessageAboveL != null) {
            onActivityResultAboveL(requestCode, resultCode, data);
        } else if (uploadMessage != null) {
            uploadMessage.onReceiveValue(result);
            uploadMessage = null;
        }
    }
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void onActivityResultAboveL(int requestCode, int resultCode, Intent intent) {
    if (requestCode != FILE_CHOOSER_RESULT_CODE || uploadMessageAboveL == null)
        return;
    Uri[] results = null;
    if (resultCode == Activity.RESULT_OK) {
        if (intent != null) {
            String dataString = intent.getDataString();
            ClipData clipData = intent.getClipData();
            if (clipData != null) {
                results = new Uri[clipData.getItemCount()];
                for (int i = 0; i < clipData.getItemCount(); i++) {
                    ClipData.Item item = clipData.getItemAt(i);
                    results[i] = item.getUri();
                }
            }
            if (dataString != null)
                results = new Uri[]{Uri.parse(dataString)};
        }
    }
    uploadMessageAboveL.onReceiveValue(results);
    uploadMessageAboveL = null;
}
複製程式碼

}


重點坑:針對Android4.4,系統把openFileChooser方法去掉了,怎麼解決?

詳情請見 部落格 http://blog.csdn.net/xiexie758/article/details/52446937 我這裡就不多說了。

(6) WebView呼叫手機系統相簿來上傳圖片,處理好第六點說的方法,我們打好release包測試的時候卻又發現還是沒法選擇圖片了。怎麼解決?

★★ 原因分析:無奈去翻WebChromeClient的原始碼,發現openFileChooser()是系統API,我們的release包是開啟了混淆的,所以在打包的時候混淆了openFileChooser(),這就導致無法回撥openFileChooser()了。

◆◆ 解決方案也很簡單,直接不混淆openFileChooser()就好了。

-keepclassmembers class * extends android.webkit.WebChromeClient{
   public void openFileChooser(...);
}
複製程式碼

(7)怎麼在 WebView 中長按儲存圖片?

1. 給 WebView新增監聽

mWebview.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {
    }
});
複製程式碼

2. 獲取點選的圖片地址

先獲取型別,根據相應的型別來處理對應的資料。

//首先判斷點選的型別
WebView.HitTestResult result = ((WebView) v).getHitTestResult();
int type = result.getType();

//獲取具體資訊,圖片這裡就是圖片地址
String imgurl = result.getExtra();
複製程式碼

type有這幾種型別:

  • WebView.HitTestResult.UNKNOWN_TYPE 未知型別
  • WebView.HitTestResult.PHONE_TYPE 電話型別
  • WebView.HitTestResult.EMAIL_TYPE 電子郵件型別
  • WebView.HitTestResult.GEO_TYPE 地圖型別
  • WebView.HitTestResult.SRC_ANCHOR_TYPE 超連結型別
  • WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE 帶有連結的圖片型別
  • WebView.HitTestResult.IMAGE_TYPE 單純的圖片型別
  • WebView.HitTestResult.EDIT_TEXT_TYPE 選中的文字型別

3. 操作圖片

你可以彈出儲存圖片,或者點選之後跳轉到顯示圖片的頁面。

最後整理一下程式碼:

mWebView.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {
        WebView.HitTestResult result = ((WebView)v).getHitTestResult();
        if (null == result)
            return false;
        int type = result.getType();
        if (type == WebView.HitTestResult.UNKNOWN_TYPE)
            return false;

        // 這裡可以攔截很多型別,我們只處理圖片型別就可以了
        switch (type) {
            case WebView.HitTestResult.PHONE_TYPE: // 處理撥號
                break;
            case WebView.HitTestResult.EMAIL_TYPE: // 處理Email
                break;
            case WebView.HitTestResult.GEO_TYPE: // 地圖型別
                break;
            case WebView.HitTestResult.SRC_ANCHOR_TYPE: // 超連結
                break;
            case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
                break;
            case WebView.HitTestResult.IMAGE_TYPE: // 處理長按圖片的選單項
                // 獲取圖片的路徑
                String saveImgUrl = result.getExtra();

                // 跳轉到圖片詳情頁,顯示圖片
                Intent i = new Intent(MainActivity.this, ImageActivity.class);
                i.putExtra("imgUrl", saveImgUrl);
                startActivity(i);
                break;
            default:
                break;
        }
    }
});
複製程式碼

(8) WebView 開啟硬體加速導致的問題?

WebView有很多問題,比如:不能開啟pdf,播放視屏也只能開啟硬體加速才能支援,在某些機型上會崩潰。 下面看一下硬體加速, 硬體加速 分為四個級別:

  • Application級別
     <application android:hardwareAccelerated="true"...>
複製程式碼
  • Activity級別
     <activity android:hardwareAccelerated="true"...>
複製程式碼
  • window級別(目前為止,Android還不支援在Window級別關閉硬體加速。)
    getWindow().setFlags(
         WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
         WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
複製程式碼
  • View級別
     view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
複製程式碼

WebView開啟硬體加速導致螢幕花屏問題的解決:

★★ 原因分析: 4.0以上的系統我們開啟硬體加速後,WebView渲染頁面更加快速,拖動也更加順滑。但有個副作用就是,當WebView檢視被整體遮住一塊,然後突然恢復時(比如使用SlideMenu將WebView從側邊滑出來時),這個過渡期會出現白塊同時介面閃爍。

◆◆ 解決方案: 在過渡期前將WebView的硬體加速臨時關閉,過渡期後再開啟,程式碼如下:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }
複製程式碼

Android 4.0+ 版本中的EditText字元重疊問題: 做的軟體,在一些機器上,打字的時候,EditText中的內容會出現重疊,而大部分機器沒有,所以感覺不是程式碼的問題,一直沒有頭緒。

深度學習js與安卓的互動以及WebView的那些坑

出現原因:JellyBean的硬體加速bug,在此我們關掉硬體加速即可。 解決方案:在EditText中加入一句:

android:layerType=”software”  
複製程式碼

圖片無法顯示: 做的程式裡有的時候會需要載入大圖,但是硬體加速中 OpenGL對於記憶體是有限制的。如果遇到了這個限制,LogCat只會報一個Warning: Bitmap too large to be uploaded into a texture (587x7696, max=2048x2048)

深度學習js與安卓的互動以及WebView的那些坑
這時我們就需要把硬體加速關閉了。 但開始我是這樣處理的,我關閉了整個應用的硬體加速:

<application  
    android:allowBackup="true"  
    android:icon="@drawable/ic_launcher"  
    android:hardwareAccelerated="false"  
    android:label="@string/app_name"  
    android:theme="@style/AppTheme" >  
複製程式碼

隨後我就發現,雖然圖片可以顯示了,但是ListView和WebView等控制元件顯得特別的卡,這說明硬體加速對於程式的效能提升是很明顯的。所以我就改為對於Activity的關閉。

<activity  
    android:name="icyfox.webviewimagezoomertest.MainActivity"  
    android:label="@string/app_name"  
    android:hardwareAccelerated="false"  
複製程式碼

(9) ViewPager裡非首屏WebView點選事件不響應是什麼原因?

  如果你的多個WebView是放在ViewPager裡一個個載入出來的,那麼就會遇到這樣的問題。ViewPager首屏WebView的建立是在前臺,點選時沒有問題;而其他非首屏的WebView是在後臺建立,滑動到它後點選頁面會出現如下錯誤日誌:

20955-20968/xx.xxx.xxx E/webcoreglue﹕ Should not happen: no rect-based-test nodes found
複製程式碼

◆◆ 解決方案: 這個問題的辦法是繼承WebView類,在子類覆蓋onTouchEvent方法,填入如下程式碼:

@Override
public boolean onTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onScrollChanged(getScrollX(), getScrollY(), getScrollX(), getScrollY());
    }
    return super.onTouchEvent(ev);
}
複製程式碼

⇒ 七、安卓8.0關於WebView的新特性(自己整理收集的)

WebView新增了一些非常有用的API,可以使用和chrome瀏覽器類似的API來實現對惡意網站的檢測來保護web瀏覽的安全性,為此需要在manifest中新增如下meta-data標籤:

<manifest>
<meta-data
    android:name="android.webkit.WebView.EnableSafeBrowing"
    android:value="true" />
<!-- ... -->
</manifest>
複製程式碼

WebView還增加了關於多程式的API,可以使用多程式來增強安全性和健壯性,如果render程式崩潰了,你還可以使用Termination Handler API來檢測到崩潰並做出相應處理。


★ 另外,我還有一點話是要想說的:(自己整理收集的)

★ 關於WebView的一點小優化:

(1)給WebView加一個載入進度條

  用Webview載入一個網頁時,如果載入時間長,介面會一直空白,體驗不太好,所以加個進度條更好看一下,主流APP也都有進度條效果,大概思路我來說一下:   首先自定義一個HorizontalProgressView繼承View,然後自定義一個MyWebView繼承WebView,然後初始化的時候通過addView方法把前面自定義HorizontalProgressView,然後在MyWebView裡面寫一個內部類繼承WebChromeClient,大致程式碼如下:

private class MyWebCromeClient extends WebChromeClient {
    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        if (newProgress == 100) {
            //載入完畢進度條消失
            progressView.setVisibility(View.GONE);
        } else {
            //更新進度
            progressView.setProgress(newProgress);
        }
        super.onProgressChanged(view, newProgress);
    }
}
複製程式碼

主要是通過MyWebCromeClient 的onProgressChanged方法裡面的進度值呼叫 progressView.setProgress()方法去更新進度條,當載入100%的時候讓進度條消失。 具體實現你們自己去處理吧。

(2)加快HTML網頁載入完成的速度,等頁面finish再載入圖片

  預設情況html程式碼下載到WebView後,webkit開始解析網頁各個節點,發現有外部樣式檔案或者外部指令碼檔案時,會非同步發起網路請求下載檔案,但如果在這之前也有解析到image節點,那勢必也會發起網路請求下載相應的圖片。在網路情況較差的情況下,過多的網路請求就會造成頻寬緊張,影響到css或js檔案載入完成的時間,造成頁面空白loading過久。解決的方法就是告訴WebView先不要自動載入圖片,等頁面finish後再發起圖片載入。

◆◆ 解決辦法:

在WebView初始化時設定如下程式碼:

public void int () {
    if(Build.VERSION.SDK_INT >= 19) {
        webView.getSettings().setLoadsImagesAutomatically(true);
    } else {
        webView.getSettings().setLoadsImagesAutomatically(false);
    }
}
複製程式碼

同時在WebView的WebViewClient例項中的onPageFinished()方法新增如下程式碼:

@Override
public void onPageFinished(WebView view, String url) {
    if(!webView.getSettings().getLoadsImagesAutomatically()) {
        webView.getSettings().setLoadsImagesAutomatically(true);
    }
}
複製程式碼

(3)自定義WebView頁面載入出錯介面

  當WebView載入頁面出錯時(一般為404 NOT FOUND),安卓WebView會預設顯示一個賣萌的出錯介面。但我們怎麼能讓使用者發現原來我使用的是網頁應用呢,我們期望的是使用者在網頁上得到是如原生般應用的體驗,那就先要從幹掉這個預設出錯頁面開始。當WebView載入出錯時,我們會在WebViewClient例項中的onReceivedError()方法接收到錯誤,我們就在這裡做些手腳:

@Override
public void onReceivedError (WebView view, int errorCode, String description, String failingUrl) {
    super.onReceivedError(view, errorCode, description, failingUrl);
    loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
    mErrorFrame.setVisibility(View.VISIBLE);
}
複製程式碼

  從上面可以看出,我們先使用loadDataWithBaseURL清除掉預設錯誤頁內容,再讓我們自定義的View得到顯示(mErrorFrame為蒙在WebView之上的一個LinearLayout佈局,預設為View.GONE)。

(4) 怎麼知道WebView是否已經滾動到頁面底端?

◆◆ 解決方案:

  • 方案1,使用原生WebView的api可以獲取到:
    if (mWebView.getContentHeight() * mWebView.getScale()  == (mWebView.getHeight() + 
    mWebView.getScrollY())) {
        //說明已經到底了
    }
複製程式碼
  • 方案2,繼承WebView,重寫onScrollChanged方法:   我們在做上拉載入下一頁這樣的功能時,也需要知道當前頁面滾動條所處的狀態,如果快到底部,則要發起網路請求資料更新網頁。同樣繼承WebView類,在子類覆蓋onScrollChanged方法。   以下程式碼中mCurrContentHeight用於記錄上次觸發時的網頁高度,用來防止在網頁總高度未發生變化而目標區域發生連續滾動時會多次觸發TODO,mThreshold是一個閾值,當頁面底部距離滾動條底部的高度差<=這個值時會觸發TODO裡面的程式碼。

具體如下:

@Override
protected void onScrollChanged(int newX, int newY, int oldX, int oldY) {
    super.onScrollChanged(newX, newY, oldX, oldY);
    if (newY != oldY) {
        float contentHeight = getContentHeight() * getScale();
        // 當前內容高度下從未觸發過, 瀏覽器存在滾動條且滑動到將抵底部位置
        if (mCurrContentHeight != contentHeight && newY > 0 && contentHeight <= newY + getHeight() + mThreshold) {
            // TODO Something...
            mCurrContentHeight = contentHeight;
        }
    }
}
複製程式碼

★★ 相關API介紹:

●   getContentHeight() @return the height of the HTML content
●   getScale() @return the current scale
●   getHeight() @return The height of your view
●   getScrollY() @return The top edge of the displayed part of your view, in pixels.
複製程式碼

(5) 怎麼知道WebView是否存在滾動條?

  當我們做類似上拉載入下一頁這樣的功能的時候,頁面初始的時候需要知道當前WebView是否存在縱向滾動條,如果有則不載入下一頁,如果沒有則載入下一頁直到其出現縱向滾動條。   首先繼承WebView類,在子類新增下面的程式碼:

public boolean existVerticalScrollbar () {
    return computeVerticalScrollRange() > computeVerticalScrollExtent();
}
複製程式碼

  computeVerticalScrollRange得到的是可滑動的最大高度,computeVerticalScrollExtent得到的是滾動把手自身的高,當不存在滾動條時,兩者的值是相等的。當有滾動條時前者一定是大於後者的。


參考博文: http://blog.csdn.net/carson_ho/article/details/64904691 http://bbs.csdn.net/topics/390905615 http://blog.csdn.net/cyuyanshujujiegou/article/details/52267817

相關文章