Hybrid開發(fā)之WebView使用方法及注意事項

工作這么長時間,細(xì)細(xì)想來有很長時間都在與WebView打交道。在Hybrid App的開發(fā)中,積累了一定的經(jīng)驗,在此做一個簡單的工作總結(jié)。

Hybrid開發(fā)中最常用的組件就是WebView了。WebView不僅用來展示W(wǎng)eb頁面,更是Web頁面和安卓手機(jī)Native之間溝通的橋梁。但由于歷史原因,android不同版本之間WebView不同,存在一些兼容性問題。

注:鑒于在市場上Android 4.0及以后的系統(tǒng)占90%之上,較多的開源工程的minSdkVersion為14。本人參與開發(fā)的app也只關(guān)注Android 4.0及以上系統(tǒng)的兼容性。

WebView

在Android 4.4系統(tǒng)之前,WebView一直采用WebKit內(nèi)核;而在Android 4.4及以后google采用了chromium內(nèi)核。二者的API變化不大,但是在某些場景下的表現(xiàn)有些微差異。整體來說chromium內(nèi)核更為高效,支持V8引擎解析Javascript。更重要的是Chromium支持遠(yuǎn)程調(diào)試。

開發(fā)中web頁面可使用console.log打印控制臺日志。此時可通過Android Studio的Logcat查看到打印信息。在4.4系統(tǒng)之前,WebView相關(guān)日志tag是webkit;而在4.4及之后,tag是chromium。日志中包括Javascript運(yùn)行中打印的日志,錯誤信息等,有助于分析Hybrid開發(fā)中遇到的問題。

I/chromium: [INFO:CONSOLE(1)] "The key "target-densitydpi" is not supported.", source: file:///data/user/0/com.tfzq.gcs.dev/files/www/m_tf/trade/indexTota.js?v=0.4672763997119571 (1)

創(chuàng)建WebView包括兩種方法,一是在Xml中配置;二是直接使用new動態(tài)創(chuàng)建。推薦使用第二種方式進(jìn)行開發(fā)。WebView可以使用loadUrl加載本地或線上的Web頁面,執(zhí)行javascript語句;也可以使用loadData直接加載html數(shù)據(jù)。另一方法loadDataWithBaseUrl在加載頁面中有本地圖片時可以使用。

使用WebView之前需要通過WebSettings進(jìn)行一定的配置。

WebSettings settings = getSettings();
 //默認(rèn)是false 設(shè)置true允許和js交互
 settings.setJavaScriptEnabled(true);
 //  WebSettings.LOAD_DEFAULT 如果本地緩存可用且沒有過期則使用本地緩存,否加載網(wǎng)絡(luò)數(shù)據(jù) 默認(rèn)值
 //  WebSettings.LOAD_CACHE_ELSE_NETWORK 優(yōu)先加載本地緩存數(shù)據(jù),無論緩存是否過期
 //  WebSettings.LOAD_NO_CACHE  只加載網(wǎng)絡(luò)數(shù)據(jù),不加載本地緩存
 //  WebSettings.LOAD_CACHE_ONLY 只加載緩存數(shù)據(jù),不加載網(wǎng)絡(luò)數(shù)據(jù)
 //Tips:有網(wǎng)絡(luò)可以使用LOAD_DEFAULT 沒有網(wǎng)時用LOAD_CACHE_ELSE_NETWORK
 settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
 //開啟 DOM storage API 功能 較大存儲空間,使用簡單
 settings.setDomStorageEnabled(true);
 //設(shè)置數(shù)據(jù)庫緩存路徑 存儲管理復(fù)雜數(shù)據(jù) 方便對數(shù)據(jù)進(jìn)行增加、刪除、修改、查詢 不推薦使用
 settings.setDatabaseEnabled(true);
 final String dbPath = context.getApplicationContext().getDir("db", Context.MODE_PRIVATE).getPath();
 settings.setDatabasePath(dbPath);
 //開啟 Application Caches 功能 方便構(gòu)建離線APP 不推薦使用
 settings.setAppCacheEnabled(true);
 final String cachePath = context.getApplicationContext().getDir("cache", Context.MODE_PRIVATE).getPath();
 settings.setAppCachePath(cachePath);
 settings.setAppCacheMaxSize(5 * 1024 * 1024);

