2019-05-08 带你解决 WebView 里的常见问题

通常我们在自己开发的 APP 中打开网页无非两种方法: 一是跳转到系统自带的浏览器,二是使用 WebView 控件加载页面。使用 WebView 控件的好处就是可以通过各种 api 接口来定制各种行为,常用的几个设置地方为WebSettingsJavaScriptInterfaceWebViewClientWebChromeClient。平时出现的问题都可以通过修改这些设置来解决。

WebView.jpg

使用了 WebView 还是跳转到了系统自带的浏览器?

很简单的解决方法,为你的 webview 设置一个新的 WebViewClient。

webView.setWebViewClient(newWebViewClient(){@OverridepublicbooleanshouldOverrideUrlLoading(WebView view, String url){        view.loadUrl(url);returntrue;    }});

// 或者直接添加,效果是一样的webView.setWebViewClient(newWebViewClient());

获取网页的标题和图标

通过 WebChromeClient 可以获取到这些信息。

webView.setWebChromeClient(newWebChromeClient() {@OverridepublicvoidonReceivedTitle(WebView view, String title){super.onReceivedTitle(view, title);        setTitle(title);    }@OverridepublicvoidonReceivedIcon(WebView view, Bitmap icon){super.onReceivedIcon(view, icon);        setIcon(icon);    }});

但是,这里有个问题,当通过webView.goBack()方式返回上一级Web页面的时候不会触发这个方法,因此会导致标题无法跟随历史记录返回上一级页面。所以需要在onPageFinished()中对界面标题重新设置。

webView.setWebViewClient(newWebViewClient(){@OverridepublicvoidonPageFinished(WebView view, String url){super.onPageFinished(view, url);        setTitle(String.valueOf(view.getTitle()));    }});

返回键实现网页的后退键

在 WebView 中可以通过 goBack() 方法后退到历史记录的上一项。

// 在 Actvity 中监听返回键按钮@OverridepublicvoidonBackPressed(){if(webView.canGoBack())            webView.goBack();elsesuper.onBackPressed();    }

设置 WebView 的 header

在 WebView 的loadUrl()方法中传入 Header 参数即可。

publicvoidloadURLWithHTTPHeaders(){finalString url ="http://cpacm.net";    WebView webView =newWebView(getActivity());    Map extraHeaders =newHashMap();    extraHeaders.put("Referer","http://www.google.com");    webView.loadUrl(url, extraHeaders);}

设置 WebView 的 User-Agent

不要试图在 Header 里面去修改,而是在 WebSettings 修改

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

如何设置 WebView 的缓存

当需要本地缓存网页的时候就需要打开 WebViewSettings 的缓存开关,这样子当下次进到该页面无网络的情况下也能打开页面。

WebSettings settings = webView.getSettings();settings.setAppCacheEnabled(true);//启用应用缓存settings.setDomStorageEnabled(true);//启用或禁用DOM缓存。settings.setDatabaseEnabled(true);//启用或禁用DOM缓存。if(SystemUtil.isNetworkConnected()) {//判断是否联网settings.setCacheMode(WebSettings.LOAD_DEFAULT);//默认的缓存使用模式}else{    settings.setCacheMode(WebSettings.LOAD_CACHE_ONLY);//不从网络加载数据,只从缓存加载数据。}

无法下载文件?

在自己写的 WebView 下是无法直接下载文件,需要自己监听下载事件并对下载的动作进行处理。

/**

* 当下载文件时打开系统自带的浏览器进行下载,当然也可以对捕获到的 url 进行处理在应用内下载。

**/webView.setDownloadListener(newFileDownLoadListener());privateclassFileDownLoadListenerimplementsDownloadListener{@OverridepublicvoidonDownloadStart(String url, String userAgent, String contentDisposition, String mimetype,longcontentLength){        Uri uri = Uri.parse(url);        Intent intent =newIntent(Intent.ACTION_VIEW, uri);        startActivity(intent);    }}

无法打开文件选择器?

通过重写WebChromeClient来实现点击来打开系统文件选择器。

一个完整的Activity示例

