【Android】混合開發(fā)之WebView的介紹及使用

前言

Android項目中WebView是必不可少的,越來越開的迭代節(jié)奏導致越來越多的App采用混合開發(fā),接著我們就介紹一下Android中WebView的使用。

一、混合開發(fā)的優(yōu)缺點:

優(yōu)點:
1.開發(fā)成本較低:Android和iOS使用一個地址就可以。
2.自動更新最新的web內(nèi)容。
3.兼容平臺較多。
缺點:
1.用戶體驗沒有原生的炫酷。
2.連接網(wǎng)絡等性能較差。
但瑕不掩瑜,對于不需要炫酷動效的簡單頁面如:用戶協(xié)議、注冊說明、banner跳轉(zhuǎn)的一些推廣頁面、圖文展示的文章等頁面都可以用WebView來完成!

二、如何配置WebView:

常用的類:
WebSettings用于管理WebView狀態(tài)配置

        webView.getSettings().setDisplayZoomControls(false);//是否使用內(nèi)置縮放機制
        webView.getSettings().setSupportZoom(true);// 是否支持變焦
        wvSignin.getSettings().setBuiltInZoomControls(true);// 設置WebView是否應該使用其內(nèi)置變焦機制,顯示放大縮小 
        webView.getSettings().setUseWideViewPort(true);//是否開啟控件viewport。默認false,自適應;true時標簽中指定寬度值生效
        webView.getSettings().setLoadWithOverviewMode(true);
        webView.setInitialScale(100);// 初始化時縮放
        webView.getSettings().setJavaScriptEnabled(true);

WebViewClient :主要幫助WebView處理各種通知、請求事件的。
shouldOverrideUrlLoading(打開網(wǎng)頁時不調(diào)用系統(tǒng)瀏覽器)
onLoadResource(在加載頁面資源時會調(diào)用)
onPageStart(設定加載開始的操作)
onPageFinish(在頁面加載結束時調(diào)用。我們可以關閉loading 條,切換程序動作)
onReceiveError(加載出錯調(diào)用)

  //首先選擇加載方式
  //方式1. 加載一個網(wǎng)頁:
  webView.loadUrl("http://www.google.com/");
  //方式2:加載apk包中的html頁面
  webView.loadUrl("file:///android_asset/test.html");
  //方式3:加載手機本地的html頁面
   webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");
webView.setWebViewClient(new WebViewClient() {
            @Override
            public void doUpdateVisitedHistory(WebView view, String url,
                                               boolean isReload) {
                super.doUpdateVisitedHistory(view, url, isReload);
            }

            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);// 點擊超鏈接的時候重新在原來進程上加載URL
                return true;
            }

            @Override
            public void onPageFinished(WebView view, String url) {
            }

        });

WebChromeClien:WebChromeClient主要輔助WebView處理Javascript的對話框、網(wǎng)站圖標、網(wǎng)站title、加載進度等
onCloseWindow(關閉WebView)
onCreateWindow()
onJsAlert (WebView上alert無效,需要定制WebChromeClient處理彈出)
onJsPrompt(支持javascript輸入框)
onJsConfirm(支持javascript的確認框)
onProgressChanged(獲得網(wǎng)頁的加載進度并顯示)
onReceivedIcon(獲取WebView的icon)
onReceivedTitle(獲取WebView的標題)
如果你的WebView只是用來處理一些html的頁面內(nèi)容,只用WebViewClient就行了,如果需要更豐富的處理效果,比如JS、進度條等,就要用到WebChromeClient。
進度條例子:

 webView.setWebChromeClient(webChromeClient)
 WebChromeClient webChromeClient = new WebChromeClient() {

        public void onProgressChanged(WebView view, int progress) {
            super.onProgressChanged(view, progress);
            wvProgressbar.setMax(100);
            if (progress < 100) {
                wvProgressbar.setVisibility(View.VISIBLE);
                if (progress < 10) {
                    wvProgressbar.setProgress(10);
                } else {
                    wvProgressbar.setProgress(progress);
                }
            } else {
                wvProgressbar.setProgress(100);
                wvProgressbar.setVisibility(View.GONE);
            }
        }

        @Override
        public void onReceivedTitle(WebView view, String title) {

            super.onReceivedTitle(view, title);
        }

    };
