帶你解決 WebView 里的常見問題

通常我們在自己開發(fā)的 APP 中打開網(wǎng)頁無非兩種方法: 一是跳轉(zhuǎn)到系統(tǒng)自帶的瀏覽器初家,二是使用 WebView 控件加載頁面。使用 WebView 控件的好處就是可以通過各種 api 接口來定制各種行為漠秋,常用的幾個設(shè)置地方為 WebSettingsJavaScriptInterface寥假、WebViewClientWebChromeClient妄壶。平時出現(xiàn)的問題都可以通過修改這些設(shè)置來解決。

WebView.jpg

使用了 WebView 還是跳轉(zhuǎn)到了系統(tǒng)自帶的瀏覽器键耕?

很簡單的解決方法寺滚,為你的 webview 設(shè)置一個新的 WebViewClient。

webView.setWebViewClient(new WebViewClient(){
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        view.loadUrl(url);
        return true;
    }
});
// 或者直接添加屈雄,效果是一樣的
webView.setWebViewClient(new WebViewClient());

獲取網(wǎng)頁的標(biāo)題和圖標(biāo)

通過 WebChromeClient 可以獲取到這些信息村视。

webView.setWebChromeClient(new WebChromeClient() {
    @Override
    public void onReceivedTitle(WebView view, String title) {
        super.onReceivedTitle(view, title);
        setTitle(title);
    }

    @Override
    public void onReceivedIcon(WebView view, Bitmap icon) {
        super.onReceivedIcon(view, icon);
        setIcon(icon);
    }

});

但是,這里有個問題酒奶,當(dāng)通過 webView.goBack() 方式返回上一級Web頁面的時候不會觸發(fā)這個方法蚁孔,因此會導(dǎo)致標(biāo)題無法跟隨歷史記錄返回上一級頁面。所以需要在 onPageFinished() 中對界面標(biāo)題重新設(shè)置讥蟆。

webView.setWebViewClient(new WebViewClient(){
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        setTitle(String.valueOf(view.getTitle()));
    }
});

返回鍵實現(xiàn)網(wǎng)頁的后退鍵

在 WebView 中可以通過 goBack() 方法后退到歷史記錄的上一項。

    // 在 Actvity 中監(jiān)聽返回鍵按鈕
    @Override
    public void onBackPressed() {
        if (webView.canGoBack())
            webView.goBack();
        else
            super.onBackPressed();
    }

設(shè)置 WebView 的 header

在 WebView 的 loadUrl() 方法中傳入 Header 參數(shù)即可纺阔。

public void loadURLWithHTTPHeaders() {
    final String url = "http://cpacm.net";
    WebView webView = new WebView(getActivity());
    Map<String,String> extraHeaders = new HashMap<String, String>();
    extraHeaders.put("Referer", "http://www.google.com");
    webView.loadUrl(url, extraHeaders);
}

設(shè)置 WebView 的 User-Agent

不要試圖在 Header 里面去修改瘸彤,而是在 WebSettings 修改

webView.getSettings().setUserAgentString("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");

如何設(shè)置 WebView 的緩存

當(dāng)需要本地緩存網(wǎng)頁的時候就需要打開 WebViewSettings 的緩存開關(guān),這樣子當(dāng)下次進到該頁面無網(wǎng)絡(luò)的情況下也能打開頁面。

WebSettings settings = webView.getSettings();
settings.setAppCacheEnabled(true); //啟用應(yīng)用緩存
settings.setDomStorageEnabled(true); //啟用或禁用DOM緩存笛钝。
settings.setDatabaseEnabled(true); //啟用或禁用DOM緩存质况。
if (SystemUtil.isNetworkConnected()) { //判斷是否聯(lián)網(wǎng)
    settings.setCacheMode(WebSettings.LOAD_DEFAULT); //默認(rèn)的緩存使用模式
} else {
    settings.setCacheMode(WebSettings.LOAD_CACHE_ONLY); //不從網(wǎng)絡(luò)加載數(shù)據(jù)愕宋,只從緩存加載數(shù)據(jù)。
}

