https://blog.csdn.net/lsyz0021/article/details/56677132
https://blog.csdn.net/carson_ho/article/details/64904691
以上是原文,
一、onJsAlert拦截后,后续的js无法执行,导致页面卡顿,解决:
- class MChromeClient extends WebChromeClient {
- @Override
- public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
- //需要做的操作
- result.cancel();//很关键,cancel后使后续代码正常运行生效。
- return true;
- }
- }
二:原生与js交互
JS调用Android:
1. WebView的addJavascriptInterface()对象映射
2. WebViewClient 的shouldOverrideUrlLoading ()回调拦截超链接 url
3. WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()拦截JS对话框 消息
Android调用JS:
1. WebView的loadUrl()
2. WebView的evaluateJavascript()
使用了 WebView 还是跳转到了系统自带的浏览器?
很简单的解决方法,为你的 webview 设置一个新的 WebViewClient。
webView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
// 或者直接添加,效果是一样的
webView.setWebViewClient(new WebViewClient());
获取网页的标题和图标
通过 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);
}
});
但是,这里有个问题,当通过 webView.goBack()
方式返回上一级Web页面的时候不会触发这个方法,因此会导致标题无法跟随历史记录返回上一级页面。所以需要在 onPageFinished()
中对界面标题重新设置。
webView.setWebViewClient(new WebViewClient(){
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
setTitle(String.valueOf(view.getTitle()));
}
});
返回键实现网页的后退键
在 WebView 中可以通过 goBack() 方法后退到历史记录的上一项。
// 在 Actvity 中监听返回键按钮
@Override
public void onBackPressed() {
if (webView.canGoBack())
webView.goBack();
else
super.onBackPressed();
}
设置 WebView 的 header
在 WebView 的 loadUrl()
方法中传入 Header 参数即可。
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);
}
设置 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(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
来实现点击 <input type='file'>
来打开系统文件选择器。
一个完整的Activity示例
public class MainActivity extends AppCompatActivity {
/** Android 5.0以下版本的文件选择回调 */
protected ValueCallback<Uri> mFileUploadCallbackFirst;
/** Android 5.0及以上版本的文件选择回调 */
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)版本选择文件时会触发该隐藏方法
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
openFileChooser(uploadMsg, null);
}
// Android 3.0 (API level 11)到 Android 4.0 (API level 15))版本选择文件时会触发,该方法为隐藏方法
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
openFileChooser(uploadMsg, acceptType, null);
}
// Android 4.1 (API level 16) -- Android 4.3 (API level 18)版本选择文件时会触发,该方法为隐藏方法
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
openFileInput(uploadMsg, null, false);
}
// Android 5.0 (API level 21)以上版本会触发该方法,该方法为公开方法
@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在不同系统版本下分别持有了
//WebView对象,在用户取消文件选择器的情况下,需给onReceiveValue传null返回值
//否则WebView在未收到返回值的情况下,无法进行任何操作,文件选择器会失效
if (mFileUploadCallbackFirst != null) {
mFileUploadCallbackFirst.onReceiveValue(null);
mFileUploadCallbackFirst = null;
}
else if (mFileUploadCallbackSecond != null) {
mFileUploadCallbackSecond.onReceiveValue(null);
mFileUploadCallbackSecond = null;
}
}
}
}
}
怎么为 WebView 的加载添加进度条
这里的 onPageFinished()
有个问题,不能在这里监听页面是否加载完毕(我自己测试的时候,好像在重定向和加载完 iframes 时都会调用这个方法)。
把页面加载完毕的判断放在 onProgressChanged()
里可能会更为准确。
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);
}
}
简单的说就是向网页注入一段 js, 在这段 js 里面设置回调到java中的方法 getResult()
,由 WebAppBridge.getResult 来回收。
其中js的核心代码为
oauth.getResult(str);
其中 oauth 这个名称要与 webView.addJavascriptInterface()
方法的第二个参数一样。
具体的代码可以参考这个项目中写的 js 注入逻辑 OauthDialog
如何手动添加 Cookie
需要获得 CookieManager 的对象并将 cookie 设置进去。
从服务器的返回头中取出 cookie 根据Http请求的客户端不同,获取 cookie 的方式也不同,请自行获取。
/**
* 将cookie设置到 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 设置形式
* 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 全屏显示
当网页全屏播放视频时会调用 WebChromeClient.onShowCustomView()
方法,所以可以通过将 video 播放的视图全屏达到目的。
@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;
}
}
但是很多的手机版本在网页视频播放时是不会调用这个方法的,所以这个方法局限性很大。
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 置空
@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();
}
总结
如果你踩到了 WebView 上的坑,请先默哀一分钟,然后努力找找解决方法吧,总会有人体验过你的悲剧,也会有人重蹈你的覆辙。
当然 WebView 里肯定不止我上面列出来的这些问题,如果你有更多的 WebView 问题解决方案欢迎评论交流
现在混合开发的app越来越多,Google对webview也越来越重视,在早期的版本webview有很多的bug,例如404、500等请求错误码我们无发直接获取(这个bug早在2008年就有人提交过issue给Google),好在Google在Android6.0修复了这个问题。根据google官网提供的最新的官网文档,我们可以重写onReceivedHttpError()方法可以捕获http Error。
@TargetApi(android.os.Build.VERSION_CODES.M)
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
super.onReceivedHttpError(view, request, errorResponse);
// 这个方法在6.0才出现
int statusCode = errorResponse.getStatusCode();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这是google关于这个issue的:
https://code.google.com/p/android/issues/detail?id=968
下面是google开发人员回复:
Google虽然在Android6.0解决了这个问题,但是Android6.0以下的手机市场占有率还是很多的,所以我们就要自己手动去解决这个问题。
处理404、500
在Android6.0以下我们是不能直接获取到404或者500的,Android6.0谷歌解决了这个问题。那么在Android6.0以下的系统我们如何处理404这样的问题呢?
两种解决方案:
方案一(推荐)通过获取网页的title,判断是否为系统默认的404页面
我们可以在WebChromeClient()子类中可以重写他的onReceivedTitle()方法
Android6.0以下判断404或者500:
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
// android 6.0 以下通过title获取
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
if (title.contains("404") || title.contains("500") || title.contains("Error")) {
view.loadUrl("about:blank");// 避免出现默认的错误界面
view.loadUrl(mErrorUrl);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
Android6.0以上判断404或者500:
@TargetApi(android.os.Build.VERSION_CODES.M)
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
super.onReceivedHttpError(view, request, errorResponse);
// 这个方法在6.0才出现
int statusCode = errorResponse.getStatusCode();
System.out.println("onReceivedHttpError code = " + statusCode);
if (404 == statusCode || 500 == statusCode) {
view.loadUrl("about:blank");// 避免出现默认的错误界面
view.loadUrl(mErrorUrl);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
判断断网和链接超时
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
// 断网或者网络连接超时
if (errorCode == ERROR_HOST_LOOKUP || errorCode == ERROR_CONNECT || errorCode == ERROR_TIMEOUT) {
view.loadUrl("about:blank"); // 避免出现默认的错误界面
view.loadUrl(mErrorUrl);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
方案二(不推荐) 通过HttpURLConnection请求url获取状态码
在webview加载url前先通过HttpURLConnection获取请求码,这种方案有时候不会触发shouldOverrideUrlLoading()方法,但是网上还有很多的呼声。
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
int code = msg.what;
if (code == 404 || code == 500) {
System.out.println("handler = " + code);
mWebView.loadUrl(mErrorUrl);
}
}
};
new Thread(new Runnable() {
@Override
public void run() {
int responseCode = getResponseCode(mUrl);
if (responseCode == 404 || responseCode == 500) {
Message message = mHandler.obtainMessage();
message.what = responseCode;
mHandler.sendMessage(message);
}
}
}).start();
mWebView.loadUrl(mUrl);
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
new Thread(new Runnable() {
@Override
public void run() {
int responseCode = getResponseCode(mUrl);
if (responseCode == 404 || responseCode == 500) {
Message message = mHandler.obtainMessage();
message.what = responseCode;
mHandler.sendMessage(message);
}
}
}).start();
view.loadUrl(url);
return true;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
HttpURLConnection获取请求状态码
/**
* 获取请求状态码
*
* @param url
* @return 请求状态码
*/
private int getResponseCode(String url) {
try {
URL getUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) getUrl.openConnection();
connection.setRequestMethod("GET");
connection.setReadTimeout(5000);
connection.setConnectTimeout(5000);
return connection.getResponseCode();
} catch (IOException e) {
e.printStackTrace();
}
return -1;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
方案三(推荐) 服务器后台是能获取404、500的,最好的办法是让后台捕获
这里就不说了
测试用的500的test.jsp页面