該文章是一個系列文章,是本人在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
- 該方法的執行不會使頁面刷新,而第一種方法(loadUrl )的執行則會。因此該方法比第一種方法效率更高。
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代碼。
###三種方法優劣比較
若是JS想要獲得Android方法的返回值,只能經過 WebView 的 loadUrl ()去執行 JS 方法把返回值傳遞回去
#WebView的文件上傳 當在網頁裏有文件上傳組件時,咱們驚奇的發現Android端這個文件上傳組件並無起做用。緣由何在呢?由於Android 中的 WebView是不能直接打開文件選擇彈框的。 接下來我講簡單提供一下解決方案,先說一下思路
接收WebView打開文件選擇器的通知,收到通知後,打開文件選擇器等待用戶選擇須要上傳的文件
在onActivityResult中獲得用戶選擇的文件的Uri
而後把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)結束整個進程,內存天然回收了。該方法簡單暴力,並有如下優勢
###源碼解決法 這個方法就是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的坑也會繼續追加更新。
此致,敬禮