首個hybird商業項目踩坑總結

該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,我會盡可能按照先易後難的順序進行編寫該系列。該系列引用了《Android開發藝術探索》以及《深刻理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相關知識,另外也借鑑了其餘的優質博客,在此向各位大神表示感謝,膜拜!!!javascript


前言

前段時間作了首個hybird商業上面,hybird雖然私下裏有些瞭解,而且寫了些demo,可是作正式的商業項目仍是首次,這一篇也算是本身首個hybird項目的反思與總結吧。 注:該項目涉及到的技術大概分爲如下幾個方面,1,微信登陸 2,WebView與原生代碼的交互 3,WebView的優化,下面也分這幾個大方面進行一一說明 #微信登陸 ##微信登陸的準備 準備什麼,天然是開發者帳號以及認證開發者資質,而後建立應用,認證開發者資質須要300人民幣,而且填寫一系列資料,接着走一系列流程,這些本應該是公司應該提早準備好的事情,不過我遇到的並非這樣,拿到這些準備的東西多是整個開發環節中最費勁的事情。 ##微信登陸的斷點調試 咱們在微信開放平臺建立移動應用時,須要填入應用簽名以及應用包名,以下圖 這裏寫圖片描述 其實咱們若是想要斷點調試WXEntryActivity類,那麼咱們只須要Debug包的簽名與上面的應用簽名保持一致,那麼咱們便能以Debug的方式運行安裝包,斷點調試微信登陸、分享之類的功能html

#WebView的基本信息 除去WebView外,在開發中咱們還常常用到其餘的WebView工具類 ###WebSettingsjava

對WebView進行配置和管理android

//若是訪問的頁面中要與Javascript交互,則webview必須設置支持Javascript
webSettings.setJavaScriptEnabled(true);  
// 若加載的 html 裏有JS 在執行動畫等操做,會形成資源浪費(CPU、電量)
// 在 onStop 和 onResume 裏分別把 setJavaScriptEnabled() 給設置成 false 和 true 便可

//支持插件
webSettings.setPluginsEnabled(true); 

//設置自適應屏幕,二者合用
webSettings.setUseWideViewPort(true); //將圖片調整到適合webview的大小 
webSettings.setLoadWithOverviewMode(true); // 縮放至屏幕的大小

//縮放操做
webSettings.setSupportZoom(true); //支持縮放,默認爲true。是下面那個的前提。
webSettings.setBuiltInZoomControls(true); //設置內置的縮放控件。若爲false,則該WebView不可縮放
webSettings.setDisplayZoomControls(false); //隱藏原生的縮放控件

//其餘細節操做
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); //關閉webview中緩存 
webSettings.setAllowFileAccess(true); //設置能夠訪問文件 
webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持經過JS打開新窗口 
webSettings.setLoadsImagesAutomatically(true); //支持自動加載圖片
webSettings.setDefaultTextEncodingName("utf-8");//設置編碼格式

###WebClientweb

處理各類通知 & 請求事件瀏覽器

mWebView.setWebViewClient(new FNWebViewClient());

private class FNWebViewClient extends WebViewClient {
        //複寫shouldOverrideUrlLoading()方法,使得打開網頁時不調用系統瀏覽器, 而是在本WebView中顯示
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {

            // 特定的url調到native 頁面進行處理 返回true
            if (LinkHandleUtils.handle(FNWebPageActivity.this, url, true)) {
                return true;
            }

            mCurUrl = url;
            return false;
        }

        //開始載入頁面調用的,咱們能夠設定一個loading的頁面,告訴用戶程序在等待網絡響應。
        @Override
        public void onPageStarted(WebView webView, String s, Bitmap bitmap) {
            super.onPageStarted(webView, s, bitmap);
        }

        //在頁面加載結束時調用。咱們能夠關閉loading 條,切換程序動做
        @Override
        public void onPageFinished(WebView webView, String s) {
            super.onPageFinished(webView, s);
        }

        //在加載頁面資源時會調用,每個資源(好比圖片)的加載都會調用一次。
        @Override
        public void onLoadResource(WebView webView, String s) {
            super.onLoadResource(webView, s);
        }

        //加載頁面的服務器出現錯誤時(如404)調用
        @Override
        public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
            super.onReceivedError(view, errorCode, description, failingUrl);
        }

        //處理https請求
        @Override
        public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError) {
            sslErrorHandler.proceed();    //表示等待證書響應
            // sslErrorHandler.cancel();      //表示掛起鏈接,爲默認方式
            // sslErrorHandler.handleMessage(null);    //可作其餘處理
        }
    }

