Webview 非http或者https的網絡請求攔截

webview調起撥打電話,支付寶,微信,qq等       url攔截

關鍵步驟

1,extends   WebViewClient

2,複寫其方法shouldOverrideUrlLoading   webview.loadURL的時候纔會調用

具體代碼實現:

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
KLog.e("urlurl====="+url);

if( url.startsWith("http:") || url.startsWith("https:") ) {  

if(url.startsWith("https://wx.tenpay.com")){//H5使用微信支付

Map<String, String> extraHeaders = new HashMap<>();

extraHeaders.put("Referer", realm);  
view.loadUrl(url, extraHeaders);

} else{

view.loadUrl(url); 

}

}else{ //非http或者https的網絡請求攔截,用action_view啓動。可能報錯。 

try {
Uri uri = Uri.parse(url);     
Intent intent =new Intent(Intent.ACTION_VIEW, uri); 
view.getContext().startActivity(intent);   
}catch (Exception e){
e.printStackTrace();   
if (url.startsWith("alipay")){ 
Toast.makeText(view.getContext(), "請確認是否安裝支付寶",Toast.LENGTH_SHORT).show();
}else if (url.startsWith("mqqwpa")){
Toast.makeText(view.getContext(), "請確認是否安裝QQ",Toast.LENGTH_SHORT).show();       
}
}
}
return true;
}

注意:如不處理的話,會提示找不到網頁

下面是調用支付寶付款,出現的錯誤

找不到網頁 alipays://platformapi/startapp?appid=20000125&ordersuffix=h5_route_token

----------------------喚起支付寶前會發起請求,webview的連續動作是先後請求兩個url-------------------------------

1.http://tt.cpostcard.com/Wap/Shop/pay/sid/0/orderid/492/paytype/alipayApp

2.https://mclient.alipay.com/home/exterfaceAssign.htm?alipay_exterface_invoke_assign_client_ip=222.128.6.225&body=20161229125138-492&subject=20161229125138-492&sign_type=RSA&notify_url=http%3A%2F%2Ftest.pay.hualumedia.com%2Fcallback%2Falipay%2Fproject%2Fwxmall&out_trade_no=20161229125138-492&return_url=http%3A%2F%2Ftest.pay.hualumedia.com%2Fcallback%2Falipay%2Fpage%2F1%2Fproject%2Fwxmall&sign=CBLgNa9chILk45N6h0KrqOaXR90sAxMKGKUtfWNr7Ie1NF8w5pUy8ZtyJdsUovzGXbXVy6Zssq%2FkHoWad79rcYutsL0qsvdum0WJO0%2BYGjqNDuf%2BMqey8ax8B7Y97WmWVtomsQgG18rMkcJAfUT8heXIy3N1lwGDMLbgatJiWRk%3D&app_pay=Y&_input_charset=utf-8&it_b_pay=30m&alipay_exterface_invoke_assign_target=mapi_direct_trade.htm&alipay_exterface_invoke_assign_model=cashier&total_fee=129.00&service=alipay.wap.create.direct.pay.by.user&partner=2088111492620239&seller_id=hualumedia%40sina.com&alipay_exterface_invoke_assign_sign=_j9_c_n3_x_b2uo_p_fz_f_qgbzrj7c_s%2F%2B4_qc_iy_z_h_jc_tl_gu_y_do_ps_ls_f3w_j22_d7w%3D%3D&payment_type=1

之後返回一個意圖,也是用這個意圖來打開支付寶app

alipays://platformapi/startApp?appId=20000125&orderSuffix=h5_route_token%3D%2294b58b83d6708541f9563c74fd14eba0%22%26is_h5_route%3D%22true%22#Intent;scheme=alipays;package=com.eg.android.AlipayGphone;end



webview的一些方法

接下來再介紹一些WebView的常用方法,具體演示會在後面章節的代碼裏統一展示。

String getUrl():獲取當前頁面的URL。

reload():重新reload當前的URL,即刷新。

boolean canGoBack():用來確認WebView裏是否還有可回退的歷史記錄。通常我們會在WebView裏重寫返回鍵的點擊事件,通過該方法判斷WebView裏是否還有歷史記錄,若有則返回上一頁。

boolean canGoForward():用來確認WebView是否還有可向前的歷史記錄。

boolean canGoBackOrForward(int steps):以當前的頁面爲起始點,用來確認WebView的歷史記錄是否足以後退或前進給定的步數,正數爲前進,負數爲後退。

goBack():在WebView歷史記錄後退到上一項。

goForward():在WebView歷史記錄裏前進到下一項。

goBackOrForward(int steps):以當前頁面爲起始點,前進或後退歷史記錄中指定的步數,正數爲前進,負數爲後退。

clearCache(boolean includeDiskFiles):清空網頁訪問留下的緩存數據。需要注意的時,由於緩存是全局的,所以只要是WebView用到的緩存都會被清空,即便其他地方也會使用到。該方法接受一個參數,從命名即可看出作用。若設爲false,則只清空內存裏的資源緩存,而不清空磁盤裏的。

