Android爬坑之旅之WebView

2021-02-13 安卓巴士Android開發者門戶

不知不覺,Hybrid App已經成了目前比較主流的一種開發方式。

對於用戶體驗要求較高或者與硬體交互較多的功能我們一般都會採用Native原生的方式來實現。
而用戶交互少,偏展示類,活動類的功能我們則通常採用H5的方式來實現,
例如新聞類的app,詳情展示頁一般就是H5的頁面

一方面圖文排版上web有著先天的優勢,同時純展示類的頁面在目前的行動裝置上,性能體驗已經很難讓用戶分辨是網頁還是原生了;

另一方面,H5的頁面跨平臺,方便在原生客戶端上實現分享功能,擁有較強的傳播性,我們平時常見的活動頁面也擁有這樣的優勢,所以你看到的活動頁面也基本都是H5,只需輕輕一點就能分享到各個平臺;

同時,H5的頁面開發降低了開發成本,一套代碼,web,android,ios都能訪問。(然而實際開發過程中,H5的適配也都是各種淚)

既然Hybrid App有這麼多優勢,那在Android中我們通過什麼樣的方式在原生項目中嵌入H5頁面呢?

那就不得不提到我們的WebVew了,作為官方唯一用來顯示web的組件,
展示網頁這樣的任務也只能交給它了。

A View that displays web pages. This class is the basis upon which you can roll your own web browser or simply display some online content within your Activity. It uses the WebKit rendering engine to display web pages and includes methods to navigate forward and backward through a history, zoom in and out, perform text searches and more.

引用官方文檔的一句話:
WebView是一個用來在Activity中顯示我們網頁的視圖組件,它通過webkit渲染引擎渲染和顯示我們的web頁面,並且包含了web的歷史導航操法,頁面放大縮小,文本搜索等方法。

我們首先來看一下WebView的基本用法:

WebView的基本用法

關於WebView的基本用法,大部分人也是輕車熟路,
本來也是寫了一部分,無意中發現有位博主的博客對WebView的介紹實在太過詳細,像我這樣的懶人,有更好的文章是不會自己去寫的,
所以刪了自己寫的,將大牛博主的博客分享出來,感興趣同學的可以一起看一看:

Android WebView 開發詳解(一)
Android WebView 開發詳解(二)
Android WebView 開發詳解(三)

了解完WebView的基本用法,那就來總結下最近項目中遇到的關於WebView的坑

項目中使用WebView遇到的問題WebView界面的原生標題設置Picture

如圖所示,
一般情況下,我們WebView所在界面由頂部帶標題的原生導航欄WebView的內容部分組成,
而WebView中的界面可能在點擊後還會再跳其他Web頁面(如圖點擊請假會在當前WebView跳轉請假的Web頁面)。

由於點擊內容的不確定性,所以通常情況下,最簡單的做法就是捕獲h5頁面的<title>標籤來進行標題設置。

對於捕獲<title>標籤內容的方式,WebView也很好地提供了支持,我們可以通過繼承WebChromeClient的onReceivedTitle來進行獲取:

 private class WebViewChromeClient extends WebChromeClient {

        @Override
        public void onReceivedTitle(WebView view, String title) {
            super.onReceivedTitle(view, title);
            mTitleText.setTitle(String.valueOf(view.getTitle()));
        }
    }

然而這樣的方式在實際使用中有一個問題:

當通過 webView.goBack() 方式返回上一級Web頁面的時候不會觸發這個方法,因此會導致標題無法跟隨歷史記錄返回上一級頁面。

所以在項目中,我們可以通過重寫 WebViewClient 的 onPageFinished 方法,在 onPageFinished 中對界面標題進行設置。
因為不管是歷史記錄的返回還是點擊跳轉都會觸發頁面加載,當頁面加載完成時(不包括js動態創建以及img圖片加載完畢)都會觸發onPageFinished 這個方法,
此時我們去獲取<title>的標題內容不會有任何問題,可以確保在頁面返回時能夠獲取到正確的標題。

      mWebView.setWebViewClient(new WebViewClient(){

            
            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
                mToolbar.setTitle(String.valueOf(view.getTitle()));
                Log.i (LOG_TAG, "onPageFinished");
            }
        });

