關於android WebView我趟過的坑!
最近發現偶爾還是會踩到以前踩過的坑,所以開始了我的部落格之旅,自己備忘,也可以將這些坑分享出來,幫助大家快速解決問題:
1. ## webview記憶體洩露 -異常加日誌截圖 ##
Activity com.dejia.demo.webview.WebViewActivity has leaked ServiceConnection
com.android.org.chromium.com.googl[email protected]426aef18 that was originally bound here
LeakCanary截圖:
經過一頓排查,當加上
mWebView.getSettings().setJavaScriptEnabled(true);
詞句程式碼時,就會報以上錯誤,去掉則不會洩露。從LeakCanary日誌中看到是由TextToSpeech.mContext,引用導致記憶體洩露的;通過google發現TextToSpeech.connecttoengine()
會採用上下文呼叫bindservice()
從日誌中也可以發現相關問題;
詳細請轉本地址瞭解
解決方案:
1.方案一:採用微信和QQ的做法,就是 當你要用webview的時候,記得最好 另外單獨開一個程序 去使用webview 並且當這個 程序結束時,請手動呼叫System.exit(0),目前本人感覺是最好的解決辦法。
2.方案二 :在程式碼中動態引用Application的上下文建立webview,
mWebView = new WebView(getApplicationContext());
注意:採用方案二 ,載入個別頁面可能會出現功能不能用或者導致應用崩潰,如果影響功能,那就放棄此方法;
2.## 通過h5調支付寶支付,載入頁面失敗 ##
如果點選返回,我們可以看到正在載入支付寶web支付頁面,並且又迅速進入上面的截圖頁面,這是因為在調支付寶付款的時候支付寶會通過Uri調本地支付寶app, 而如果我們在程式碼中像如下這樣配置就會找不到網頁:
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super .shouldOverrideUrlLoading(view,url);
mWebView.loadUrl(url);
return true;
}
通過列印log發現訪問不到的url為
alipays://platformapi/startApp?+支付資訊
所以會找不地址,我們應該建立隱式意圖去啟動支付寶客戶端;
解決方案如下:
if (url.contains("alipays://platformapi")){
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
List<ResolveInfo> resolveInfos = getPackageManager().queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
if(resolveInfos.size() > 0){
startActivity( intent );
}
}else{
mWebView.loadUrl(url);
}
如果本地存在支付寶app則優先喚起app進行支付,如果本地沒有安裝支付寶app則在web進行支付寶支付。
通過上邊處理方法,如果本地沒有安裝app的話不會導致拋異常,如果直接startActivity(new Intent(…))會找不到該指向並拋異常;
3.## webview清理Cookie ##
在專案中,前端同志在獲取獲取token或cookie的時候優先從快取的Cookie中獲取,如果退出webview或是專案的時候未進行清理本地Cookie快取,則有可能會在攜帶過期token或Cookie訪問伺服器的時候不能正常訪問,所以在退出webview或是登出登入的時候清楚快取Cookie;
處理辦法:
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
4.## webview退出時候,釋放記憶體佔用,防止記憶體洩漏 ##
直接上程式碼:
@Override
protected void onDestroy() {
super.onDestroy();
if (mWebView != null) {
mWebView.removeAllViews();
((ViewGroup) mWebView.getParent()).removeView(mWebView);
mWebView.setTag(null);
mWebView.clearHistory();
mWebView.destroy();
mWebView = null;
}
}
5.## web頁面調本地相機拍照或相簿時無反應 ##
如果遇到此問題,則需要在改WebViewActivity中進行一些配置,程式碼如下:
private ValueCallback<Uri> mUploadMessage;// 表單的資料資訊
private ValueCallback<Uri[]> mUploadCallbackAboveL;
private final static int FILECHOOSER_RESULTCODE = 1;// 表單的結果回撥</span>
private Uri imageUri;
WebChromeClient webChromeClient = new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
}
//=========h5調相機相簿==========================================================
@Override
public boolean onShowFileChooser(WebView webView,
ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams) {
mUploadCallbackAboveL = filePathCallback;
take();
return true;
}
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
mUploadMessage = uploadMsg;
take();
}
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
mUploadMessage = uploadMsg;
take();
}
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
mUploadMessage = uploadMsg;
take();
}
//=========h5調相機相簿==========================================================
};
//=========h5調相機相簿==========================================================
private void take() {
File imageStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "MyApp");
// Create the storage directory if it does not exist
if (!imageStorageDir.exists()) {
imageStorageDir.mkdirs();
}
File file = new File(imageStorageDir + File.separator + "IMG_" + String.valueOf(System.currentTimeMillis()) + ".jpg");
imageUri = Uri.fromFile(file);
final List<Intent> cameraIntents = new ArrayList<Intent>();
final Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
final PackageManager packageManager = getPackageManager();
final List<ResolveInfo> listCam = packageManager.queryIntentActivities(captureIntent, 0);
for (ResolveInfo res : listCam) {
final String packageName = res.activityInfo.packageName;
final Intent i = new Intent(captureIntent);
i.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
i.setPackage(packageName);
i.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
cameraIntents.add(i);
}
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("image/*");
Intent chooserIntent = Intent.createChooser(i, "Image Chooser");
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(new Parcelable[]{}));
WebViewActivity.this.startActivityForResult(chooserIntent, FILECHOOSER_RESULTCODE);
}
//=========h5調相機相簿==========================================================
@SuppressWarnings("null")
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void onActivityResultAboveL(int requestCode, int resultCode, Intent data) {
if (requestCode != FILECHOOSER_RESULTCODE
|| mUploadCallbackAboveL == null) {
return;
}
Uri[] results = null;
if (resultCode == Activity.RESULT_OK) {
if (data == null) {
results = new Uri[]{imageUri};
} else {
String dataString = data.getDataString();
ClipData clipData = data.getClipData();
if (clipData != null) {
results = new Uri[clipData.getItemCount()];
for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
results[i] = item.getUri();
}
}
if (dataString != null)
results = new Uri[]{Uri.parse(dataString)};
}
}
if (results != null) {
mUploadCallbackAboveL.onReceiveValue(results);
mUploadCallbackAboveL = null;
} else {
results = new Uri[]{imageUri};
mUploadCallbackAboveL.onReceiveValue(results);
mUploadCallbackAboveL = null;
}
return;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
//=========h5調相機相簿==========================================================
if (requestCode == FILECHOOSER_RESULTCODE) {
if (null == mUploadMessage && null == mUploadCallbackAboveL) return;
Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
if (mUploadCallbackAboveL != null) {
onActivityResultAboveL(requestCode, resultCode, data);
} else if (mUploadMessage != null) {
if (result != null) {
String path = getPath(getApplicationContext(),
result);
Uri uri = Uri.fromFile(new File(path));
mUploadMessage
.onReceiveValue(uri);
} else {
mUploadMessage.onReceiveValue(imageUri);
}
mUploadMessage = null;
}
}
}
//=========h5調相機相簿==========================================================
@SuppressLint("NewApi")
@TargetApi(Build.VERSION_CODES.KITKAT)
public static String getPath(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[]{split[1]};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
//=========h5調相機相簿==========================================================
public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {column};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null) cursor.close();
}
return null;
}
//=========h5調相機相簿==========================================================
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
//=========h5調相機相簿==========================================================
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
//=========h5調相機相簿=======
直接copy到你的WebViewActivity中即可,親測有效;
6. 訪問https資源無法正常顯示
在webview中載入https資源無法正常顯示,原來是因為沒有沒有識別的證書所以無法正常顯示資源,那麼我們應該設定WebView接受所有網站的證書,具體程式碼如下:
webView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedSslError(WebView view,
SslErrorHandler handler, SslError error) {
// handler.cancel();// Android預設的處理方式
handler.proceed();// 接受所有網站的證書
// handleMessage(Message msg);// 其他處理
}
});
7. 在WebView中無法通過Scheme喚起QQ或其它應用
在WebView的使用中遇到在三星手機上無法通過Scheme喚起QQ,經過排查,是因為下面這句程式碼導致:
mWebSettings.setSupportMultipleWindows(true);
那為什麼這句會導致無法開啟呢,這其實和html的程式碼有關,html頁面中設定點選按鈕開啟QQ,它設定了這個屬性
target="_blank"
也就是在新視窗開啟,但是如果設定支援多視窗,但是又沒有重寫下面onCreateWindow(…)就導致出問題;所以預設為不支援多視窗,如果設定多視窗,就必須重寫下面的onCreateWindow(…)方法:
WebChromeClient webChromeClient = new WebChromeClient() {
@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
WebView newWebView = new WebView(view.getContext());
view.addView(newWebView);
newWebView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (!TextUtils.isEmpty(url) && url.contains("mqq://im/chat")){
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
List<ResolveInfo> resolveInfos = getPackageManager().queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
if(resolveInfos.size() > 0){
startActivity( intent );
}else if(url.contains("mqq://im/chat")){ //本地未安裝QQ後的操作
Toast.makeText(getApplicationContext(),"請安裝QQ後使用此功能",Toast.LENGTH_LONG).show();
}
}else{
mWebView.loadUrl(url);
}
return true;
}
});
newWebView.setWebChromeClient(this);
WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(newWebView);
resultMsg.sendToTarget();
return true;
}
}
但其實最簡單的辦法就是不支援多視窗:直接像如下這樣設定或者不設定也可以解決問題:
mWebSettings.setSupportMultipleWindows(false);
如果我們僅僅是將WebView嵌入我們自己的應用然後載入網頁,很少有必要去設定支援多視窗。只在當前視窗載入新的網頁就可。
希望本部落格,可以幫到你 ——勤勞的碼農們!