clearHistory():清除當前webview訪問的歷史記錄。

clearFormData():清除自動完成填充的表單數據。需要注意的是,該方法僅僅清除當前表單域自動完成填充的表單數據,並不會清除WebView存儲到本地的數據。

onPause():當頁面被失去焦點被切換到後臺不可見狀態,需要執行onPause操作,該操作會通知內核安全地暫停所有動作,比如動畫的執行或定位的獲取等。需要注意的是該方法並不會暫停JavaScript的執行,若要暫停JavaScript的執行請使用接下來的這個方法。

onResume():在先前調用onPause()後,我們可以調用該方法來恢復WebView的運行。

pauseTimers():該方法面向全局整個應用程序的webview,它會暫停所有webview的layout,parsing,JavaScript Timer。當程序進入後臺時,該方法的調用可以降低CPU功耗。

resumeTimers():恢復pauseTimers時的所有操作。

destroy():銷燬WebView。需要注意的是:這個方法的調用應在WebView從父容器中被remove掉之後。我們可以手動地調用

用例如下:

rootLayout.removeView(webView);
webView.destroy();

getScrollY():該方法返回的當前可見區域的頂端距整個頁面頂端的距離,也就是當前內容滾動的距離。

getHeight():方法都返回當前WebView這個容器的高度。其實以上兩個方法都屬於View。

getContentHeight():該方法返回整個HTML頁面的高度,但該高度值並不等同於當前整個頁面的高度,因爲WebView有縮放功能, 所以當前整個頁面的高度實際上應該是原始HTML的高度再乘上縮放比例。因此,準確的判斷方法應該是:

if (webView.getContentHeight() * webView.getScale() == (webView.getHeight() + webView.getScrollY())) {
    //已經處於底端
}

if(webView.getScrollY() == 0){
    //處於頂端
}

pageUp(boolean top):將WebView展示的頁面滑動至頂部。

pageDown(boolean bottom):將WebView展示的頁面滑動至底部。

WebSettings

WebSettings是用來管理WebView配置的類。當WebView第一次創建時,內部會包含一個默認配置的集合。若我們想更改這些配置,便可以通過WebSettings裏的方法來進行設置。

WebSettings對象可以通過WebView.getSettings()獲得,它的生命週期是與它的WebView本身息息相關的,如果WebView被銷燬了,那麼任何由WebSettings調用的方法也同樣不能使用。

獲取WebSettings對象

WebSettings webSettings = webView.getSettings();

WebSettings常用方法

(幾乎所有的set方法都有相應的get方法,這裏就只介紹set了。另,所有未寫方法返回值類型的皆爲空類型) setJavaScriptEnabled(boolean flag):設置WebView是否可以運行JavaScript。

setJavaScriptCanOpenWindowsAutomatically(boolean flag):設置WebView是否可以由JavaScript自動打開窗口,默認爲false,通常與JavaScript的window.open()配合使用。

setAllowFileAccess(boolean allow):啓用或禁用WebView訪問文件數據。

setBlockNetworkImage(boolean flag):禁止或允許WebView從網絡上加載圖片。需要注意的是,如果設置是從禁止到允許的轉變的話,圖片數據並不會在設置改變後立刻去獲取,而是在WebView調用reload()的時候纔會生效。 這個時候,需要確保這個app擁有訪問Internet的權限,否則會拋出安全異常。 通常沒有禁止圖片加載的需求的時候,完全不用管這個方法,因爲當我們的app擁有訪問Internet的權限時,這個flag的默認值就是false。

setSupportZoom(boolean support):設置是否支持縮放。

setBuiltInZoomControls(boolean enabled):顯示或不顯示縮放按鈕(wap網頁不支持)。

setSupportMultipleWindows(boolean support):設置WebView是否支持多窗口。

setLayoutAlgorithm(WebSettings.LayoutAlgorithm l):指定WebView的頁面佈局顯示形式,調用該方法會引起頁面重繪。默認值爲LayoutAlgorithm#NARROW_COLUMNS。

setNeedInitialFocus(boolean flag):通知WebView是否需要設置一個節點獲取焦點當WebView#requestFocus(int,android.graphics.Rect)被調用時,默認爲true。

setAppCacheEnabled(boolean flag):啓用或禁用應用緩存。

setAppCachePath(String appCachePath):設置應用緩存路徑,這個路徑必須是可以讓app寫入文件的。該方法應該只被調用一次,重複調用會被無視~

setCacheMode(int mode):用來設置WebView的緩存模式。當我們加載頁面或從上一個頁面返回的時候,會按照設置的緩存模式去檢查並使用(或不使用)緩存。

緩存模式有四種:

  1. LOAD_DEFAULT:默認的緩存使用模式。在進行頁面前進或後退的操作時,如果緩存可用並未過期就優先加載緩存,否則從網絡上加載數據。這樣可以減少頁面的網絡請求次數。

  2. LOAD_CACHE_ELSE_NETWORK:只要緩存可用就加載緩存,哪怕它們已經過期失效。如果緩存不可用就從網絡上加載數據。

  3. LOAD_NO_CACHE:不加載緩存,只從網絡加載數據。

  4. LOAD_CACHE_ONLY:不從網絡加載數據,只從緩存加載數據。