###WebChromeClient緩存

輔助 WebView 處理 Javascript 的對話框,網站圖標,網站標題等等。安全

setWebChromeClient(new ProgressWebChromeClient());

private class ProgressWebChromeClient extends WebChromeClient {
   //得到網頁的加載進度並顯示
   @Override
   public void onProgressChanged(com.tencent.smtt.sdk.WebView webView, int newProgress) {
       if (newProgress <= 100 && mProgressBar != null) {
           if (GONE == mProgressBar.getVisibility()) {
               mProgressBar.setVisibility(VISIBLE);
           }
           startProgressAnimation(newProgress);
       }
       super.onProgressChanged(webView, newProgress);
   }

   //獲取Web頁中的標題
   @Override
   public void onReceivedTitle(WebView webView, String title) {
       super.onReceivedTitle(webView, title);
       if (mCallback != null && StringUtils.isNotBlank(title)) {
           mCallback.setTitle(title);
       }
   }

   //支持javascript的警告框
   @Override
   public boolean onJsAlert(WebView webView, String url, String message, final JsResult result) {
       new AlertDialog.Builder(getContext())
               .setTitle("JsAlert")
               .setMessage(message)
               .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                   @Override
                   public void onClick(DialogInterface dialog, int which) {
                       result.confirm();
                   }
               })
               .setCancelable(false)
               .show();
       return true;
   }

   //支持javascript的確認框
   @Override
   public boolean onJsConfirm(WebView webView, String url, String message, final JsResult jsResult) {
       new AlertDialog.Builder(getContext())
               .setTitle("JsConfirm")
               .setMessage(message)
               .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                   @Override
                   public void onClick(DialogInterface dialog, int which) {
                       jsResult.confirm();
                   }
               })
               .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                   @Override
                   public void onClick(DialogInterface dialog, int which) {
                       jsResult.cancel();
                   }
               })
               .setCancelable(false)
               .show();
       // 返回布爾值:判斷點擊時確認仍是取消
       // true表示點擊了確認;false表示點擊了取消;
       return true;
   }

   //支持javascript輸入框
   @Override
   public boolean onJsPrompt(WebView webView, String url, String message, String defaultValue, final JsPromptResult result) {
       return super.onJsPrompt(webView, s, s1, s2, jsPromptResult);
   }
}

#WebView與原生代碼的交互 ##Java->JS ###loadUrl服務器

//mJSMethodName對應js方法名
//result對應js方法參數
mWebView.loadUrl("javascript:" + mJSMethodName + "(\" " + param + "\")");

對應的html文件以下微信

<!DOCTYPE html>
<html>
   <head>
      <meta charset="utf-8">
// JS代碼
     <script>
// Android須要調用的方法
   function mJSMethodName(){
      alert("Android調用了JS的mJSMethodName方法");
   }
</script>

   </head>

</html>

特別注意:JS代碼調用必定要在 onPageFinished() 回調以後才能調用,不然不會調用。 ###evaluateJavascript

  1. 該方法的執行不會使頁面刷新,而第一種方法(loadUrl )的執行則會。因此該方法比第一種方法效率更高。
  1. Android 4.4 後纔可以使用
mWebView.evaluateJavascript("javascript:" + mJSMethodName + "(\" " + param + "\")", new ValueCallback<String>() {
                @Override
                public void onReceiveValue(String result) {
                    //result爲js方法返回結果
                }
            });

注:上面兩種方法各有優劣,建議根據Android版本混合使用,

// Android版本變量
final int version = Build.VERSION.SDK_INT;
// 由於該方法在 Android 4.4 版本纔可以使用,因此使用時需進行版本判斷
if (version < 18) {
    mWebView.loadUrl("javascript:" + mJSMethodName + "(\" " + param + "\")");
} else {
    mWebView.evaluateJavascript("javascript:" + mJSMethodName + "(\" " + param + "\")", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String result) {
            //result爲js方法返回結果
        }
    });
}

##JS->Java ###經過WebView的addJavascriptInterface()方法 這種方法是咱們最經常使用的方法,使用方法以下

//添加映射對象以及命名空間
 mWebView.addJavascriptInterface(new JsInteration(), "android");
 
private class JsInteration {
        @JavascriptInterface
        public void hello(String messsage) {
        }
    }

上面的java代碼對應的js代碼是

//
//注意android是上面定義的命名空間
window.android.hello(message)