WebChromeClient

WebChromeClient常用以下幾個回調(diào)方法

  • WebChromeClient. onProgressChanged
    頁面加載進(jìn)度回調(diào),progress從0-100??捎脕韺崿F(xiàn)自定義加載進(jìn)度條。

  • WebChromeClient. onReceivedTitle
    可用來接收當(dāng)前頁面的標(biāo)題,實現(xiàn)本地標(biāo)題欄的變化。

  • WebChromeClient.onJsPrompt
    可用來實現(xiàn)自定義的彈窗,也可用來實現(xiàn)安全的JSBridge。phonegap混合開發(fā)框架即使用onJsPrompt實現(xiàn)在Android 4.2以下系統(tǒng)的安全性。類似的方法還有onJsConfirm,onJsAlert。

在開發(fā)中遇到過界面相關(guān)問題,恍惚記得WebView使用chromium內(nèi)核之后,onJsPrompt方法運(yùn)行線程不在是主線程導(dǎo)致。年代久遠(yuǎn),有興趣的讀者可自行驗證。

WebViewClient

WebViewClient常用以下幾個回調(diào)方法。

  • WebViewClient.shouldOverrideUrlLoading
    在WebView加載Url前調(diào)用,app可攔截該方法來自己處理本次url的加載。該方法返回true代表app自己處理url;返回false代表WebView處理url。該方法可配合自定義的協(xié)議頭來區(qū)分url是事件或普通的web連接。JSBridge框架主要依賴此方法實現(xiàn)JS事件的回調(diào)。

  • WebViewClient.shouldInterceptRequest
    可攔截WebView對頁面中資源的加載,使用本地資源代替。曾經(jīng)在一項目中用該方法實現(xiàn)本地緩存。

  • WebViewClient.onPageStarted
    該方法表示頁面開始加載,理論上是只調(diào)用一次。但是在WebKit內(nèi)核開發(fā)時,遇到過調(diào)用次數(shù)超過一次的情況。

  • WebViewClient.onReceivedError
    頁面加載出現(xiàn)錯誤時調(diào)用該方法??捎脕矶ㄖ棋e誤頁面。

  • WebViewClient.onPageFinished
    該方法一般用來處理頁面加載完成時的一些操作。比如注入JSBridge框架代碼建立JS-Native通信通道。但是在我早期的開發(fā)經(jīng)驗中,4.*的系統(tǒng)上低概率出現(xiàn)onPageFinished方法未回調(diào)的問題。在開發(fā)測試中需要關(guān)注。Web頁面中很容易有一些自動跳轉(zhuǎn)邏輯,這時onPageFinished會被調(diào)用多次,當(dāng)然url的參數(shù)不同??蓞⒖?a target="_blank" rel="nofollow">How to listen for a Webview finishing loading a URL in Android?

  • WebViewClient

Web與Native通信

關(guān)于Web與Native之間的通信,可參考我的文章。介紹了基本的通信方法,及開源庫JsBridge的使用。
JSbridge系列解析(一):JS-Native調(diào)用方法
JSbridge系列解析(二):lzyzsd/JsBridge使用方法
JSbridge系列解析(三):lzyzsd/JsBridge源碼解析
JSbridge系列解析(四):Web端發(fā)消息給Native代碼流程具體分析

WebView內(nèi)存泄漏

網(wǎng)上搜索Webview,可以說最多的就是內(nèi)存泄漏相關(guān)的介紹,如說某篇文章中出現(xiàn)的下段。文章指出不要在xml中直接使用webview,否則可能出現(xiàn)webview所在activity的內(nèi)存泄漏。