三、WebView和JS的交互:

之間的交互無非兩種,Android調(diào)用Js,Js調(diào)用Android。
1.Android調(diào)用Js:

  • 通過WebView的loadUrl()
 webView.loadUrl(url);里面可以配置userId、page等信息。
  • 通過WebView的evaluateJavascript()
比第一種方法效率更高,有返回值。但只支持Android4.4以上
 mWebView.evaluateJavascript(url, new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //結果
        }
    });

2.Js調(diào)用Android:

  • 通過 addJavascriptInterface(存在嚴重的漏洞問題)
被JS調(diào)用的方法必須加入@JavascriptInterface注解
 @JavascriptInterface
    public void getData(String msg) {
        System.out.println("JS調(diào)用了Android的getData方法");
    }
  • 通過WebViewClient 的方法回調(diào)攔截,常用的如 shouldOverrideUrlLoading ()方法回調(diào)攔截 url(不存在漏洞,但使用麻煩)
    Android通過 WebViewClient 的回調(diào)方法shouldOverrideUrlLoading ()攔截 url
    解析該 url 的協(xié)議
    如果檢測到是預先約定好的協(xié)議,就調(diào)用相應方法
四、封裝一下WebView讓使用更簡單~
public class MyWebView extends WebView {
    private int currentY = 0;
    private Context context;

    public MyWebView(Context context) {
        super(context);
        init(context);
    }

    public  MyWebView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

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

    private void init(Context context) {
        this.context = context;
        initWebSettings();
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(ev);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        currentY = t;
        super.onScrollChanged(l, t, oldl, oldt);
    }

    public int getCurrentY() {
        return currentY;
    }