通常我們可以根據網絡情況將這幾種模式結合使用,比如有網的時候使用LOAD_DEFAULT,離線時使用LOAD_CACHE_ONLY、LOAD_CACHE_ELSE_NETWORK,讓用戶不至於在離線時啥都看不到。

setDatabaseEnabled(boolean flag):啓用或禁用數據庫緩存。

setDomStorageEnabled(boolean flag):啓用或禁用DOM緩存。

setUserAgentString(String ua):設置WebView的UserAgent值。

setDefaultEncodingName(String encoding):設置編碼格式,通常都設爲“UTF-8”。

setStandardFontFamily(String font):設置標準的字體族,默認“sans-serif”。

setCursiveFontFamily:設置草書字體族,默認“cursive”。

setFantasyFontFamily:設置CursiveFont字體族,默認“cursive”。

setFixedFontFamily:設置混合字體族,默認“monospace”。

setSansSerifFontFamily:設置梵文字體族,默認“sans-serif”。

setSerifFontFamily:設置襯線字體族,默認“sans-serif”

setDefaultFixedFontSize(int size):設置默認填充字體大小,默認16,取值區間爲[1-72],超過範圍,使用其上限值。

setDefaultFontSize(int size):設置默認字體大小,默認16,取值區間[1-72],超過範圍,使用其上限值。

setMinimumFontSize:設置最小字體,默認8. 取值區間[1-72],超過範圍,使用其上限值。

setMinimumLogicalFontSize:設置最小邏輯字體,默認8. 取值區間[1-72],超過範圍,使用其上限值。

以上就是一些WebSettings的常用方法,具體的使用以及一些緩存的問題會在接下來的代碼以及文章中有更加直觀的說明。

WebViewClient

從名字上不難理解,這個類就像WebView的委託人一樣,是幫助WebView處理各種通知和請求事件的,我們可以稱他爲WebView的“內政大臣”。

  • onLoadResource(WebView view, String url):該方法在加載頁面資源時會回調,每一個資源(比如圖片)的加載都會調用一次。

  • onPageStarted(WebView view, String url, Bitmap favicon):該方法在WebView開始加載頁面且僅在Main frame loading(即整頁加載)時回調,一次Main frame的加載只會回調該方法一次。我們可以在這個方法裏設定開啓一個加載的動畫,告訴用戶程序在等待網絡的響應。

  • onPageFinished(WebView view, String url):該方法只在WebView完成一個頁面加載時調用一次(同樣也只在Main frame loading時調用),我們可以可以在此時關閉加載動畫,進行其他操作。

  • onReceivedError(WebView view, WebResourceRequest request, WebResourceError error):該方法在web頁面加載錯誤時回調,這些錯誤通常都是由無法與服務器正常連接引起的,最常見的就是網絡問題。 這個方法有兩個地方需要注意:

    1. 這個方法只在與服務器無法正常連接時調用,類似於服務器返回錯誤碼的那種錯誤(即HTTP ERROR),該方法是不會回調的,因爲你已經和服務器正常連接上了(全怪官方文檔(︶^︶));

    2. 這個方法是新版本的onReceivedError()方法,從API23開始引進,與舊方法onReceivedError(WebView view,int errorCode,String description,String failingUrl)不同的是,新方法在頁面局部加載發生錯誤時也會被調用(比如頁面裏兩個子Tab或者一張圖片)。這就意味着該方法的調用頻率可能會更加頻繁,所以我們應該在該方法裏執行儘量少的操作。

  • onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse):上一個方法提到onReceivedError並不會在服務器返回錯誤碼時被回調,那麼當我們需要捕捉HTTP ERROR並進行相應操作時應該怎麼辦呢?API23便引入了該方法。當服務器返回一個HTTP ERROR並且它的status code>=400時,該方法便會回調。這個方法的作用域並不侷限於Main Frame,任何資源的加載引發HTTP ERROR都會引起該方法的回調,所以我們也應該在該方法裏執行儘量少的操作,只進行非常必要的錯誤處理等。

  • onReceivedSslError(WebView view, SslErrorHandler handler, SslError error):當WebView加載某個資源引發SSL錯誤時會回調該方法,這時WebView要麼執行handler.cancel()取消加載,要麼執行handler.proceed()方法繼續加載(默認爲cancel)。需要注意的是,這個決定可能會被保留並在將來再次遇到SSL錯誤時執行同樣的操作。

  • WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request):當WebView需要請求某個數據時,這個方法可以攔截該請求來告知app並且允許app本身返回一個數據來替代我們原本要加載的數據。