publicclassMainActivityextendsAppCompatActivity{/** Android 5.0以下版本的文件选择回调 */protectedValueCallback mFileUploadCallbackFirst;/** Android 5.0及以上版本的文件选择回调 */protectedValueCallback mFileUploadCallbackSecond;protectedstaticfinalintREQUEST_CODE_FILE_PICKER =51426;protectedString mUploadableFileTypes ="image/*";privateWebView mWebView;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        initWebView();    }privatevoidinitWebView(){        mWebView = (WebView) findViewById(R.id.my_webview);        mWebView.loadUrl("file:///android_asset/index.html");        mWebView.setWebChromeClient(newOpenFileChromeClient());    }privateclassOpenFileChromeClientextendsWebChromeClient{//  Android 2.2 (API level 8)到Android 2.3 (API level 10)版本选择文件时会触发该隐藏方法@SuppressWarnings("unused")publicvoidopenFileChooser(ValueCallback uploadMsg){            openFileChooser(uploadMsg,null);        }// Android 3.0 (API level 11)到 Android 4.0 (API level 15))版本选择文件时会触发,该方法为隐藏方法publicvoidopenFileChooser(ValueCallback uploadMsg, String acceptType){            openFileChooser(uploadMsg, acceptType,null);        }// Android 4.1 (API level 16) -- Android 4.3 (API level 18)版本选择文件时会触发,该方法为隐藏方法@SuppressWarnings("unused")publicvoidopenFileChooser(ValueCallback uploadMsg, String acceptType, String capture){            openFileInput(uploadMsg,null,false);        }// Android 5.0 (API level 21)以上版本会触发该方法,该方法为公开方法@SuppressWarnings("all")publicbooleanonShowFileChooser(WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams){if(Build.VERSION.SDK_INT >=21) {finalbooleanallowMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE;//是否支持多选openFileInput(null, filePathCallback, allowMultiple);returntrue;            }else{returnfalse;            }        }    }@SuppressLint("NewApi")protectedvoidopenFileInput(finalValueCallback fileUploadCallbackFirst,finalValueCallback fileUploadCallbackSecond,finalbooleanallowMultiple){//Android 5.0以下版本if(mFileUploadCallbackFirst !=null) {            mFileUploadCallbackFirst.onReceiveValue(null);        }        mFileUploadCallbackFirst = fileUploadCallbackFirst;//Android 5.0及以上版本if(mFileUploadCallbackSecond !=null) {            mFileUploadCallbackSecond.onReceiveValue(null);        }        mFileUploadCallbackSecond = fileUploadCallbackSecond;        Intent i =newIntent(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);    }publicvoidonActivityResult(finalintrequestCode,finalintresultCode,finalIntent 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;                    }elseif(mFileUploadCallbackSecond !=null) {//Android 5.0及以上版本Uri[] dataUris =null;try{if(intent.getDataString() !=null) {                                dataUris =newUri[] { Uri.parse(intent.getDataString()) };                            }else{if(Build.VERSION.SDK_INT >=16) {if(intent.getClipData() !=null) {finalintnumSelectedFiles = intent.getClipData().getItemCount();                                        dataUris =newUri[numSelectedFiles];for(inti =0; i < numSelectedFiles; i++) {                                            dataUris[i] = intent.getClipData().getItemAt(i).getUri();                                        }                                    }                                }                            }                        }catch(Exception ignored) { }                        mFileUploadCallbackSecond.onReceiveValue(dataUris);                        mFileUploadCallbackSecond =null;                    }                }            }else{//这里mFileUploadCallbackFirst跟mFileUploadCallbackSecond在不同系统版本下分别持有了//WebView对象,在用户取消文件选择器的情况下,需给onReceiveValue传null返回值//否则WebView在未收到返回值的情况下,无法进行任何操作,文件选择器会失效if(mFileUploadCallbackFirst !=null) {                    mFileUploadCallbackFirst.onReceiveValue(null);                    mFileUploadCallbackFirst =null;                }elseif(mFileUploadCallbackSecond !=null) {                    mFileUploadCallbackSecond.onReceiveValue(null);                    mFileUploadCallbackSecond =null;                }            }        }    }}

怎么为 WebView 的加载添加进度条

这里的onPageFinished()有个问题,不能在这里监听页面是否加载完毕(我自己测试的时候,好像在重定向和加载完 iframes 时都会调用这个方法)。

把页面加载完毕的判断放在onProgressChanged()里可能会更为准确。

webView.setWebChromeClient(newWebChromeClient() {@OverridepublicvoidonProgressChanged(WebView view,intposition){        progressBar.setProgress(position);if(position ==100) {            progressBar.setVisibility(View.GONE);        }super.onProgressChanged(view, position);    }});webView.setWebViewClient(newWebViewClient(){@OverridepublicvoidonPageStarted(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(newWebAppBridge(newWebAppBridge.OauthLoginImpl() {@OverridepublicvoidgetResult(String s){//TODO}        }),"oauth");webView.loadUrl("javascript:"+ getAssetsJs("autologin.js"));webView.loadUrl("javascript:adduplistener()");

WebAppBridge的代码

publicclassWebAppBridge{privateOauthLoginImpl oauthLogin;publicWebAppBridge(OauthLoginImpl oauthLogin){this.oauthLogin = oauthLogin;    }@JavascriptInterfacepublicvoidgetResult(String str){if(oauthLogin !=null)            oauthLogin.getResult(str);    }publicinterfaceOauthLoginImpl{voidgetResult(String s);    }}

简单的说就是向网页注入一段 js, 在这段 js 里面设置回调到java中的方法getResult(),由 WebAppBridge.getResult 来回收。

其中js的核心代码为

oauth.getResult(str);

其中 oauth 这个名称要与webView.addJavascriptInterface()方法的第二个参数一样。

具体的代码可以参考这个项目中写的 js 注入逻辑OauthDialog

如何手动添加 Cookie

需要获得 CookieManager 的对象并将 cookie 设置进去。

从服务器的返回头中取出 cookie 根据Http请求的客户端不同,获取 cookie 的方式也不同,请自行获取。

/** * 将cookie设置到 WebView *@paramurl 要加载的 url *@paramcookie 要同步的 cookie */publicstaticvoidsyncCookie(String url,String cookie){if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {        CookieSyncManager.createInstance(context);    }    CookieManager cookieManager = CookieManager.getInstance();    cookieManager.setAcceptCookie(true);/**

    * cookie 设置形式

    * 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();

**/publicstaticvoidremoveCookies(){    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 全屏显示

当网页全屏播放视频时会调用WebChromeClient.onShowCustomView()方法,所以可以通过将 video 播放的视图全屏达到目的。

@OverridepublicvoidonShowCustomView(View view, CustomViewCallback callback){if(viewinstanceofFrameLayout && fullScreenView !=null) {// A video wants to be shownthis.videoViewContainer = (FrameLayout) view;this.videoViewCallback = callback;        fullScreenView.addView(videoViewContainer,newViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));        fullScreenView.setVisibility(View.VISIBLE);        isVideoFullscreen =true;    }}@OverridepublicvoidonHideCustomView(){if(isVideoFullscreen && fullScreenView !=null) {// Hide the video view, remove it, and show the non-video viewfullScreenView.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;    }}

但是很多的手机版本在网页视频播放时是不会调用这个方法的,所以这个方法局限性很大。

Android5.0上 WebView中Http和Https混合问题

/**

* MIXED_CONTENT_ALWAYS_ALLOW:允许从任何来源加载内容,即使起源是不安全的;

* MIXED_CONTENT_NEVER_ALLOW:不允许Https加载Http的内容,即不允许从安全的起源去加载一个不安全的资源;

* MIXED_CONTENT_COMPATIBILITY_MODE:当涉及到混合式内容时,WebView 会尝试去兼容最新Web浏览器的风格。

**/if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {    webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);}

如何避免 WebView 的内存泄露问题

可以将 Webview 的 Activity 新起一个进程,结束的时候直接System.exit(0);退出当前进程;

不在xml中定义 WebView,而是在代码中创建,使用 getApplicationgContext() 作为传递的 Conetext;

在 Activity 销毁的时候,将 WebView 置空

@OverrideprotectedvoidonDestroy(){if(webView !=null) {        webView.loadDataWithBaseURL(null,"","text/html","utf-8",null);        webView.clearHistory();        ((ViewGroup) webView.getParent()).removeView(webView);        webView.destroy();        webView =null;    }super.onDestroy();}

总结

如果你踩到了 WebView 上的坑,请先默哀一分钟,然后努力找找解决方法吧,总会有人体验过你的悲剧,也会有人重蹈你的覆辙。

当然 WebView 里肯定不止我上面列出来的这些问题,如果你有更多的 WebView 问题解决方案欢迎评论交流。

作者:cpacm

链接:https://www.jianshu.com/p/fea5e829b30a

来源:

著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

你可能感兴趣的:(2019-05-08 带你解决 WebView 里的常见问题)