    public boolean canZoomIn() {
        boolean canZoomIn = true;
        try {
            Field mActualScale = WebView.class.getDeclaredField("mActualScale");
            Field mMaxZoomScale = WebView.class
                    .getDeclaredField("mMaxZoomScale");
            mActualScale.setAccessible(true);
            mMaxZoomScale.setAccessible(true);
            canZoomIn = mActualScale.getFloat(this) < mMaxZoomScale
                    .getFloat(this);
        } catch (Exception e) {
            try {
                Field mZoomManager = WebView.class
                        .getDeclaredField("mZoomManager");
                if (mZoomManager != null) {
                    mZoomManager.setAccessible(true);
                    Object zoomManager = mZoomManager.get(this);
                    if (zoomManager != null) {
                        Field mEmbeddedZoomControl = zoomManager.getClass()
                                .getDeclaredField("mEmbeddedZoomControl");
                        if (mEmbeddedZoomControl != null) {
                            mEmbeddedZoomControl.setAccessible(true);
                            Object zoomControlEmbedded = mEmbeddedZoomControl
                                    .get(zoomManager);
                            if (zoomControlEmbedded != null) {
                                mZoomManager = zoomControlEmbedded.getClass()
                                        .getDeclaredField("mZoomManager");
                                if (mZoomManager != null) {
                                    mZoomManager.setAccessible(true);
                                    zoomManager = mZoomManager
                                            .get(zoomControlEmbedded);
                                    Method canZoomInMethod = zoomManager
                                            .getClass().getDeclaredMethod(
                                                    "canZoomIn");
                                    if (canZoomInMethod != null) {
                                        canZoomInMethod.setAccessible(true);
                                        Object canZoomInObj = canZoomInMethod
                                                .invoke(zoomManager,
                                                        new Object[0]);
                                        if (canZoomInObj != null
                                                && canZoomInObj instanceof Boolean) {
                                            return (Boolean) canZoomInObj;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            } catch (Exception e2) {
            }
        }
        return canZoomIn;
    }

    public boolean canZoomOut() {
        boolean canZoomOut = true;
        try {
            Field mActualScale = WebView.class.getDeclaredField("mActualScale");
            Field mMinZoomScale = WebView.class
                    .getDeclaredField("mMinZoomScale");
            Field mInZoomOverview = WebView.class
                    .getDeclaredField("mInZoomOverview");
            mActualScale.setAccessible(true);
            mMinZoomScale.setAccessible(true);
            mInZoomOverview.setAccessible(true);
            canZoomOut = mActualScale.getFloat(this) > mMinZoomScale
                    .getFloat(this) && !mInZoomOverview.getBoolean(this);
        } catch (Exception e) {
            try {
                Field mZoomManager = WebView.class
                        .getDeclaredField("mZoomManager");
                if (mZoomManager != null) {
                    mZoomManager.setAccessible(true);
                    Object zoomManager = mZoomManager.get(this);
                    if (zoomManager != null) {
                        Field mEmbeddedZoomControl = zoomManager.getClass()
                                .getDeclaredField("mEmbeddedZoomControl");
                        if (mEmbeddedZoomControl != null) {
                            mEmbeddedZoomControl.setAccessible(true);
                            Object zoomControlEmbedded = mEmbeddedZoomControl
                                    .get(zoomManager);
                            if (zoomControlEmbedded != null) {
                                mZoomManager = zoomControlEmbedded.getClass()
                                        .getDeclaredField("mZoomManager");
                                if (mZoomManager != null) {
                                    mZoomManager.setAccessible(true);
                                    zoomManager = mZoomManager
                                            .get(zoomControlEmbedded);
                                    Method canZoomOutMethod = zoomManager
                                            .getClass().getDeclaredMethod(
                                                    "canZoomOut");
                                    if (canZoomOutMethod != null) {
                                        canZoomOutMethod.setAccessible(true);
                                        Object canZoomOutObj = canZoomOutMethod
                                                .invoke(zoomManager,
                                                        new Object[0]);
                                        if (canZoomOutObj != null
                                                && canZoomOutObj instanceof Boolean) {
                                            return (Boolean) canZoomOutObj;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            } catch (Exception e2) {
            }
        }
        return canZoomOut;
    }

    @SuppressLint("SetJavaScriptEnabled")
    @SuppressWarnings("deprecation")
    private void initWebSettings() {
        WebSettings webSettings = getSettings();
        webSettings.setRenderPriority(RenderPriority.HIGH);
        webSettings.setTextSize(TextSize.NORMAL);// 設置字體
        
        // 設置支持縮放
        webSettings.setAllowFileAccess(true);// 設置可以訪問文件
        webSettings.setDomStorageEnabled(true);
        // 設置屏幕自適應
        if (Integer.parseInt(Build.VERSION.SDK) <= 10) {
            webSettings.setLayoutAlgorithm(LayoutAlgorithm.NARROW_COLUMNS);
        } else {
            webSettings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
        }
        // 啟用插件
        // webSettings.setPluginsEnabled(true);

        // 設置緩存
        webSettings.setAppCacheEnabled(true);
        webSettings.setAppCacheMaxSize(10 * 1204 * 1024);
        webSettings.setDatabaseEnabled(true);
        webSettings.setBlockNetworkImage(false);//延遲加載圖片,首先加載文字
        
        this.setBackgroundColor(context.getResources().getColor(
                R.color.white));
        webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
        // 去掉縮放按鈕
        webSettings.setDisplayZoomControls(false);
        webSettings.setSupportZoom(true);// 是否支持變焦
        webSettings.setBuiltInZoomControls(true);// 設置WebView是否應該使用其內(nèi)置變焦機制,顯示放大縮小 
        webSettings.setUseWideViewPort(true);
        webSettings.setLoadWithOverviewMode(true);
        this.setInitialScale(100);// 初始化時縮放
        webSettings.setJavaScriptEnabled(true);
        this.addJavascriptInterface(new JavaScriptinterface(context), "android");
        this.setWebViewClient(new WebViewClient() {
            @Override
            public void doUpdateVisitedHistory(WebView view, String url,
                                               boolean isReload) {
                super.doUpdateVisitedHistory(view, url, isReload);
            }

            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                DfheWebView.this.loadUrl(url);// 點擊超鏈接的時候重新在原來進程上加載URL
                return true;
            }

            @Override
            public void onPageFinished(WebView view, String url) {
            }

        });
    }
    
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        // TODO Auto-generated method stub
        return super.onKeyDown(keyCode, event);
    }
    

    public interface OnScrollListener {
        void onScroll();
    }
}
628180

五、WebView 踩過的坑:

1.WebView內(nèi)存泄漏問題
解決方案:
1.展示webview的activity可以另開一個進程,這樣就能和我們app的主進程分開了,即使webview產(chǎn)生了oom崩潰等問題也不會影響到主程序,在Androidmanifest.xml的activity標簽里加上Android:process=”packagename.web”就可以了,并且當這個 進程結束時,請手動調(diào)用System.exit(0)。

  1. 如果實在不想用開額外進程的方式解決webview 內(nèi)存泄露的問題,那么下面的方法很大程度上可以避免這種情況,在webview的 destroy方法里 調(diào)用這個方法就行了。
public void releaseAllWebViewCallback() {
         if (android.os.Build.VERSION.SDK_INT < 16) {
             try {
                 Field field = WebView.class.getDeclaredField("mWebViewCore");
                 field = field.getType().getDeclaredField("mBrowserFrame");
                 field = field.getType().getDeclaredField("sConfigCallback");
                 field.setAccessible(true);
                 field.set(null, null);
             } catch (NoSuchFieldException e) {
                 if (BuildConfig.DEBUG) {
                     e.printStackTrace();
                 }
             } catch (IllegalAccessException e) {
                 if (BuildConfig.DEBUG) {
                     e.printStackTrace();
                 }
             }
         } else {
             try {
                 Field sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback");
                 if (sConfigCallback != null) {
                     sConfigCallback.setAccessible(true);
                     sConfigCallback.set(null, null);
                 }
             } catch (NoSuchFieldException e) {
                 if (BuildConfig.DEBUG) {
                     e.printStackTrace();
                 }
             } catch (ClassNotFoundException e) {
                 if (BuildConfig.DEBUG) {
                     e.printStackTrace();
                 }
             } catch (IllegalAccessException e) {
                 if (BuildConfig.DEBUG) {
                     e.printStackTrace();
                 }
             }
         }
     }

2.getSettings().setBuiltInZoomControls(true) 引發(fā)的crash。
這個方法調(diào)用以后 如果你觸摸屏幕 彈出的提示框還沒消失的時候 你如果activity結束了 就會報錯了。3.0以上 4.4以下很多手機會出現(xiàn)這種情況。解決方法是在activity的onDestroy方法里手動的將webiew設置成 setVisibility(View.GONE)
3.WebView后臺耗電問題。
WebView會自己開啟一些線程,如果沒有正確的銷毀,這些殘留的線程會一直在后臺運行,導致耗費電量。還有在有的手機里,你如果webview加載的html里 有一些js 一直在執(zhí)行比如動畫之類的東西,如果此刻webview 掛在了后臺,這些資源是不會被釋放 用戶也無法感知,導致一直占有cpu 耗電特別快。
解決方案:在Activity.onDestroy()中直接調(diào)用System.exit(0),使得應用程序完全被移出虛擬機。在Activity的onstop和onresume里分別把setJavaScriptEnabled();給設置成false和true。

六、WebView進階,緩存原理:

原文鏈接:WebView緩存原理分析和應用
減少流量和資源的占用,加載完一次后Js沒有變化就不再發(fā)起網(wǎng)絡請求去加載網(wǎng)頁

  • 瀏覽器自帶的網(wǎng)頁數(shù)據(jù)緩存:瀏覽器自帶
  • H5緩存:由web頁面的開發(fā)者設置
    利用App Cache 來緩存js文件
    瀏覽器緩存機制是通過HTTP協(xié)議Header里的Cache-Control和Last-Modified等字段來控制的。
    接受響應時: 加載文件時,瀏覽器是否發(fā)出請求字段:
    Cache-Control:max-age=36000**,這表示緩存時長為36000秒。如果36000秒內(nèi)需要再次請求這個文件,那么瀏覽器不會發(fā)出請求,直接使用本地的緩存的文件。這是HTTP/1.1標準中的字段。
    發(fā)起請求時:服務器決定文件是否需要更新的字段:
    Last-Modified:Wed, 28 Sep 2016 09:24:35 GMT,表示這個文件最后的修改時間是2016年9月28日9點24分35秒。這個字段對于瀏覽器來說,會在下次請求的時候作為Request Header的If-Modified-Since字段帶上。例如瀏覽器緩存的文件已經(jīng)超過了Cache-Control,那么需要加載這個文件時,就會發(fā)出請求,請求的Header有一個字段為If-Modified-Since:Wed, 28 Sep 2016 09:24:35 GMT,服務器接收到請求后,會把文件的Last-Modified時間和這個時間對比,如果時間沒變,那么瀏覽器將返回304 Not Modified給瀏覽器,且content-length肯定是0個字節(jié)。如果時間有變化,那么服務器會返回200 OK,并返回相應的內(nèi)容給瀏覽器。
    WebView 如何設置:設置WebView的Cache Mode:
  • LOAD_CACHE_ONLY: 不使用網(wǎng)絡,只讀取本地緩存數(shù)據(jù)。
  • LOAD_DEFAULT: 根據(jù)cache-control決定是否從網(wǎng)絡上取數(shù)據(jù)。
  • LOAD_CACHE_NORMAL: API level 17中已經(jīng)廢棄,從API level 11開始作用同LOAD_DEFAULT模式
  • LOAD_NO_CACHE: 不使用緩存,只從網(wǎng)絡獲取數(shù)據(jù)。
  • LOAD_CACHE_ELSE_NETWORK,只要本地有,無論是否過期,或者no-cache,都使用緩存中的數(shù)據(jù)。本地沒有緩存時才從網(wǎng)絡上獲取。
    例子
WebSettings settings = webView.getSettings();
settings.setCacheMode(WebSettings.LOAD_DEFAULT);

瀏覽器默認緩存的路徑
WebView自帶的瀏覽器協(xié)議支持的緩存,在不同的系統(tǒng)版本上,位置是不一樣的。
H5的緩存
這個Cache是由開發(fā)Web頁面的開發(fā)者控制的,而不是由Native去控制的,但是Native里面的WebView也需要我們做一下設置才能支持H5的這個特性
1.工作原理
寫Web頁面代碼時,指定manifest屬性即可讓頁面使用App Cache。通常html頁面代碼會這么寫:

<html manifest="xxx.appcache">
</html>

xxx.appcache文件用的是相對路徑,這時appcache文件的路徑是和頁面一樣的。也可以使用的絕對路徑,但是域名要保持和頁面一致。
完整的xxx.appcache文件一般包括了3個section,基本格式如下:

CACHE MANIFEST
# 2017-05-13 v1.0.0
/bridge.js
NETWORK:
*
FALLBACK:
/404.html
  • CACHE MANIFEST下面文件就是要被瀏覽器緩存的文件

  • NETWORK下面的文件就是要被加載的文件

  • FALLBACK下面的文件是目標頁面加載失敗時的顯示的頁面
    AppCache工作的原理:
    當一個設置了manifest文件的html頁面被加載時,CACHE MANIFEST指定的文件就會被緩存到瀏覽器的App Cache目錄下面。當下次加載這個頁面時,會首先應用通過manifest已經(jīng)緩存過的文件,然后發(fā)起一個加載xxx.appcache文件的請求到服務器,如果xxx.appcache文件沒有被修改過,那么服務器會返回304 Not Modified給到瀏覽器,如果xxx.appcache文件被修改過,那么服務器會返回200 OK,并返回新的xxx.appcache文件的內(nèi)容給瀏覽器,瀏覽器收到之后,再把新的xxx.appcache文件中指定的內(nèi)容加載過來進行緩存。
    可以看到,AppCache緩存需要在每次加載頁面時都發(fā)出一個xxx.appcache的請求去檢查manifest文件是不是有更新(byte by byte)。根據(jù)這篇文章(H5 緩存機制淺析 移動端 Web 加載性能優(yōu)化)的介紹,AppCache有一些坑的地方,且官方已經(jīng)不推薦使用了,但目前主流的瀏覽器依然是支持的。文章里主要提到下面這些坑:

  • 要更新緩存的文件,需要更新包含它的 manifest 文件,那怕只加一個空格。常用的方法,是修改 manifest 文件注釋中的版本號。如:# 2012-02-21 v1.0.0

  • 被緩存的文件,瀏覽器是先使用,再通過檢查 manifest 文件是否有更新來更新緩存文件。這樣緩存文件可能用的不是最新的版本。

  • 在更新緩存過程中,如果有一個文件更新失敗,則整個更新會失敗。

  • manifest 和引用它的HTML要在相同 HOST。

  • manifest 文件中的文件列表,如果是相對路徑,則是相對 manifest 文件的相對路徑。

  • manifest 也有可能更新出錯,導致緩存文件更新失敗。

  • 沒有緩存的資源在已經(jīng)緩存的 HTML 中不能加載,即使有網(wǎng)絡。例如:http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/

  • manifest 文件本身不能被緩存,且 manifest 文件的更新使用的是瀏覽器緩存機制。所以 manifest 文件的 Cache-Control 緩存時間不能設置太長。

2.WebView如何設置才能支持AppCache

WebView默認是沒有開啟AppCache支持的,需要添加下面這幾行代碼來設置:

WebSettings webSettings = webView.getSettings();
webSettings.setAppCacheEnabled(true);
String cachePath = getApplicationContext().getCacheDir().getPath(); // 把內(nèi)部私有緩存目錄'/data/data/包名/cache/'作為WebView的AppCache的存儲路徑
webSettings.setAppCachePath(cachePath);
webSettings.setAppCacheMaxSize(5 * 1024 * 1024);

注意:WebSettings的setAppCacheEnabled和setAppCachePath都必須要調(diào)用才行。
3.存儲AppCache的路徑
按照Android SDK的API說明,setAppCachePath是可以用來設置AppCache路徑的,但是我實際測試發(fā)現(xiàn),不管你怎么設置這個路徑,設置到應用自己的內(nèi)部私有目錄還是外部SD卡,都無法生效。AppCache緩存文件最終都會存到/data/data/包名/app_webview/cache/Application Cache這個文件夾下面,在上面的Android 4.4和5.1系統(tǒng)目錄截圖可以看得到,但是如果你不調(diào)用setAppCachePath方法,WebView將不會產(chǎn)生這個目錄。這里有點讓我覺得奇怪,我猜測可能從某一個系統(tǒng)版本開始,為了緩存文件的完整性和安全性考慮,SDK實現(xiàn)的時候就吧AppCache緩存目錄設置到了內(nèi)部私有存儲。

關于WebView的基礎用法基本就這么多,了解本篇足以應付項目中的場景,但使用WebView也會遇到很多小問題,我會結合以往總結一下,再拜讀一下大家的博客遇到的問題總結一下~
如果喜歡請點個贊再走~
github地址:https://github.com/bigeyechou

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容