比如你對web的某個js做了本地緩存,希望在加載該js時不再去請求服務器而是可以直接讀取本地緩存的js,這個方法就可以幫助你完成這個需求。你可以寫一些邏輯檢測這個request,並返回相應的數據,你返回的數據就會被WebView使用,如果你返回null,WebView會繼續向服務器請求。

  • boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request):哈~ 終於到了這個方法,在最開始的基礎演示時我們用到了這個方法。從實踐中我們知道,當我們沒有給WebView提供WebViewClient時,WebView如果要加載一個url會向ActivityManager尋求一個適合的處理者來加載該url(比如系統自帶的瀏覽器),這通常是我們不想看到的。於是我們需要給WebView提供一個WebViewClient,並重寫該方法返回true來告知WebView url的加載就在app中進行。這時便可以實現在app內訪問網頁。

  • onScaleChanged(WebView view, float oldScale, float newScale):當WebView得頁面Scale值發生改變時回調。

  • boolean shouldOverrideKeyEvent(WebView view, KeyEvent event):默認值爲false,重寫此方法並return true可以讓我們在WebView內處理按鍵事件。

WebChromeClient

如果說WebViewClient是幫助WebView處理各種通知、請求事件的“內政大臣”的話,那麼WebChromeClient就是輔助WebView處理Javascript的對話框,網站圖標,網站title,加載進度等偏外部事件的“外交大臣”。

  • onProgressChanged(WebView view, int newProgress):當頁面加載的進度發生改變時回調,用來告知主程序當前頁面的加載進度。

  • onReceivedIcon(WebView view, Bitmap icon):用來接收web頁面的icon,我們可以在這裏將該頁面的icon設置到Toolbar。

  • onReceivedTitle(WebView view, String title):用來接收web頁面的title,我們可以在這裏將頁面的title設置到Toolbar。

以下兩個方法是爲了支持web頁面進入全屏模式而存在的(比如播放視頻),如果不實現這兩個方法,該web上的內容便不能進入全屏模式。

  • onShowCustomView(View view, WebChromeClient.CustomViewCallback callback):該方法在當前頁面進入全屏模式時回調,主程序必須提供一個包含當前web內容(視頻 or Something)的自定義的View。

  • onHideCustomView():該方法在當前頁面退出全屏模式時回調,主程序應在這時隱藏之前show出來的View。

  • Bitmap getDefaultVideoPoster():當我們的Web頁面包含視頻時,我們可以在HTML裏爲它設置一個預覽圖,WebView會在繪製頁面時根據它的寬高爲它佈局。而當我們處於弱網狀態下時,我們沒有比較快的獲取該圖片,那WebView繪製頁面時的gitWidth()方法就會報出空指針異常~ 於是app就crash了。。

這時我們就需要重寫該方法,在我們尚未獲取web頁面上的video預覽圖時,給予它一個本地的圖片,避免空指針的發生。

  • View getVideoLoadingProgressView():重寫該方法可以在視頻loading時給予一個自定義的View,可以是加載圓環 or something。

  • boolean onJsAlert(WebView view, String url, String message, JsResult result):處理Javascript中的Alert對話框。

  • boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result):處理Javascript中的Prompt對話框。

  • boolean onJsConfirm(WebView view, String url, String message, JsResult result):處理Javascript中的Confirm對話框

  • boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams):該方法在用戶進行了web上某個需要上傳文件的操作時回調。我們應該在這裏打開一個文件選擇器,如果要取消這個請求我們可以調用filePathCallback.onReceiveValue(null)並返回true。

  • onPermissionRequest(PermissionRequest request):該方法在web頁面請求某個尚未被允許或拒絕的權限時回調,主程序在此時調用grant(String [])或deny()方法。如果該方法沒有被重寫,則默認拒絕web頁面請求的權限。

  • onPermissionRequestCanceled(PermissionRequest request):該方法在web權限申請權限被取消時回調,這時應該隱藏任何與之相關的UI界面。

Js與WebView交互

既然嗨鳥應用大行其道,那麼毫無疑問Android與JavaScript的交互我們也必須瞭解清楚,下面來介紹一下JavaScript與Android是如何互相調用的。

利用WebView調用網頁上的JavaScript代碼

在WebView中調用Js的基本格式爲webView.loadUrl("javascript:methodName(parameterValues)");

現有以下這段JavaScript代碼

  function readyToGo() {
      alert("Hello")
  }

  function alertMessage(message) {
      alert(message)
  }

  function getYourCar(){
      return "Car";
  }
  1. WebView調用JavaScript無參無返回值函數

    String call = "javascript:readyToGo()";
    webView.loadUrl(call);
    
  2. WebView調用JavScript有參無返回值函數

    String call = "javascript:alertMessage(\"" + "content" + "\")";
    webView.loadUrl(call);
    
  3. WebView調用JavaScript有參數有返回值的函數

      @TargetApi(Build.VERSION_CODES.KITKAT) private void evaluateJavaScript(WebView webView) {
        webView.evaluateJavascript("getYourCar()", new ValueCallback<String>() {
          @Override public void onReceiveValue(String s) {
            Log.d("findCar", s);
          }
        });
      }
    

JavaScript通過WebView調用Java代碼