webview的創(chuàng)建也是有技巧的,最好不要在layout.xml中使用webview,可以通過一個viewgroup容器,使用代碼動態(tài)往容器里addview(webview),這樣可以在onDestory()里銷毀掉webview及時清理內(nèi)存,另外需要注意創(chuàng)建webview需要使用applicationContext而不是activity的context,銷毀時不再占有activity對象,這個大家應(yīng)該都知道了,最后離開的時候需要及時銷毀webview,onDestory()中應(yīng)該先從viewgroup中remove掉webview,再調(diào)用webview.removeAllViews();webview.destory();

最初做hybrid相關(guān)開發(fā)時,4.4及以上系統(tǒng)的手機(jī)較少,確實存在webview.destroy()后無法釋放內(nèi)存的問題。但現(xiàn)在6.0系統(tǒng)已經(jīng)成了主流,為了進(jìn)一步驗證,我用紅米Note 4,Android 6.0系統(tǒng)進(jìn)行了測試。

代碼如下,webview直接定義在xml文件中,且onDestroy中沒有針對webview的destroy操作。通過模擬器驗證Android 5.0表現(xiàn)與6.0一致,back退出后內(nèi)存順利回收;但4.4系統(tǒng)就會出現(xiàn)內(nèi)存泄漏。

public class WebViewTestActivity extends Activity {
    private WebView webview;

    @Override
    public void onCreate(Bundle saveInstance){
        super.onCreate(saveInstance);
        setContentView(R.layout.activity_webview_test);

        webview = findViewById(R.id.webview);
        webview.loadUrl("https://www.baidu.com");

        //部分頁面,如百度主頁,如果不設(shè)置setJavaScriptEnabled為true,則顯示白屏
        WebSettings webSettings = webview.getSettings();
        webSettings.setJavaScriptEnabled(true);
    }

    @Override
    public void onDestroy(){
        super.onDestroy();
    }
}

多次進(jìn)入主界面后按Back鍵退出,通過執(zhí)行adb shell dumpsys meminfo 包名命令,監(jiān)控發(fā)現(xiàn)WebView及對應(yīng)Activity的資源釋放了。這也說明google官方做了一定的修復(fù)。但為了兼容低版本,仍建議通過ViewGroup容器動態(tài)添加WebView,使用完成后進(jìn)行清理操作。

public class WebViewTestActivity extends Activity {
    private WebView webview;
    private FrameLayout webviewContainer;

    @Override
    public void onCreate(Bundle saveInstance){
        super.onCreate(saveInstance);
        setContentView(R.layout.activity_webview_test);

        webviewContainer = findViewById(R.id.webview_container);

        webview = new WebView(this);
        webviewContainer.addView(webview);
        webview.loadUrl("https://www.baidu.com");

        //部分頁面,如百度主頁,如果不設(shè)置setJavaScriptEnabled為true,則顯示白屏
        WebSettings webSettings = webview.getSettings();
        webSettings.setJavaScriptEnabled(true);
    }

    @Override
    public void onDestroy(){
        super.onDestroy();

        if(webview != null){
            ViewGroup parent = (ViewGroup)webview.getParent();
            if (parent != null){
                parent.removeView(webview);
            }
            webview.removeAllViews();
            webview.destroy();
        }
    }
}

4.4的模擬器使用上面改進(jìn)的代碼,但仍出現(xiàn)內(nèi)存泄漏,不過是InputMethodManager泄漏引起。后面有時間再研究這個。

網(wǎng)上另一種解決內(nèi)存泄漏的方法,是在創(chuàng)建WebView時使用Application的Context。但是如果Web頁面中出現(xiàn)Video時,會出現(xiàn)如果崩潰。Web頁面彈框的場景未測試。