注: 這種做法有一個缺陷,就是返回上一個界面的時候,等頁面加載完成的時候標題才會顯示出來,為了更好地優化,我們可以創建一個集合用來保存我們的標題,加載url的時候把標題添加進集合,當返回上一級頁面的時候,從集合中取出標題進行顯示,同時從集合中移除標題。

WebView中的Web頁面存在 <input type='file'>標籤時無法打開文件選擇器

在我們的手機瀏覽器中,當web頁面中有<input type='file'> 按鈕標籤的時候點擊會自動打開系統的文件選擇器,然而這個功能在主流系統的WebView中沒有被默認實現,
因此,為了讓<input type='file'> 點擊時能夠打開系統的文件選擇器,我們必須通過重寫 WebChromeClient 來實現點擊 <input type='file'>打開系統文件選擇器。

代碼如下:

public class MainActivity extends AppCompatActivity {

    
    protected ValueCallback<Uri> mFileUploadCallbackFirst;
    
    protected ValueCallback<Uri[]> mFileUploadCallbackSecond;

    protected static final int REQUEST_CODE_FILE_PICKER = 51426;


    protected String mUploadableFileTypes = "image/*";

    private WebView mWebView;

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

        initWebView();
    }

    private void initWebView() {
        mWebView = (WebView) findViewById(R.id.my_webview);

        mWebView.loadUrl("file:///android_asset/index.html");
        mWebView.setWebChromeClient(new OpenFileChromeClient());
    }

    private class OpenFileChromeClient extends WebChromeClient {

        
        @SuppressWarnings("unused")
        public void openFileChooser(ValueCallback<Uri> uploadMsg) {
            openFileChooser(uploadMsg, null);
        }

        
        public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
            openFileChooser(uploadMsg, acceptType, null);
        }

        
        @SuppressWarnings("unused")
        public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
            openFileInput(uploadMsg, null, false);
        }

        
        @SuppressWarnings("all")
        public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
            if (Build.VERSION.SDK_INT >= 21) {
                final boolean allowMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE;


                openFileInput(null, filePathCallback, allowMultiple);

                return true;
            }
            else {
                return false;
            }
        }


    }

    @SuppressLint("NewApi")
    protected void openFileInput(final ValueCallback<Uri> fileUploadCallbackFirst, final ValueCallback<Uri[]> fileUploadCallbackSecond, final boolean allowMultiple) {
        
        if (mFileUploadCallbackFirst != null) {
            mFileUploadCallbackFirst.onReceiveValue(null);
        }
        mFileUploadCallbackFirst = fileUploadCallbackFirst;

        
        if (mFileUploadCallbackSecond != null) {
            mFileUploadCallbackSecond.onReceiveValue(null);
        }
        mFileUploadCallbackSecond = fileUploadCallbackSecond;

        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);

        if (allowMultiple) {
            if (Build.VERSION.SDK_INT >= 18) {
                i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
            }
        }

        i.setType(mUploadableFileTypes);

        startActivityForResult(Intent.createChooser(i, "選擇文件"), REQUEST_CODE_FILE_PICKER);

    }

    public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
        if (requestCode == REQUEST_CODE_FILE_PICKER) {
            if (resultCode == Activity.RESULT_OK) {
                if (intent != null) {
                    
                    if (mFileUploadCallbackFirst != null) {
                        mFileUploadCallbackFirst.onReceiveValue(intent.getData());
                        mFileUploadCallbackFirst = null;
                    }
                    else if (mFileUploadCallbackSecond != null) {
                        Uri[] dataUris = null;

                        try {
                            if (intent.getDataString() != null) {
                                dataUris = new Uri[] { Uri.parse(intent.getDataString()) };
                            }
                            else {
                                if (Build.VERSION.SDK_INT >= 16) {
                                    if (intent.getClipData() != null) {
                                        final int numSelectedFiles = intent.getClipData().getItemCount();

                                        dataUris = new Uri[numSelectedFiles];

                                        for (int i = 0; i < numSelectedFiles; i++) {
                                            dataUris[i] = intent.getClipData().getItemAt(i).getUri();
                                        }
                                    }
                                }
                            }
                        }
                        catch (Exception ignored) { }
                        mFileUploadCallbackSecond.onReceiveValue(dataUris);
                        mFileUploadCallbackSecond = null;
                    }
                }
            }
            else {
                
                
                
                if (mFileUploadCallbackFirst != null) {
                    mFileUploadCallbackFirst.onReceiveValue(null);
                    mFileUploadCallbackFirst = null;
                }
                else if (mFileUploadCallbackSecond != null) {
                    mFileUploadCallbackSecond.onReceiveValue(null);
                    mFileUploadCallbackSecond = null;
                }
            }
        }
    }

}