從API19開始,Android提供了 @JavascriptInterface 對象註解的方式來建立起 Javascript 對象和 Android 原生對象的綁定,提供給JavScript調用的函數必須帶有 @JavascriptInterface。

演示一 JavaScript 調用 Android Toast 方法

  1. 編寫Java原生方法並用使用 @JavascriptInterface 註解

    @JavascriptInterface
    public void show(String s){
        Toast.makeText(getApplication(), s, Toast.LENGTH_SHORT).show();
    }
    
  2. 註冊JavaScriptInterface

    webView.addJavascriptInterface(this, "android");
    

    addJavascriptInterface 的作用是把 this 所代表的類映射爲 JavaScript 中的 android 對象。

  3. 編寫JavaScript代碼

    function toastClick(){
        window.android.show("JavaScript called~!");
    }
    

演示二 JavaScript調用有返回值的Java方法

  1. 定義一個帶返回值的Java方法,並使用 @JavaInterface:

      @JavaInterface
      public String getMessage(){
        return "Hello,boy~";
      }
    
  2. 添加JavaScript的映射

    webView.addJavaScriptInterface(this,"Android");
    
  3. 通過JavaScript調用Java方法

    function showHello(){
        var str=window.Android.getMessage();
        console.log(str);
    }
    

以上就是Js與WebView交互的一些介紹,希望能對你有幫助。

WebView加載優化

當WebView的使用頻率變得頻繁的時候,對於其各方面的優化就變得逐漸重要了起來。可以知道的是,我們每加載一個 H5頁面,都會有很多的請求。除了HTML主URL自身的請求外,HTML外部引用的 JS、CSS、字體文件、圖片都是一個個獨立的HTTP 請求,雖然請求是併發的,但當網頁整體數量達到一定程度的時候,再加上瀏覽器解析、渲染的時間,Web整體的加載時間變得很長。同時請求文件越多,消耗的流量也會越多。那麼對於加載的優化就變得非常重要,這方面的經驗我也沒有什麼別的,大概三個方面:

一個,就是資源本地化的問題

首先可以明確的是,以目前的網絡條件,通過網絡去服務器獲取資源的速度是遠遠比不上從本地讀取的。談論各種優化策略其實恰恰忽略了“需要加載”纔是阻擋速度提升的最大絆腳石。所以我們的思路一,就是將一些較重的資源比如js、css、圖片甚至HTML本身進行本地化處理,在每次加載到這些資源的時候,從本地讀取進行加載,可以簡單記憶爲“存·取·更”。

具體實現思路爲:

  1. ”——將上述重量級資源打包進apk文件,每次加載相應文件時時從本地取即可。也可不打包,在第一次加載時以及接下來的若干間隔時間裏動態下載存儲,將所有的資源文件都存在Android的asset目錄下;

  2. ”——重寫WebViewClient的WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)方法,通過一定的判別方法(例如正則表達式)攔截相應的請求,從本地讀取相應資源並返回;

  3. ”——建立起Cache Control機制,定期或使用API通知的形式控制本地資源的更新,保證本地資源是最新和可用的。

第二個,就是緩存的問題

倘若你不採用或不完全採用第一條資源本地化的思路,那麼你的WebView緩存是必須要開啓的(雖然這一思路和第一條有重合的地方)。

WebSettings settings = webView.getSettings();
settings.setAppCacheEnabled(true);
settings.setDatabaseEnabled(true);
settings.setDomStorageEnabled(true);//開啓DOM緩存
settings.setCacheMode(WebSettings.LOAD_DEFAULT);

在網絡正常時,採用默認緩存策略,在緩存可獲取並且沒有過期的情況下加載緩存,否則通過網絡獲取資源以減少頁面的網絡請求次數。

這裏值得提起的是,我們經常在app裏用WebView展示頁面時,並不想讓用戶覺得他是在訪問一個網頁。因爲倘若我們的app裏網頁非常多,而我們給用戶的感覺又都像在訪問網頁的話,我們的app便失去了意義。(我的意思是爲什麼用戶不直接使用瀏覽器呢?)

所以這時,離線緩存的問題就值得我們注意。我們需要讓用戶在沒有網的時候,依然能夠操作我們的app,而不是面對一個和瀏覽器裏的網絡錯誤一樣的頁面,哪怕他能進行的操作十分有限。

這裏我的思路是,在開啓緩存的前提下,WebView在加載頁面時檢測網絡變化,倘若在加載頁面時用戶的網絡突然斷掉,我們應當更改WebView的緩存策略。

ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if(networkInfo.isAvailable()) {
    settings.setCacheMode(WebSettings.LOAD_DEFAULT);//網絡正常時使用默認緩存策略
} else {
    settings.setCacheMode(WebSettings.LOAD_CACHE_ONLY);//網絡不可用時只使用緩存
}

既然有緩存,就要有緩存控制,與一相似的是我們也要建立緩存控制機制,定期或接受服務器通知來進行緩存的清空或更新。

第三個,就是延遲加載和執行js