無法下載文件结榄?

在自己寫的 WebView 下是無法直接下載文件中贝,需要自己監(jiān)聽下載事件并對下載的動作進行處理。

/**
* 當(dāng)下載文件時打開系統(tǒng)自帶的瀏覽器進行下載臼朗,當(dāng)然也可以對捕獲到的 url 進行處理在應(yīng)用內(nèi)下載邻寿。
**/
webView.setDownloadListener(new FileDownLoadListener());

private class FileDownLoadListener implements DownloadListener {
    @Override
    public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
        Uri uri = Uri.parse(url);
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        startActivity(intent);
    }
}

無法打開文件選擇器?

通過重寫 WebChromeClient 來實現(xiàn)點擊 <input type='file'> 來打開系統(tǒng)文件選擇器视哑。

一個完整的Activity示例

public class MainActivity extends AppCompatActivity {

    /** Android 5.0以下版本的文件選擇回調(diào) */
    protected ValueCallback<Uri> mFileUploadCallbackFirst;
    /** Android 5.0及以上版本的文件選擇回調(diào) */
    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 {

        //  Android 2.2 (API level 8)到Android 2.3 (API level 10)版本選擇文件時會觸發(fā)該隱藏方法
        @SuppressWarnings("unused")
        public void openFileChooser(ValueCallback<Uri> uploadMsg) {
            openFileChooser(uploadMsg, null);
        }

        // Android 3.0 (API level 11)到 Android 4.0 (API level 15))版本選擇文件時會觸發(fā)俄占,該方法為隱藏方法
        public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
            openFileChooser(uploadMsg, acceptType, null);
        }

        // Android 4.1 (API level 16) -- Android 4.3 (API level 18)版本選擇文件時會觸發(fā)录别,該方法為隱藏方法
        @SuppressWarnings("unused")
        public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
            openFileInput(uploadMsg, null, false);
        }

        // Android 5.0 (API level 21)以上版本會觸發(fā)該方法,該方法為公開方法
        @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) {
        //Android 5.0以下版本
        if (mFileUploadCallbackFirst != null) {
            mFileUploadCallbackFirst.onReceiveValue(null);
        }
        mFileUploadCallbackFirst = fileUploadCallbackFirst;

        //Android 5.0及以上版本
        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) {
                    //Android 5.0以下版本
                    if (mFileUploadCallbackFirst != null) {
                        mFileUploadCallbackFirst.onReceiveValue(intent.getData());
                        mFileUploadCallbackFirst = null;
                    }
                    else if (mFileUploadCallbackSecond != null) {//Android 5.0及以上版本
                        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 {
                //這里mFileUploadCallbackFirst跟mFileUploadCallbackSecond在不同系統(tǒng)版本下分別持有了
                //WebView對象,在用戶取消文件選擇器的情況下晴氨,需給onReceiveValue傳null返回值
                //否則WebView在未收到返回值的情況下,無法進行任何操作嚷量,文件選擇器會失效
                if (mFileUploadCallbackFirst != null) {
                    mFileUploadCallbackFirst.onReceiveValue(null);
                    mFileUploadCallbackFirst = null;
                }
                else if (mFileUploadCallbackSecond != null) {
                    mFileUploadCallbackSecond.onReceiveValue(null);
                    mFileUploadCallbackSecond = null;
                }
            }
        }
    }

}

怎么為 WebView 的加載添加進度條

這里的 onPageFinished() 有個問題镇饮,不能在這里監(jiān)聽頁面是否加載完畢(我自己測試的時候,好像在重定向和加載完 iframes 時都會調(diào)用這個方法)耗绿。
把頁面加載完畢的判斷放在 onProgressChanged() 里可能會更為準(zhǔn)確苹支。