註:當用戶點擊input file彈出文件選擇器後,點擊取消或者返回按鈕沒有執行選擇時,必須在onActivityResult裡給valueCallback的onReceiveValue傳null,因為valueCallback持有的是WebView,在onReceiveValue沒有回傳值的情況下,WebView無法進行下一步操作,會導致取消選擇文件後,點擊input file不會再響應:

  if (mFileUploadCallbackFirst != null) {
         mFileUploadCallbackFirst.onReceiveValue(null);
         mFileUploadCallbackFirst = null;
   }
   else if (mFileUploadCallbackSecond != null) {
         mFileUploadCallbackSecond.onReceiveValue(null);
         mFileUploadCallbackSecond = null;
  }

示例demo地址:
https://github.com/cjpx00008/FileChooser4WebViewDemo

WebView中的web頁面調用系統選擇器或者相機導致app進入後臺被系統釋放

眾所周知,WebView基於webkit內核來渲染web頁面,因此使用起來相當於一個小型瀏覽器,即使頁面內容不複雜,只要使用WebView也會佔用大量的內存。

而Android的內存回收機制,在系統內存不足的情況下會優先釋放內存佔用較大的app從而回收內存資源,此時正在使用WebView的運行在後臺的app肯定是首當其衝被回收的。

因此,當WebView通過input file調用系統文件選擇器,或者通過文件選擇器調用了相機時,我們的app就進入了後臺,在部分低端Android設備(尤其紅米這類手機,默認的神隱模式會在app進入後臺的時候較大概率的釋放app)或者系統內存資源不足的情況下,我們的app就會優先被釋放掉,導致文件選擇完畢後,回到上一界面時,app的界面重新走了onCreate,web頁面也因此重建了。

對於部分需要填寫大量表單的web頁面來說,用戶填寫的數據會隨著界面的銷毀重建而丟失,而選擇的文件也因為頁面的重建而無法回傳給input file,這對於用戶的體驗來說肯定是不友好的。

也許你會說,重寫onSaveInstance保存數據就是啦。
這也是我一開始考慮的,
我們的WebView也提供了 saveState  以及 restoreState  來保存狀態。

然而悲催的是,這兩個方法並不會保存web頁面內的數據,它只保存了WebView加載的頁面,前進後退的歷史狀態等數據。

引用官方文檔的描述:

Saves the state of this WebView used in onSaveInstanceState(Bundle)
. Please note that this method no longer stores the display data for this WebView. The previous behavior could potentially leak files if  restoreState(Bundle)
was never called.

Please note that this method no longer stores the display data for this WebView

WebView的saveState並不會保存界面的數據。

所以,對於表單數據的恢復,我們只能自己想辦法了,我們這裡採用了兩套方案:

WebSettings settings = mWebView.getSettings();
settings.setDomStorageEnabled(true);

另一種則提供JS接口將數據傳遞給原生,通過原生代碼將數據保存到本地,在頁面重建渲染完成時,web頁面通過JS接口調用原生方法拉取數據判斷是否有值,有則填充表單,無則不做操作,提交數據後調用JS接口調用原生方法清空本地數據。

以上是表單數據的恢復方案,

而對於從系統文件選擇器選擇的文件web頁面是無法直接接收並處理了,這裡我們提供了一個JS接口在web頁面加載完成時,進行觸發,並將數據傳遞給web頁面。