在WebView中,onPageFinished()的回調意味着頁面加載的完成。但該方法會在JavScript腳本執行完成後纔會觸發,倘若我們要加載的頁面使用了JQuery,會在處理完DOM對象,執行完$(document).ready(function() {})後纔會渲染並顯示頁面。這是不可接受的,所以我們需要對Js進行延遲加載,當然這部分是Web前端的工作。

如果說還有什麼

那就是 JsBridge 一律不得濫用,這個對頁面加載的完成速度是有很大影響的,倘若一個頁面很多操作都通過 JSbridge 來控制,再怎麼優化也無濟於事(因爲畢竟有那麼多操作要實際執行)。同時要注意的是,不管你是否對資源進行緩存,都請將資源在服務器端進行壓縮。因爲無論是資源的獲取和更新,都是要從服務器獲取的,所以對於資源文件的壓縮其實是最直接也最應該做的事情之一,但是一般服務器端都會做好,所以主要就是上面這三件事。

實戰

介紹了這麼多,希望能對你有點幫助。接下來時純實戰時間,我會將上面所介紹的很多知識點在接下來的代碼裏實際應用一遍,希望能夠帶給你更加直觀的使用感受。

xml代碼

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
  <android.support.design.widget.AppBarLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      >
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:theme="@style/ThemeOverlay.AppCompat.Light"
        >
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="horizontal"
      >
    <Button
        android:id="@+id/btn_up"
        android:text="向上"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
    <Button
        android:id="@+id/btn_down"
        android:text="向下"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
    <Button
        android:id="@+id/btn_refresh"
        android:text="刷新"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
  </LinearLayout>

  <FrameLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      >
    <WebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    </WebView>
    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_gravity="center"
        android:visibility="gone"
        />
  </FrameLayout>

</LinearLayout>