W/System.err: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
W/System.err:     at android.app.ContextImpl.startActivity(ContextImpl.java:1034)
W/System.err:     at android.app.ContextImpl.startActivity(ContextImpl.java:1021)
W/System.err:     at android.content.ContextWrapper.startActivity(ContextWrapper.java:311)
W/System.err:     at com.android.webview.chromium.WebViewContentsClientAdapter$NullWebViewClient.shouldOverrideUrlLoading(WebViewContentsClientAdapter.java:196)
W/System.err:     at com.android.webview.chromium.WebViewContentsClientAdapter.shouldOverrideUrlLoading(WebViewContentsClientAdapter.java:293)
W/System.err:     at com.android.org.chromium.android_webview.AwContentsClientBridge.shouldOverrideUrlLoading(AwContentsClientBridge.java:96)
W/System.err:     at com.android.org.chromium.base.SystemMessageHandler.nativeDoRunLoopOnce(Native Method)
W/System.err:     at com.android.org.chromium.base.SystemMessageHandler.handleMessage(SystemMessageHandler.java:27)
W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:102)
W/System.err:     at android.os.Looper.loop(Looper.java:136)
W/System.err:     at android.app.ActivityThread.main(ActivityThread.java:5017)
W/System.err:     at java.lang.reflect.Method.invokeNative(Native Method)
W/System.err:     at java.lang.reflect.Method.invoke(Method.java:515)
W/System.err:     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
W/System.err:     at dalvik.system.NativeStart.main(Native Method)
A/libc: Fatal signal 6 (SIGABRT) at 0x0000084d (code=-6), thread 2125 (n.myapplication)

WebView如果執(zhí)行destory操作,則后續(xù)不能再進(jìn)行l(wèi)oadUrl的操作,否則會出現(xiàn)白屏。這一點(diǎn)在webView的復(fù)用時需要考慮。同時WebView復(fù)用時,

實際測試WebView復(fù)用的場景,將WebView置為static變量,下列代碼僅有示范的作用,不具實際的意義。目前我仍未想到WebView復(fù)用的意義所在。注意在界面銷毀時需要將WebView從父布局中remove,避免持有父布局引用導(dǎo)致當(dāng)前界面的內(nèi)存泄漏。但是仍無辦法解決WebView創(chuàng)建時持有的Activity的內(nèi)存泄漏。最初我懷疑用復(fù)用的WebVIew播放視頻會出現(xiàn)問題,畢竟其持有的Context已不可見,但實際測試運(yùn)行視頻OK。

public class WebViewTestActivity extends Activity {
    private static WebView webview;
    private FrameLayout webviewContainer;

    @Override
    public void onCreate(Bundle saveInstance){
        super.onCreate(saveInstance);
        setContentView(R.layout.activity_webview_test);

        webviewContainer = findViewById(R.id.webview_container);

        if (webview == null) {
            webview = new WebView(this);
        }

        webviewContainer.addView(webview);
        webview.loadUrl("https://www.baidu.com");

        //部分頁面,如百度主頁,如果不設(shè)置setJavaScriptEnabled為true,則顯示白屏
        WebSettings webSettings = webview.getSettings();
        webSettings.setJavaScriptEnabled(true);
    }

    @Override
    public void onDestroy(){
        super.onDestroy();

        if(webview != null){
            ViewGroup parent = (ViewGroup)webview.getParent();
            if (parent != null){
                parent.removeView(webview);
            }
            webview.removeAllViews();
//            webview.destroy();
        }
    }
}

關(guān)于內(nèi)存泄漏的另一種解決方案,即將顯示W(wǎng)ebView的activity運(yùn)行在另外的進(jìn)程。這樣在WebView界面關(guān)閉時將該進(jìn)程直接kill,避免對主程序的影響。但是該方法對多WebView的程序不太適合,畢竟跨進(jìn)程通信成本較大,容易出現(xiàn)各種問題。該方案只適應(yīng)一定場景,了解即可。

參考:
Android Webview的一些使用總結(jié)和遇到過得坑
Android:你不知道的 WebView 使用漏洞
Android 各個版本W(wǎng)ebView
安卓webview的一些坑
H5 緩存機(jī)制淺析 移動端 Web 加載性能優(yōu)化

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

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

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