webView.setWebChromeClient(new WebChromeClient() {

    @Override
    public void onProgressChanged(WebView view, int position) {
        progressBar.setProgress(position);
        if (position == 100) {
            progressBar.setVisibility(View.GONE);
        }
        super.onProgressChanged(view, position);
    }
});

webView.setWebViewClient(new WebViewClient(){
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        progressBar.setVisibility(View.VISIBLE);
        super.onPageStarted(view, url, favicon);
    }
});

怎樣對頁面進行 Js 注入?

首先你要在 WebView 開啟 JavaScript,然后搭建橋梁

WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(new WebAppBridge(new WebAppBridge.OauthLoginImpl() {
            @Override
            public void getResult(String s) {
                //TODO
            }
        }),
        "oauth");
webView.loadUrl("javascript:" + getAssetsJs("autologin.js"));
webView.loadUrl("javascript:adduplistener()");

WebAppBridge的代碼

public class WebAppBridge {

    private OauthLoginImpl oauthLogin;

    public WebAppBridge(OauthLoginImpl oauthLogin) {
        this.oauthLogin = oauthLogin;
    }

    @JavascriptInterface
    public void getResult(String str) {
        if (oauthLogin != null)
            oauthLogin.getResult(str);
    }

    public interface OauthLoginImpl {
        void getResult(String s);
    }
}

簡單的說就是向網(wǎng)頁注入一段 js, 在這段 js 里面設(shè)置回調(diào)到j(luò)ava中的方法 getResult()缭乘,由 WebAppBridge.getResult 來回收沐序。
其中js的核心代碼為

oauth.getResult(str);

其中 oauth 這個名稱要與 webView.addJavascriptInterface()方法的第二個參數(shù)一樣。

具體的代碼可以參考這個項目中寫的 js 注入邏輯 OauthDialog

如何手動添加 Cookie

需要獲得 CookieManager 的對象并將 cookie 設(shè)置進去堕绩。
從服務(wù)器的返回頭中取出 cookie 根據(jù)Http請求的客戶端不同策幼,獲取 cookie 的方式也不同,請自行獲取奴紧。

/**
 * 將cookie設(shè)置到 WebView
 * @param url 要加載的 url
 * @param cookie 要同步的 cookie
 */
public static void syncCookie(String url,String cookie) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        CookieSyncManager.createInstance(context);
    }
    CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.setAcceptCookie(true);
    
    /**
     * cookie 設(shè)置形式
     * cookieManager.setCookie(url, "key=value;" + "domain=[your domain];path=/;")
    **/
    cookieManager.setCookie(url, cookie);
}

刪除 Cookie 的方法

/**
 * 這個兩個在 API level 21 被拋棄
 * CookieManager.getInstance().removeSessionCookie();
 * CookieManager.getInstance().removeAllCookie();
 *
 * 推薦使用這兩個特姐, level 21 新加的
 * CookieManager.getInstance().removeSessionCookies();
 * CookieManager.getInstance().removeAllCookies();
**/
public static void removeCookies() {
    CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.removeAllCookie();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        cookieManager.flush();
    } else {
        CookieSyncManager.createInstance(Application.getInstance());
        CookieSyncManager.getInstance().sync();
    }
}

如何使 HTML5 video 在 WebView 全屏顯示

當(dāng)網(wǎng)頁全屏播放視頻時會調(diào)用 WebChromeClient.onShowCustomView() 方法,所以可以通過將 video 播放的視圖全屏達(dá)到目的黍氮。

@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
    if (view instanceof FrameLayout && fullScreenView != null) {
        // A video wants to be shown
        this.videoViewContainer = (FrameLayout) view;
        this.videoViewCallback = callback;
        fullScreenView.addView(videoViewContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        fullScreenView.setVisibility(View.VISIBLE);
        isVideoFullscreen = true;
    }
}