###經過WebViewClient 的shouldOverrideUrlLoading()方法回調 這個咱們已經在上面的代碼裏寫過了,好比你能夠本身維護一些特殊的URL以及處理這些URL的Activity,而後複寫shouldOverrideUrlLoading(),在該方法中攔截特定URL轉到特定的Activity進行處理。也能達到JS->Java的目的。而且這種形式也是比較常見的處理方式。

//複寫shouldOverrideUrlLoading()方法,使得打開網頁時不調用系統瀏覽器, 而是在本WebView中顯示
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {

            // 特定的url調到native 頁面進行處理 返回true
            if (LinkHandleUtils.handle(FNWebPageActivity.this, url, true)) {
                return true;
            }

            mCurUrl = url;
            return false;
        }

###經過WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調 這種方法跟上面的沒有本質差別,也是在回調函數中進行Java代碼操做,目前我在項目中用到的地方較少,主要用來作一些比較特殊的功能,例如檢測到Alert彈框中的內容符合條件進行Java代碼。

###三種方法優劣比較

  1. 經過WebView的addJavascriptInterface()方法比較簡單,而且也更爲常見,不過其存在不小的安全隱患。
  2. 經過WebViewClient 的shouldOverrideUrlLoading()方法回調這個使用起來也比較簡單,也不存在方式1的安全隱患,不過JS獲取Android方法的返回值複雜。

若是JS想要獲得Android方法的返回值,只能經過 WebView 的 loadUrl ()去執行 JS 方法把返回值傳遞回去

#WebView的文件上傳 當在網頁裏有文件上傳組件時,咱們驚奇的發現Android端這個文件上傳組件並無起做用。緣由何在呢?由於Android 中的 WebView是不能直接打開文件選擇彈框的。 接下來我講簡單提供一下解決方案,先說一下思路

  1. 接收WebView打開文件選擇器的通知,收到通知後,打開文件選擇器等待用戶選擇須要上傳的文件

  2. 在onActivityResult中獲得用戶選擇的文件的Uri

  3. 而後把Uri傳遞給Html5

這樣就完成了一次H5選擇文件的過程,下面我把代碼貼出來看一下

1.當H5在調用上傳文件的Api的時候,WebView會回調 openFileChooser和onShowFileChooser 方法來通知咱們,那咱們就得重寫了

須要注意的是openFileChooser在不一樣的Android版本上是形參不一樣的,

private class ProgressWebChromeClient extends WebChromeClient {
        //支持文件選擇上傳
        @Override
        public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> valueCallback, FileChooserParams fileChooserParams) {
            return super.onShowFileChooser(webView, valueCallback, fileChooserParams);
        }

        // Android > 4.1.1 調用這個方法
        public void openFileChooser(ValueCallback<Uri> uploadMsg,
                                    String acceptType, String capture) {
            if (mFileUploadSupportListener == null)
                return;
            //調用傳入的接口進行回調
            mFileUploadSupportListener.call(uploadMsg);
        }


        // 3.0 + 調用這個方法
        public void openFileChooser(ValueCallback<Uri> uploadMsg,
                                    String acceptType) {
            if (mFileUploadSupportListener == null)
                return;
            mFileUploadSupportListener.call(uploadMsg);
        }

        // Android < 3.0 調用這個方法
        public void openFileChooser(ValueCallback<Uri> uploadMsg) {
            if (mFileUploadSupportListener == null)
                return;
            mFileUploadSupportListener.call(uploadMsg);
        }
    }

2.注入接口

//注入接口
 mWebView.setFileUploadSupportListener(new IFileUploadSupportListener() {
      @Override
      public void call(ValueCallback<Uri> valueCallback) {
          mUploadMessage = valueCallback;
          chooseFile();
      }
  });
 //選擇文件
private void chooseFile() {
        PhotoPicker.builder()
                .setPhotoCount(1)
                .setShowCamera(true)
                .setShowGif(true)
                .setPreviewEnabled(false)
                .start(FNWebPageActivity.this, PhotoPicker.REQUEST_CODE);
    }

3.進行回傳

if (null == mUploadMessage) {
    return;
}
if (resultCode == RESULT_OK && requestCode == PhotoPicker.REQUEST_CODE) {
    ArrayList<String> photos = data.getStringArrayListExtra(PhotoPicker.KEY_SELECTED_PHOTOS);
    Uri result = Uri.parse(photos.get(0));
    mUploadMessage.onReceiveValue(result);
    mUploadMessage = null;
} else {
    mUploadMessage.onReceiveValue(null);
}