Java代碼

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

  private WebView mWebView;
  private Toolbar mToolbar;
  private ProgressBar mProgressBar;
  private Button mBtnUp;
  private Button mBtnDown;
  private Button mBtnRefresh;

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

    //初始化Toolbar
    initAppBar();

    //初始化WebView
    initWebView();

    //初始化WebSettings
    initWebSettings();

    //初始化WebViewClient
    initWebViewClient();

    //初始化WebChromeClient
    initWebChromeClient();
  }

  private void initAppBar() {
    mToolbar = (Toolbar) findViewById(R.id.toolbar);
    mToolbar.setTitle("載入中...");
    mToolbar.setTitleTextColor(getResources().getColor(R.color.colorWhite));
    setSupportActionBar(mToolbar);
    if (getSupportActionBar() != null) {
      getSupportActionBar().setDisplayHomeAsUpEnabled(false);
    }
  }

  private void initWebView() {
    mWebView = (WebView) findViewById(R.id.web_view);
    mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
    String url = "https://www.baidu.com";
    mWebView.loadUrl(url);
  }

  //果你的應用沒有在WebView內直接使用JavaScript,不要調用setJavaScriptEnabled()
  //要忽略警告及加上以下註解
  @SuppressLint("SetJavaScriptEnabled") private void initWebSettings() {
    WebSettings settings = mWebView.getSettings();
    //支持獲取手勢焦點
    mWebView.requestFocusFromTouch();
    //支持JS
    settings.setJavaScriptEnabled(true);
    //支持插件
    settings.setPluginState(WebSettings.PluginState.ON);
    //設置適應屏幕
    settings.setUseWideViewPort(true);
    settings.setLoadWithOverviewMode(true);
    //支持縮放
    settings.setSupportZoom(false);
    //隱藏原生縮放控件
    settings.setDisplayZoomControls(false);
    //支持內容重新佈局
    settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
    //設置WebView是否支持多窗口
    settings.supportMultipleWindows();
    settings.setSupportMultipleWindows(true);
    //設置緩存模式
    settings.setDomStorageEnabled(true);
    settings.setDatabaseEnabled(true);
    settings.setCacheMode(WebSettings.LOAD_DEFAULT);
    settings.setAppCacheEnabled(true);
    settings.setAppCachePath(mWebView.getContext().getCacheDir().getAbsolutePath());
    //設置可訪問文件
    settings.setAllowContentAccess(true);
    //當webview調用requestFocus時爲webview設置節點
    settings.setNeedInitialFocus(true);
    //支持自動加載圖片
    if (Build.VERSION.SDK_INT >= 19) {
      settings.setLoadsImagesAutomatically(true);
    } else {
      settings.setLoadsImagesAutomatically(false);
    }
    settings.setNeedInitialFocus(true);
    //設置編碼格式
    settings.setDefaultTextEncodingName("UTF-8");
  }

  private void initWebViewClient() {
    mWebView.setWebViewClient(new WebViewClient() {
      //頁面開始加載時
      @Override public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);
        mProgressBar.setVisibility(View.VISIBLE);
      }

      //頁面完成加載時
      @Override public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        mProgressBar.setVisibility(View.GONE);
      }

      //是否在WebView內加載新頁面
      @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        view.loadUrl(request.toString());
        return true;
      }

      //網絡錯誤時回調方法
      @Override public void onReceivedError(WebView view, WebResourceRequest request,
          WebResourceError error) {
        /**
         * 在這裏寫網絡錯誤時的邏輯,比如顯示一個錯誤頁面
         *
         * 這裏我偷個懶不寫了
         */
        view.loadUrl("http://www.jianshu.com/users/d251eadadd37/latest_articles");
      }

      //捕捉HTTP ERROR並進行相應操作
      @Override public void onReceivedHttpError(WebView view, WebResourceRequest request,
          WebResourceResponse errorResponse) {
        view.loadUrl("http://www.jianshu.com/");
      }
    });
  }

  private void initWebChromeClient() {
    mWebView.setWebChromeClient(new WebChromeClient() {
      //默認的視頻展示圖
      private Bitmap mDefaultVideoPoster;

      @Override public void onReceivedTitle(WebView view, String title) {
        super.onReceivedTitle(view, title);
        setToolbarTitle(title);
      }

      @Override public Bitmap getDefaultVideoPoster() {
        if (mDefaultVideoPoster == null) {
          mDefaultVideoPoster = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
          return mDefaultVideoPoster;
        }
        return super.getDefaultVideoPoster();
      }
    });
  }

  /**
   * 設置Toolbar標題
   */
  public void setToolbarTitle(final String title) {
    Log.d("setToolbarTitle", " WebDetailActivity " + title);
    if (mToolbar != null) {
      mToolbar.post(new Runnable() {
        @Override public void run() {
          mToolbar.setTitle(TextUtils.isEmpty(title) ? getString(R.string.loading) : title);
        }
      });
    }
  }

  private void initView() {
    mBtnUp = (Button) findViewById(R.id.btn_up);
    mBtnDown = (Button) findViewById(R.id.btn_down);
    mBtnRefresh = (Button) findViewById(R.id.btn_refresh);

    mBtnUp.setOnClickListener(this);
    mBtnDown.setOnClickListener(this);
    mBtnRefresh.setOnClickListener(this);
  }

  @Override public void onClick(View v) {
    switch (v.getId()) {
      case R.id.btn_up:
        Toast.makeText(getApplicationContext(), "頁面向上", Toast.LENGTH_SHORT).show();
        mWebView.pageUp(true);
        break;
      case R.id.btn_down:
        Toast.makeText(getApplicationContext(), "頁面向下", Toast.LENGTH_SHORT).show();
        mWebView.pageDown(true);
        break;
      case R.id.btn_refresh:
        Toast.makeText(getApplicationContext(), "刷新", Toast.LENGTH_SHORT).show();
        mWebView.reload();
        break;
    }
  }

  @Override public boolean onKeyDown(int keyCode, KeyEvent event) {
    //如果按下的是回退鍵且歷史記錄裏確實還有頁面
    if((keyCode==KeyEvent.KEYCODE_BACK)&&mWebView.canGoBack()){
      mWebView.goBack();
      return true;
    }else {
      Toast.makeText(getApplicationContext(), "考試結束,恭喜您考試合格!", Toast.LENGTH_LONG).show();
    }
    return super.onKeyDown(keyCode, event);
  }
}
注意的問題

  1. mWebView.getSettings().setJavaScriptEnabled(true);  
  2. mWebView.addJavascriptInterface(new JSInterface(), "jsInterface");  
我們向WebView註冊一個名叫“jsInterface”的對象,然後在JS中可以訪問到jsInterface這個對象,就可以調用這個對象的一些方法,最終可以調用到Java代碼中,從而實現了JS與Java代碼的交互。
我們一起來看看關於addJavascriptInterface方法在Android官網的描述:
  • This method can be used to allow JavaScript to control the host application. This is a powerful feature, but also presents a security risk for applications targeted to API level JELLY_BEAN or below, because JavaScript could use reflection to access an injected object's public fields. Use of this method in a WebView containing untrusted content could allow an attacker to manipulate the host application in unintended ways, executing Java code with the permissions of the host application. Use extreme care when using this method in a WebView which could contain untrusted content.
  • JavaScript interacts with Java object on a private, background thread of this WebView. Care is therefore required to maintain thread safety.
  • The Java object's fields are not accessible.

簡單地說,就是用addJavascriptInterface可能導致不安全,因爲JS可能包含惡意代碼。今天我們要說的這個漏洞就是這個,當JS包含惡意代碼時,它可以幹任何事情。



解決方案