說到這裡,不得不提另外一個問題

WebView調用服務端頁面如何訪問本地文件

上面我們提到了通過JS接口將選擇的文件數據傳遞給web頁面,

然而由於安全原因,WebView限制了遠程url頁面訪問本地文件,
如果我們加載的url是服務端的頁面,那我們沒有任何辦法直接通過文件地址來訪問客戶端本地的文件

我們知道,WebView用來加載網頁的方式主要有三種:

loadUrl(String url)
loadUrl(String url, Map<String, String> additionalHttpHeaders)

loadData(String data, String mimeType, String encoding)

loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl)

loadData()和loadDataWithBaseURL()都是直接將數據加載進WebView中,相當於顯示的一個本地Web

loadUrl也可以通過訪問本地的文件地址(例如本地asset目錄下的存放了index.html頁面,可以通過loadUrl("file:///android_asset/index.html")來顯示web頁面)

對於這樣的三種加載本地內容的方式,我們可以使用多種方式來傳遞路徑供web頁面傳遞,這裡以圖片為例(相冊目錄下test/IMG_20170105_093405.jpg):

<img src = 'file:///storage/emulated/0/dcim/test/IMG_20170105_093405.jpg' />

<img src = 'content://media/external/images/media/102610' />

<img src = 'content://com.test.myfileprovider/dcim/test/IMG_20170105_093405.jpg'/>

可當你使用loadUrl(String url)加載服務端的http地址時,以上三種方法將均無法使用,經過各種嘗試,目前找到兩種方案來提供給web端進行圖片顯示:

mWebView.setWebViewClient(new WebViewClient(){

     @Override
     public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
          if (url.startsWith("http://")&amp;&amp;url.endWith(".jpg") {
               return getWebResourceResponse("/storage/emulated/0/dcim/trinaic/IMG_20170105_093405.jpg", "image/jpeg", ".jpg");
          }
          return super.shouldInterceptRequest(view, url);
     }

}

    private WebResourceResponse getWebResourceResponse(String url, String mime, String style) {
        WebResourceResponse response = null;
        try {
            response = new WebResourceResponse(mime, "UTF-8", new FileInputStream(new File(url)));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return response;
    }

WebView JS注入漏洞

要想讓原生跟JS進行交互,按照官方提供的方法就得使用addJavaScriptInterface

class JsObject {
    @JavascriptInterface
    public String toString() { return "injectedObject"; }
 }
 webView.addJavascriptInterface(new JsObject(), "injectedObject");
 webView.loadData("", "text/html", null);
 webView.loadUrl("javascript:alert(injectedObject.toString())");

Injects the supplied Java object into this WebView. The object is injected into the JavaScript context of the main frame, using the supplied name. This allows the Java object's methods to be accessed from JavaScript. For applications targeted to API level  JELLY_BEAN_MR1
and above, only public methods that are annotated with  JavascriptInterface
can be accessed from JavaScript. For applications targeted to API level  JELLY_BEAN
or below, all public methods (including the inherited ones) can be accessed, see the important security note below for implications.

引用官方api的說明,在Android 4.2以下,會有被注入的風險,4.2以上版本可以通過@JavascriptInterface的註解來處理這個問題。
具體的注入方式,我找了篇博客,如果有不清楚的同學可以了解下:
Android WebView的Js對象注入漏洞解決方案

在之前烏雲平臺報出的漏洞中,
android/webkit/webview中默認內置的一個searchBoxJavaBridge_ 接口同時存在遠程代碼執行漏洞

在於android/webkit/AccessibilityInjector.java中,調用了此組件的應用在開啟輔助功能選項中第三方服務的安卓系統中會造成遠程代碼執行漏洞。這兩個接口分別是"accessibility" 和"accessibilityTraversal" ,此漏洞原理與searchBoxJavaBridge_接口遠程代碼執行相似,均為未移除不安全的默認接口,不過此漏洞需要用戶啟動系統設置中的第三方輔助服務,利用條件較複雜。

因此,一般情況下我們通過removeJavaScripteInterface來移除這幾個接口

        if (Build.VERSION.SDK_INT < 17) {
            mAdvanceWebView.removeJavascriptInterface("searchBoxJavaBridge_");
            mAdvanceWebView.removeJavascriptInterface("accessibility");
            mAdvanceWebView.removeJavascriptInterface("accessibilityTraversal");
        }

除此之外也有通過onJsPrompt的方式來實現WebView原生跟JS交互功能的,github上的開源項目JSBridge就是採用這種方法:
https://github.com/lzyzsd/JsBridge

之前拜讀過大名鼎鼎的cordova的源碼,它內部的原生JS交互也是採用onJsPrompt的方式,不過在此基礎上做了更強大的封裝。

WebView後臺耗電問題

當我們的WebView的web頁面在解析或者播放視頻再或者有js定時器在執行的時,
如果我們把應用退到後臺,不做任何處理的情況下,以上的操作還會在後臺繼續執行,導致WebView在後臺持續耗電,因此一般我們會做以下處理

    @Override
    protected void onPause() {
        super.onPause();
        mWebView.onPause();
        mWebView.pauseTimers();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mWebView.onResume();
        mWebView.resumeTimers();    
    }

對於WebView的使用,在處理問題的過程中發現一個不錯的開源庫:
https://github.com/delight-im/Android-AdvancedWebView

基本上上面我提到的或者沒提到的問題它都做了一定的封裝處理,並且考慮了一些版本適配的問題,可以直接拿來使用,也可以拿來參考學習。

如果你覺得問題還是太多的話也可以考慮使用騰訊瀏覽服務,基於QQ瀏覽器X5內核,適配了Android全部主流平臺,可以在所有Android手機上使用Blink的技術能力,具有更好的H5/CSS3支持和性能,目前微信、qq都在使用它。

唯一的缺陷就是它不提供打包內核版的SDK,第一次使用時,它會自動到騰訊服務端去下載內核,下載完畢後會彈窗提示用戶是否重啟app,重啟之後就能正常使用x5瀏覽服務了,如果你不介意這樣的用戶體驗,可以考慮直接使用騰訊瀏覽服務。

(補充)

WebView混淆問題

如果app打包混淆之後發現提供給web頁面的JS接口失效了,記得檢查是否添加了JavaScriptInterface的混淆配置:

-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}

紅米WebView內部Web頁面的div自身滾動條問題

紅米上WebView內部的Web頁面的div由於內容高度大於div,產生了基於div的滾動條(WebView滾動條已禁用的情況下),通過設置div的css樣式來禁用div滾動條

Html dom元素ID或class:: -webkit-scrollbar {display:none}

WebView內部web頁面px跟dp的關係

經測試發現,WebView內部web頁面的px值會在內部自動轉換為dp,且1px=1dp,跟ppi值無關,這點跟原生開發中的1dp = 設備ppi/160 * px換算關係nveou

相關焦點

  • Android 升級適配爬坑歷程
    聲明:本文已獲如果執著授權發表,轉發等請聯繫原作者授權以下部分適配是針對的是混合開發的項目,使用的是mui及h5+ api和原生代碼實現最近接手了一個公司項目,項目比較老了,從Android 5.0之後就再也沒有適配過了,然而重寫時間又來不及,然後我的爬坑之旅便開始了
  • Android WebView 研究筆記
    :debuggerd0000000000000000: 00000002 00000000 00010000 0001 01 4640516 @webview_devtools_remote_136800000000000000000: 00000002 00000000 00010000 0001 01 17596 @jdwp-control0000000000000000
  • App自動化測試 | Android WebView測試
    WebView測試環境準備手機端PC端查看手機瀏覽器版本adb shell pm list package | grep webviewadb shell pm dump com.android.browser
  • Android WebView簡單整理
    記得第一次用webview的時候,就會個loadUrl,最近想著自己寫個簡單瀏覽器玩玩,發現了一些問題,於是寫一篇文章,文章分兩大類,一類為使用
  • Android Webview使用和遇到過的坑總結
    :name="android.permission.INTERNET" />        默認情況下,WebView不支持JavaScript,web頁面的錯誤也會被忽略,如果只是用Webview來顯示網頁而不用交互,默認配置就可以了,如果需要交互,就需要自定義配置了。
  • Android爬坑之旅之FileProvider
    本篇來自王鴻飛的投稿,這裡講了Android爬坑之旅之FileProvider的講解。FileProvider使用方法:1.在AndroidManifest.xml裡聲明Provider<manifes xmlns:android="http://schemas.android.com/apk/res/android"    package="com.example.myapp">      <application
  • Android如何獲取WebView內容高度
    ><com.trs.studyview.view.TRSScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/scrollView"
  • Android中使用WebView與JS交互全解析
    ; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.webkit.JavascriptInterface
  • Android WebView那些坑之上傳文件
    -keepclassmembers class * extends android.webkit.WebChromeClient{        public void openFileChooser(...)
  • Android Webview知識點和遇到過的坑全總結
    :name="android.permission.INTERNET" />        默認情況下,WebView不支持JavaScript,web頁面的錯誤也會被忽略,如果只是用Webview來顯示網頁而不用交互,默認配置就可以了,如果需要交互,就需要自定義配置了。
  • 全面總結WebView遇到的坑及優化
    這篇文章講一下WebView遇到的那些坑,帶領各位爬坑。這裡如果有你沒遇到的問題,歡迎留言告訴我,我盡我所能幫你解決。感謝大家支持。(1) 為什麼Webview打開一個頁面,播放一段音樂,退出Activity時音樂還在後臺播放?
  • Android 5.1 WebView內存洩漏分析
    引起的內存洩漏,而且能看到是在 org.chromium.android_webview.AwContents 類中,難道是這個類註冊了component callbacks,但是未反註冊?基於這個思路,我把chromium的源碼下載下來,代碼在這裡 chromium_org(https://android.googlesource.com/platform/external/chromium_org/?
  • 前端 WebView 指南之 Android 交互篇
    調用方法如下:Webview webview = (WebView) findViewById(R.id.webView);webView.loadUrl("javascript:console.log('hello')");這樣我們就實現了調用 JS 的目的了。
  • Android WebView使用
    package com.example.hfs.webviewsimpledemo;    import android.support.v7.app.AppCompatActivity;importandroid.os.Bundle;import android.view.View;import android.webkit.WebChromeClient;import android.webkit.WebView;import android.webkit.WebViewClient;import android.widget.Button;
  • 你真的了解webview麼?
    接下來讓我們從webview看世界。一、適用場景提到應用場景,大家最直觀的能想到一些App內嵌的頁面,為我們提供各種各樣的交互,就像下面圖片裡的這樣: 其實webview的應用場景遠遠不止這些,其實在一些PC的軟體裡,和我們交互的也是我們的html頁面,只是穿著webview的衣服,衣服太美而我們沒有發現他們的真諦。
  • Android WebView和JS交互詳細教程
    Android webview和JS的交互已經是老生常談了,坑很多、問題也很多。
  • WebView的坑別嫌多
    webView.loadUrl("http://www.google.com/"); webView.loadUrl("file:///android_asset/test.html通過webview的loadUrl注意:該方式必須在webview加載完畢之後才能調用,也就是webviewClient的onPageFinished()方法回調之後,而且該方法的執行 會刷新界面,效率較低js代碼:function callJs(){
  • 還在用Android自帶的WebView組件?太Out了!
    <com.tencent.smtt.sdk.WebView              android:id="@+id/tbsContent"              android:layout_width="match_parent"              android:layout_height="match_parent
  • WebView詳解與簡單實現Android與H5互調
    WebView常用方法加載界面,其次還有 LoadData 和 LoadDataWithBase 方法//加載assets目錄下的test.html文件webView.loadUrl("file:///android_asset/test.html");//加載網絡資源(注意要加上網絡權限)webView.loadUrl
  • 用安卓 WebView 做一個「套殼」應用
    -- 展示一個 WebView --><WebView    android:id="@+id/webview"    android:layout_width="match_parent"    android:layout_height="match_parent" />在 Android 工程中,「app