@Override
public void onHideCustomView() {
    if (isVideoFullscreen && fullScreenView != null) {
        // Hide the video view, remove it, and show the non-video view
        fullScreenView.setVisibility(View.INVISIBLE);
        fullScreenView.removeView(videoViewContainer);

        // Call back (only in API level <19, because in API level 19+ with chromium webview it crashes)
        if (videoViewCallback != null && !videoViewCallback.getClass().getName().contains(".chromium.")) {
            videoViewCallback.onCustomViewHidden();
        }

        isVideoFullscreen = false;
        videoViewContainer = null;
        videoViewCallback = null;
    }
}

但是很多的手機版本在網(wǎng)頁視頻播放時是不會調(diào)用這個方法的唐含,所以這個方法局限性很大。

Android5.0上 WebView中Http和Https混合問題

/**
 * MIXED_CONTENT_ALWAYS_ALLOW:允許從任何來源加載內(nèi)容沫浆,即使起源是不安全的捷枯;
 * MIXED_CONTENT_NEVER_ALLOW:不允許Https加載Http的內(nèi)容,即不允許從安全的起源去加載一個不安全的資源专执;
 * MIXED_CONTENT_COMPATIBILITY_MODE:當(dāng)涉及到混合式內(nèi)容時淮捆,WebView 會嘗試去兼容最新Web瀏覽器的風(fēng)格。
 **/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
     webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

如何避免 WebView 的內(nèi)存泄露問題

  1. 可以將 Webview 的 Activity 新起一個進程,結(jié)束的時候直接System.exit(0);退出當(dāng)前進程攀痊;
  2. 不在xml中定義 WebView桐腌,而是在代碼中創(chuàng)建,使用 getApplicationgContext() 作為傳遞的 Conetext苟径;
  3. 在 Activity 銷毀的時候案站,將 WebView 置空
@Override
protected void onDestroy() {
    if (webView != null) {
        webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
        webView.clearHistory();
        ((ViewGroup) webView.getParent()).removeView(webView);
        webView.destroy();
        webView = null;
    }
    super.onDestroy();
}

總結(jié)

如果你踩到了 WebView 上的坑,請先默哀一分鐘棘街,然后努力找找解決方法吧蟆盐,總會有人體驗過你的悲劇,也會有人重蹈你的覆轍蹬碧。
當(dāng)然 WebView 里肯定不止我上面列出來的這些問題舱禽,如果你有更多的 WebView 問題解決方案歡迎評論交流。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末恩沽,一起剝皮案震驚了整個濱河市誊稚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌罗心,老刑警劉巖里伯,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異渤闷,居然都是意外死亡疾瓮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門飒箭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狼电,“玉大人,你說我怎么就攤上這事弦蹂〖绲” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵凸椿,是天一觀的道長削祈。 經(jīng)常有香客問我,道長脑漫,這世上最難降的妖魔是什么髓抑? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮优幸,結(jié)果婚禮上吨拍,老公的妹妹穿的比我還像新娘。我一直安慰自己网杆,他們只是感情好羹饰,可當(dāng)我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布握爷。 她就那樣靜靜地躺著,像睡著了一般严里。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上追城,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天刹碾,我揣著相機與錄音,去河邊找鬼座柱。 笑死迷帜,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的色洞。 我是一名探鬼主播戏锹,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼火诸!你這毒婦竟也來了锦针?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤置蜀,失蹤者是張志新(化名)和其女友劉穎奈搜,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盯荤,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡馋吗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了秋秤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宏粤。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖灼卢,靈堂內(nèi)的尸體忽然破棺而出绍哎,到底是詐尸還是另有隱情,我是刑警寧澤芥玉,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布蛇摸,位于F島的核電站,受9級特大地震影響灿巧,放射性物質(zhì)發(fā)生泄漏赶袄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一抠藕、第九天 我趴在偏房一處隱蔽的房頂上張望饿肺。 院中可真熱鬧,春花似錦盾似、人聲如沸敬辣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽溉跃。三九已至村刨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撰茎,已是汗流浹背嵌牺。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留龄糊,地道東北人逆粹。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像炫惩,于是被迫代替她去往敵國和親僻弹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容