#WebView的優化

##WebView的addJavascriptInterface()方法的安全隱患 上面已經稍微說了一下,該方法只能在Android4.4以上安全使用,那麼咱們來看一下Android 系統佔比,Google公佈的數據:截止 2018 .6 .28 ,Android4.4 之下佔有約5%,具體佔好比下圖 這裏寫圖片描述 如今Android4.4 之下的Android手機已經佔比很是少了,不過有興趣的同窗可參看你不知道的 Android WebView 使用漏洞 ,該篇文章比較詳細的解析瞭如何解決該安全隱患

##WebView的內存泄露 WebView的內存泄露問題已是個老生常談的問題了,如今只要用到WebView的開發者都得注意到這個問題。 如今流行的有如下兩種解決方案 ###獨立進程法 獨立進程法顧名思義是讓包含WebView的Acitivy以android:process=":web"的形式指定單獨進程,而後在須要退出的時候使用System.exit(0)結束整個進程,內存天然回收了。該方法簡單暴力,並有如下優勢

  1. 每一個獨立的進程都能分配獨立的內存,這樣的話,你的app能夠得到雙倍的內存,其中一半給Webview吃。增大Webview得到的內存,變相的減少內存泄露產生OOM的機率。
  2. 在適當時機直接殺掉Webview獨立進程,什麼內存泄露,內存佔用巨大的問題都見鬼去吧。要問什麼時機?好比退出app時,檢測到沒有Webview頁面時。
  3. Webview發生崩潰時不會致使app閃退,就像第二點說的,由於Webview是在獨立進程中,若是發生崩潰,主進程還安然無事,app還在運行中,沒有閃退,不閃的纔是健康的。

###源碼解決法 這個方法就是RTFSC(Read The Fucking Source Code),從LeakCannary分析得出內存泄露在 org.chromium.android_webview.AwContents 類

//org.chromium.android_webview.AwContents 類的onAttachedToWindow() 和  onDetachedFromWindow()方法
@Override
public void onAttachedToWindow() {
    if (isDestroyed()) return;
    if (mIsAttachedToWindow) {
        Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
        return;
    }
    mIsAttachedToWindow = true;
 
    mContentViewCore.onAttachedToWindow();
    nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(),
            mContainerView.getHeight());
    updateHardwareAcceleratedFeaturesToggle();
 
    if (mComponentCallbacks != null) return;
    mComponentCallbacks = new AwComponentCallbacks();
    mContext.registerComponentCallbacks(mComponentCallbacks);
}
 
@Override
public void onDetachedFromWindow() {
    if (isDestroyed()) return;//注意這裏
    if (!mIsAttachedToWindow) {
        Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
        return;
    }
    mIsAttachedToWindow = false;
    hideAutofillPopup();
    nativeOnDetachedFromWindow(mNativeAwContents);
 
    mContentViewCore.onDetachedFromWindow();
    updateHardwareAcceleratedFeaturesToggle();
 
    if (mComponentCallbacks != null) {
        mContext.unregisterComponentCallbacks(mComponentCallbacks);
        mComponentCallbacks = null;
    }
 
    mScrollAccessibilityHelper.removePostedCallbacks();
}

通常狀況下,咱們的activity退出的時候,都會主動調用 WebView.destroy() 方法,通過分析,destroy()的執行時間在onDetachedFromWindow以前,因此就會致使不能正常進行unregister(),從而形成內存泄露。

知道緣由了,那麼解決辦法也就來了。 在Activity的onDestroy裏方法裏以下代碼

@Override
protected void onDestroy() {
   if (mWebView != null) {
            try {
                ViewGroup parent = (ViewGroup) mWebView.getParent();
                if (parent != null) {
                    parent.removeView(mWebView);
                }
                mWebView.removeAllViews();
                mWebView.destroy();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        super.onDestroy();
}

##X5WebView 儘管有了上述的一些優化,不過原生WebView的一些不足,如兼容性、流量消耗、以及性能等諸多方面仍是不能達到要求,不過騰訊提供的X5WebView算是目前比較好的解決方案了,關於X5WebView詳情讀者看參看騰訊官網騰訊瀏覽服務


本篇總結

本篇呢是首個hybird的項目的踩坑總結,有什麼不足之處還請不吝賜教,之後在開發過程當中遇到的更多的WebView的坑也會繼續追加更新。


此致,敬禮

相關文章
相關標籤/搜索