1. 程式人生 > >關於android WebView我趟過的坑!

關於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嵌入我們自己的應用然後載入網頁,很少有必要去設定支援多視窗。只在當前視窗載入新的網頁就可。

希望本部落格,可以幫到你 ——勤勞的碼農們!