天天看點

Android 攔截WebView加載URL,控制其加載CSS、JS資源

版權聲明:本文為部落客原創文章,轉載請标明出處。 https://blog.csdn.net/lyhhj/article/details/49517537

緒論

最近在項目中有了這樣一個需求,我們都知道WebView加載網頁可以緩存,但是web端想讓客服端根據需求來緩存網頁,也就是說web端在設定了http響應頭,我根據這個頭來攔截WebView加載網頁,去執行網絡加載還是本地緩存加載。這個需求之前一直沒聽說過,在網上搜了一下,發現有攔截WebView加載網頁這個方法,研究了一下,最終實作了,今天小編分享給大家這個開發經驗:

WebView緩存機制

1.緩存模式

Android的WebView有五種緩存模式

1.LOAD_CACHE_ONLY //不使用網絡,隻讀取本地緩存資料

2.LOAD_DEFAULT //根據cache-control決定是否從網絡上取資料。

3.LOAD_CACHE_NORMAL //API level 17中已經廢棄, 從API level 11開始作用同LOAD_DEFAULT模式

4.LOAD_NO_CACHE //不使用緩存,隻從網絡擷取資料

5.LOAD_CACHE_ELSE_NETWORK //隻要本地有,無論是否過期,或者no-cache,都使用緩存中的資料

2.緩存路徑

/data/data/包名/cache/

/data/data/包名/database/webview.db

/data/data/包名/database/webviewCache.db

3.設定緩存模式

mWebSetting.setLoadWithOverviewMode(true);
        mWebSetting.setDomStorageEnabled(true);
        mWebSetting.setAppCacheMaxSize(1024 * 1024 * 8);//設定緩存大小
        //設定緩存路徑
        appCacheDir = Environment.getExternalStorageDirectory().getPath() + "/xxx/cache";
        File fileSD = new File(appCacheDir);
        if (!fileSD.exists()) {
            fileSD.mkdir();
        }
        mWebSetting.setAppCachePath(appCacheDir);
        mWebSetting.setAllowContentAccess(true);
        mWebSetting.setAppCacheEnabled(true);
        if (CheckHasNet.isNetWorkOk(context)) {
            //有網絡網絡加載
            mWebSetting.setCacheMode(WebSettings.LOAD_DEFAULT);
        } else {
            //無網時本地緩存加載
            mWebSetting.setCacheMode(WebSettings.LOAD_CACHE_ONLY);
        }           

4.清除緩存

/**
     * 清除WebView緩存
     */ 
    public void clearWebViewCache(){ 

        //清理Webview緩存資料庫 
        try { 
            deleteDatabase("webview.db");  
            deleteDatabase("webviewCache.db"); 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } 

        //WebView 緩存檔案 
        File appCacheDir = new File(getFilesDir().getAbsolutePath()+APP_CACAHE_DIRNAME); 
        Log.e(TAG, "appCacheDir path="+appCacheDir.getAbsolutePath()); 

        File webviewCacheDir = new File(getCacheDir().getAbsolutePath()+"/webviewCache"); 
        Log.e(TAG, "webviewCacheDir path="+webviewCacheDir.getAbsolutePath()); 

        //删除webview 緩存目錄 
        if(webviewCacheDir.exists()){ 
            deleteFile(webviewCacheDir); 
        } 
        //删除webview 緩存 緩存目錄 
        if(appCacheDir.exists()){ 
            deleteFile(appCacheDir); 
        } 
    }           
好了,我們了解了WebView的緩存緩存機制了之後來看看到底怎麼攔截WebView加載網頁:
           

實作原理

1.要想攔截WebView加載網頁我們必須重寫WebViewClient類,在WebViewClient類中我們重寫shouldInterceptRequest()方法,看方法名一目了然,攔截http請求,肯定是這個方法。

2.擷取http請求的頭,看是否包含所設定的flag,如果包含這個flag說明web端想讓我們儲存這個html,那麼我們改怎麼手動儲存這個html呢?