1,Android 4.2以上的系統
在Android 4.2以上的,google作了修正,通過在Java的遠程方法上面聲明一個@JavascriptInterface,如下面代碼:
[java] view plain copy
  1. class JsObject {  
  2.    @JavascriptInterface  
  3.    public String toString() { return "injectedObject"; }  
  4. }  
  5. webView.addJavascriptInterface(new JsObject(), "injectedObject");  
  6. webView.loadData("""text/html"null);  
  7. webView.loadUrl("javascript:alert(injectedObject.toString())");  
2,Android 4.2以下的系統
這個問題比較難解決,但也不是不能解決。
首先,我們肯定不能再調用addJavascriptInterface方法了。關於這個問題,最核心的就是要知道JS事件這一個動作,JS與Java進行交互我們知道,有以下幾種,比prompt, alert等,這樣的動作都會對應到WebChromeClient類中相應的方法,對於prompt,它對應的方法是onJsPrompt方法,這個方法的聲明如下:
[java] view plain copy
  1. public boolean onJsPrompt(WebView view, String url, String message,   
  2.     String defaultValue, JsPromptResult result)  
通過這個方法,JS能把信息(文本)傳遞到Java,而Java也能把信息(文本)傳遞到JS中,通知這個思路我們能不能找到解決方案呢?
經過一番嘗試與分析,找到一種比較可行的方案,請看下面幾個小點:
【1】讓JS調用一個Javascript方法,這個方法中是調用prompt方法,通過prompt把JS中的信息傳遞過來,這些信息應該是我們組合成的一段有意義的文本,可能包含:特定標識,方法名稱,參數等。在onJsPrompt方法中,我們去解析傳遞過來的文本,得到方法名,參數等,再通過反射機制,調用指定的方法,從而調用到Java對象的方法。
【2】關於返回值,可以通過prompt返回回去,這樣就可以把Java中方法的處理結果返回到Js中。
【3】我們需要動態生成一段聲明Javascript方法的JS腳本,通過loadUrl來加載它,從而註冊到html頁面中,具體的代碼如下:
  1. javascript:(function JsAddJavascriptInterface_(){  
  2.     if (typeof(window.jsInterface)!='undefined') {      
  3.         console.log('window.jsInterface_js_interface_name is exist!!');}   
  4.     else {  
  5.         window.jsInterface = {          
  6.             onButtonClick:function(arg0) {   
  7.                 return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onButtonClick',args:[arg0]}));  
  8.             },  
  9.               
  10.             onImageClick:function(arg0,arg1,arg2) {   
  11.                 prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onImageClick',args:[arg0,arg1,arg2]}));  
  12.             },  
  13.         };  
  14.     }  
  15. }  
  16. )()  
說明:
1,上面代碼中的jsInterface就是要註冊的對象名,它註冊了兩個方法,onButtonClick(arg0)和onImageClick(arg0, arg1, arg2),如果有返回值,就添加上return。
2,prompt中是我們約定的字符串,它包含特定的標識符MyApp:,後面包含了一串JSON字符串,它包含了方法名,參數,對象名等。
3,當JS調用onButtonClick或onImageClick時,就會回調到Java層中的onJsPrompt方法,我們再解析出方法名,參數,對象名,再反射調用方法。
4,window.jsInterface這表示在window上聲明瞭一個Js對象,聲明方法的形式是:方法名:function(參數1,參數2) 

5,一些思考

以下是在實現這個解決方案過程中遇到的一些問題和思考:
【1】生成Js方法後,加載這段Js的時機是什麼?
剛開始時在當WebView正常加載URL後去加載Js,但發現會存在問題,如果當WebView跳轉到下一個頁面時,之前加載的Js就可能無效了,所以需要再次加載。這個問題經過嘗試,需要在以下幾個方法中加載Js,它們是WebChromeClient和WebViewClient的方法:
  • onLoadResource
  • doUpdateVisitedHistory
  • onPageStarted
  • onPageFinished
  • onReceivedTitle
  • onProgressChanged
目前測試了這幾個地方,沒什麼問題,這裏我也不能完全確保沒有問題。

【2】需要過濾掉Object類的方法
由於通過反射的形式來得到指定對象的方法,他會把基類的方法也會得到,最頂層的基類就是Object,所以我們爲了不把getClass方法注入到Js中,所以我們需要把Object的公有方法過濾掉。這裏嚴格說來,應該有一個需要過濾方法的列表。目前我的實現中,需要過濾的方法有:
        "getClass",
        "hashCode",
        "notify",
        "notifyAll",
        "equals",
        "toString",
        "wait",

【3】通過手動loadUrl來加載一段js,這種方式難道js中的對象就不在window中嗎?也就是說,通過遍歷window的對象,不能找到我們通過loadUrl注入的js對象嗎?
關於這個問題,我們的方法是通過Js聲明的,通過loadUrl的形式來注入到頁面中,其實本質相當於把我們這動態生成的這一段Js直接寫在Html頁面中,所以,這些Js中的window中雖然包含了我們聲明的對象,但是他們並不是Java對象,他們是通過Js語法聲明的,所以不存在getClass之類的方法。本質上他們是Js對象。

【4】在Android 3.0以下,系統自己添加了一個叫searchBoxJavaBridge_的Js接口,要解決這個安全問題,我們也需要把這個接口刪除,調用removeJavascriptInterface方法。這個searchBoxJavaBridge_好像是跟google的搜索框相關的。

【5】在實現過程中,我們需要判斷系統版本是否在4.2以下,因爲在4.2以上,Android修復了這個安全問題。我們只是需要針對4.2以下的系統作修復。



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