1)擷取url的connection

2)利用connection.getHeaderField(“flag”)擷取http請求頭資訊

3)得到請求的内容區資料的類型String contentType = connection.getContentType();

4)擷取html的編碼格式

5)将html的内容寫入檔案(具體代碼下面會介紹)

*3.注意:因為控制WebView加載網頁的方法需要三個參數

public WebResourceResponse(String mimeType, String encoding, InputStream data)

mimeType:也就是我們第3步擷取的内容區資料的類型

encoding:就是html的編碼格式

data:本地寫入的html檔案*

那麼問題來了,我們可以把html代碼寫到本地緩存檔案中,而這個html所對應的mimeType和encoding我們存到哪裡呢?因為http的頭資訊是http請求的屬性,我們存到SP中?存到資料庫中?好像都不行,無法對應關系啊。這塊小編想了好久,因為小編沒怎麼寫過檔案讀取這一塊,最後想到把這兩個參數一起存到html檔案開始的幾個位元組,每次加載先讀取這兩個參數就OK了,不過這樣讀寫比較麻煩,也比較費時,但是卻給背景減少了不小的壓力。看一下代碼具體怎麼實作的吧。

class MyWebClient extends WebViewClient {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            return true;
        }

        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
            if (!"get".equals(request.getMethod().toLowerCase())) {
                return super.shouldInterceptRequest(view, request);
            }
            String url = request.getUrl().toString();

            //todo:計算url的hash
            String md5URL = YUtils.md5(url);

            //讀取緩存的html頁面
            File file = new File(appCacheDir + File.separator + md5URL);
            if (file.exists()) {
                FileInputStream fileInputStream = null;
                try {
                    fileInputStream = new FileInputStream(file);
                    Log.e(">>>>>>>>>", "讀緩存");
                    return new WebResourceResponse(YUtils.readBlock(fileInputStream), YUtils.readBlock(fileInputStream), fileInputStream);
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
                return null;

            }

            try {
                URL uri = new URL(url);
                URLConnection connection = uri.openConnection();
                InputStream uristream = connection.getInputStream();
                String cache = connection.getHeaderField("Ddbuild-Cache");
                String contentType = connection.getContentType();
                //text/html; charset=utf-8
                String mimeType = "";
                String encoding = "";
                if (contentType != null && !"".equals(contentType)) {
                    if (contentType.indexOf(";") != -1) {
                        String[] args = contentType.split(";");
                        mimeType = args[0];
                        String[] args2 = args[1].trim().split("=");
                        if (args.length == 2 && args2[0].trim().toLowerCase().equals("charset")) {
                            encoding = args2[1].trim();
                        } else {

                            encoding = "utf-8";
                        }
                    } else {
                        mimeType = contentType;
                        encoding = "utf-8";
                    }
                }

                if ("1".equals(cache)) {
                    //todo:緩存uristream
                    FileOutputStream output = new FileOutputStream(file);
                    int read_len;
                    byte[] buffer = new byte[1024];


                    YUtils.writeBlock(output, mimeType);
                    YUtils.writeBlock(output, encoding);
                    while ((read_len = uristream.read(buffer)) > 0) {
                        output.write(buffer, 0, read_len);
                    }
                    output.close();
                    uristream.close();

                    FileInputStream fileInputStream = new FileInputStream(file);
                    YUtils.readBlock(fileInputStream);
                    YUtils.readBlock(fileInputStream);
                    Log.e(">>>>>>>>>", "讀緩存");
                    return new WebResourceResponse(mimeType, encoding, fileInputStream);
                } else {
                    Log.e(">>>>>>>>>", "網絡加載");
                    return new WebResourceResponse(mimeType, encoding, uristream);
                }

            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

            return null;
        }
       }
  //這裡面讀寫操作比較多,還有截取那兩個屬性的字元串稍微有點麻煩
   /**
     * int轉byte
     * by黃海傑 at:2015-10-29 16:15:06
     * @param iSource
     * @param iArrayLen
     * @return
     */
    public static byte[] toByteArray(int iSource, int iArrayLen) {
        byte[] bLocalArr = new byte[iArrayLen];
        for (int i = 0; (i < 4) && (i < iArrayLen); i++) {
            bLocalArr[i] = (byte) (iSource >> 8 * i & 0xFF);
        }
        return bLocalArr;
    }

    /**
     * byte轉int
     * by黃海傑 at:2015-10-29 16:14:37
     * @param bRefArr
     * @return
     */
    // 将byte數組bRefArr轉為一個整數,位元組數組的低位是整型的低位元組位
    public static int toInt(byte[] bRefArr) {
        int iOutcome = 0;
        byte bLoop;

        for (int i = 0; i < bRefArr.length; i++) {
            bLoop = bRefArr[i];
            iOutcome += (bLoop & 0xFF) << (8 * i);
        }
        return iOutcome;
    }

    /**
     * 寫入JS相關檔案
     * by黃海傑 at:2015-10-29 16:14:01
     * @param output
     * @param str
     */
    public static void writeBlock(OutputStream output, String str) {
        try {
            byte[] buffer = str.getBytes("utf-8");
            int len = buffer.length;
            byte[] len_buffer = toByteArray(len, 4);
            output.write(len_buffer);
            output.write(buffer);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 讀取JS相關檔案
     * by黃海傑 at:2015-10-29 16:14:19
     * @param input
     * @return
     */
    public static String readBlock(InputStream input) {
        try {
            byte[] len_buffer = new byte[4];
            input.read(len_buffer);
            int len = toInt(len_buffer);
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            int read_len = 0;
            byte[] buffer = new byte[len];
            while ((read_len = input.read(buffer)) > 0) {
                len -= read_len;
                output.write(buffer, 0, read_len);
                if (len <= 0) {
                    break;
                }
            }
            buffer = output.toByteArray();
            output.close();
            return new String(buffer,"utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
 //為了加密我們的html我們把url轉成md5
 /**
     * 字元串轉MD5
     * by黃海傑 at:2015-10-29 16:15:32
     * @param string
     * @return
     */
    public static String md5(String string) {

        byte[] hash;

        try {

            hash = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8"));

        } catch (NoSuchAlgorithmException e) {

            throw new RuntimeException("Huh, MD5 should be supported?", e);

        } catch (UnsupportedEncodingException e) {

            throw new RuntimeException("Huh, UTF-8 should be supported?", e);

        }


        StringBuilder hex = new StringBuilder(hash.length * 2);

        for (byte b : hash) {

            if ((b & 0xFF) < 0x10) hex.append("0");

            hex.append(Integer.toHexString(b & 0xFF));

        }

        return hex.toString();

    }           

注意

功能雖然實作了,但是發現一個比較棘手的問題,就是shouldInterceptRequest()方法有兩個:

1.public WebResourceResponse shouldInterceptRequest(WebView view, String url) {

return super.shouldInterceptRequest(view, url);

}

2.public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {}

重載的方法,第一個是已經廢棄了的,SDK 20以下的會執行1,SDK20以上的會執行2,那麼問題又來了,因為我們在擷取http請求的時候要判斷是post()請求還是get()請求,如果是post請求我們就網絡加載,而get請求才去加載本地緩存,因為post請求需要參數。是以大家可以看到我上面僅僅實作了SDK20以上的新方法,而沒有去關SDK20以下廢棄的那個函數,因為廢棄的那個函數根本擷取不到請求方式,不知道是不是因為這個原因才将這個方法廢棄的。這一塊小編會繼續研究的,一定要解決這個問題,小編已經有了思路不知道能不能實作,接下來小編會去研究一下2014年新出的CrossWalk這個浏覽器插件,據說重寫了底層,比webview能更好的相容h5新特性,更穩定,屏蔽安卓不同版本的webview的相容性問題

生命就在于折騰,小編就喜歡折騰,将Android折騰到底O(∩_∩)O~~