Android與WebView的同步和非同步訪問機制

來源:互聯網
上載者:User

標籤:android   webviewclient   webchromeclient   同步   非同步   

大家都知道,通過WebView,我們可以在Android用戶端,用Web開發的方式來開發我們的應用。

如果一個應用就是單純一個WebView,所有的邏輯都只需要在網頁上互動的話,那我們其實就只需要通過html和javascript來跟伺服器互動就可以了。

但是很多情況下,我們的應用不是單純一個WebView就可以了,有可能會需要運用到Android本身的應用,比如拍照,就需要調用Android本身的照像機等,要產生震動,在需要運用到手機特性的一些情境下,肯定需要這麼一套機制在javascript和Android之間互相通訊,包括同步和非同步方式,而這套機制就是本文中我想要介紹的。

一步一步來,我們先從最簡單的地方講起:

1)需要一個WebView去展現我們的頁面,首先定義一個布局,非常簡單,就是一個WebView,如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width= "match_parent"    android:layout_height= "match_parent"    android:orientation= "vertical">    <WebView        android:id="@+id/html5_webview"        android:layout_width="match_parent"        android:layout_height="match_parent" /></LinearLayout>

這個WebView就是承載我們頁面展現的一個最基本的控制項,所有在頁面上的邏輯,需要跟Android原生環境互動的邏輯資料都是通過它來傳輸的。

2)在對應的Activity中,對WebView進行一些初始化

mWebView = (WebView) findViewById(R.id. html5_webview );WebSettings webSettings = mWebView.getSettings();webSettings.setJavaScriptCanOpenWindowsAutomatically( true );webSettings.setJavaScriptEnabled( true );webSettings.setLayoutAlgorithm(LayoutAlgorithm. NORMAL );mWebView.setWebChromeClient( new WebServerChromeClient());mWebView.setWebViewClient( new WebServerViewClient());mWebView.setVerticalScrollBarEnabled( false );mWebView.requestFocusFromTouch();mWebView.addJavascriptInterface( new AppJavascriptInterface(), "nintf" );


在上面的代碼中,主要是對WebView的一些初始化,但其中最重要的幾句代碼是這麼幾句:

2.1)webSettings.setJavaScriptEnabled( true );

告訴WebView,讓它能夠去執行JavaScript語句。在一個互動的網頁上,javascript是沒辦法忽略的。

2.2)mWebView.setWebChromeClient( new WebServerChromeClient());2.3)mWebView.setWebViewClient( new WebServerViewClient());
WebChromeClient和WebViewClient是WebView應用中的兩個最重要的類。

通過這兩個類,WebView能夠捕獲到Html頁面中url的載入,javascript的執行等的所有操作,從而能夠在Android的原生環境中對這些來自網頁上的事件進行判斷,解析,然後將對應的處理結果返回給html網頁。

這兩個類是html頁面和Android原生環境互動的基礎,所有通過html頁面來跟後台互動的操作,都在這兩個類裡面實現,在後面我們還會詳細說明。

2.4)mWebView.addJavascriptInterface( new AppJavascriptInterface(), "nintf" );

這個JavascriptInterface,則是Android原生環境和javascript互動的另一個視窗。

將我們自訂的AppJavascriptInterface類,調用mWebView的addJavascriptInterface方法,可以將這個對象傳遞給mWebView中Window對象的nintf屬性("nintf"這個屬性名稱是自訂的)之後,

就可以直接在javascript中調用這個Java對象的方法。


3)接下來,我們就先來看看在Html中的javascript是如何跟Android原生環境來互動的。

我們按照事件發生的順序機制來看,這樣有個先後的概念,理解起來會容易一點。

在這套機制中,提供了兩種訪問Android原生環境的方法,一種是同步的,一種是非同步。

同步的概念就是說,我在跟你交流的時候,如果我還沒有收到你的回複,我是不能跟其他人交流的,我必須等在那裡,一直等著你。

非同步概念就是說,我在跟你交流的時候,如果你還沒有回複我,我還能夠去跟其他人交流,而當我收到你的回複的時候,再去看看你的回複,應該要幹些什麼。

3.1)同步訪問

在Javascript中,我們定義了這樣一個方法,如下:

var exec = function (service, action, args) {        var json = {               "service" : service,               "action" : action       };        var result_str = prompt(JSON.stringify(json), args);        var result;        try {              result = JSON.parse(result_str);       } catch (e) {              console.error(e.message);       }        var status = result.status;        var message = result.message;        if (status == 0) {               return message;       } else {              console.error( "service:" + service + " action:" + action + " error:" + message);       }}

而對此方法的,典型的調用如下:

exec( "Toast", "makeTextShort" , JSON.stringify(text));

其中Toast和makeTextShort是要調用Android原生環境的服務和參數,這些都是在PluginManager中管理的,在下一篇文章中會提及到。

在這裡,我們調用了prompt方法,通過這個方法,在WebView中定義的的WebChromeClient就會攔截到這樣一個方法,具體代碼如下:

class WebServerChromeClient extends WebChromeClient {     @Override    public boolean onJsPrompt(WebView view, String url, String message,              String defaultValue, JsPromptResult result) {            System.out.println( "onJsPrompt:defaultValue:" + defaultValue + "|" + url + "," + message);         JSONObject args = null ;         JSONObject head = null ;         try {              head = new JSONObject(message);                          args = new JSONObject(defaultValue);              String execResult = mPluginManager.exec(head.getString(IPlugin.SERVICE),                        head.getString(IPlugin.ACTION), args);              result.confirm(execResult);              return true;         ...            }         }

在這裡,我們會重載WebChromeClient的onJsPrompt方法,當此方法返回true的時候,就說明WebChromeClient已經處理了這個prompt事件,不需要再繼續分發下去;

而當返回false的時候,則此事件會繼續傳遞給WebView,由WebView來處理。

由於我們這裡是要利用這個Prompt方法,來實現Javascript跟Android原生環境之間的同步訪問,所以我們在這裡會攔截這個事件進行處理。

在這裡,通過message和defaultValue,我們可以拿到javascript中prompt方法兩個參數的值,在這裡,它們是Json資料,在這裡進行解析之後,由PluginManager來進行處理,最後將結果返回給JsPromptResult的confirm方法中。

此結果就是javascript中prompt的返回值了。

而除了JsPrompt,還有類似Javascript中的Alert方法等,我們知道瀏覽器彈出的Alert視窗跟我們手機應用中視窗風格樣式是很不一樣的,而作為一個應用,風格肯定要有一套統一的標準,所以一般情況下,我們也會攔截WebView中的Alert視窗,這個邏輯也同樣會是在這裡處理,如下:

@Overridepublic boolean onJsAlert(WebView view, String url, String message,               final JsResult result) {       System. out .println("onJsAlert : url:" + url + " | message:" + message);        if (isFinishing()) {               return true ;       }       CustomAlertDialog.Builder customBuilderres = new CustomAlertDialog.Builder(DroidHtml5.this );       customBuilderres.setTitle( "資訊提示" ).setMessage(message)                     .setPositiveButton( "確定" , new DialogInterface.OnClickListener() {                            public void onClick(DialogInterface dialog, int which) {                                  dialog.dismiss();                                  result.confirm();                           }                     }).create().show();        return true ;}


上面描述的都是同步訪問Android原生環境的方式,那麼,非同步訪問方式是怎麼樣的呢?

3.2)非同步訪問

同樣的,我們會在Javascript中定義如下一個方法:

var exec_asyn = function(service, action, args, success, fail) {       var json = {               "service" : service,               "action" : action       };                     var result = AndroidHtml5.callNative(json, args, success, fail);  }


我們會調用AndroidHtml5的callNative,此方法有四個參數:

a)json:是調用的服務和操作

b)args: 對應的參數數

c)success : 成功時的回調方

d)fail:失敗時的回調方

典型的調用如下:

var success = function(data){};var fail = functio(data){};exec_asyn( "Contacts", "openContacts" , '{}', success, fail);

在這裡,AndroidHtml5是在Javascript中定義的一個對象,它提供了訪問Android原生環境的方法,以及回調的隊列函數。它的定義如下:
var AndroidHtml5 = {       idCounter : 0,                 // 參數序列計數器       OUTPUT_RESULTS : {},      // 輸出的結果              CALLBACK_SUCCESS : {},  // 輸出的結果成功時調用的方法               CALLBACK_FAIL : {},       // 輸出的結果失敗時調用的方法       callNative : function (cmd, args, success, fail) {              var key = "ID_" + (++ this.idCounter);                           window.nintf.setCmds(cmd, key);              window.nintf.setArgs(args, key);                            if (typeof success != 'undefined'){                    AndroidHtml5.CALLBACK_SUCCESS[key] = success;              } else {                    AndroidHtml5.CALLBACK_SUCCESS[key] = function (result){};              }                            if (typeof fail != 'undefined'){                    AndroidHtml5.CALLBACK_FAIL[key] = fail;              } else {                    AndroidHtml5.CALLBACK_FAIL[key] = function (result){};              }                            //下面會定義一個Iframe,Iframe會去載入我們自訂的url,以androidhtml:開頭                                       var iframe = document.createElement("IFRAME" );              iframe.setAttribute( "src" , "androidhtml://ready?id=" + key);              document.documentElement.appendChild(iframe);              iframe.parentNode.removeChild(iframe);              iframe = null ;              return this .OUTPUT_RESULTS[key];       },        callBackJs : function (result,key) {               this .OUTPUT_RESULTS[key] = result;               var obj = JSON.parse(result);               var message = obj.message;               var status = obj.status;                                if (status == 0) {                      if (typeof this.CALLBACK_SUCCESS[key] != "undefined"){                           setTimeout( "AndroidHtml5.CALLBACK_SUCCESS['" +key+"']('" + message + "')", 0);                     }              } else {                      if (typeof this.CALLBACK_FAIL != "undefined") {                           setTimeout( "AndroidHtml5.CALLBACK_FAIL['" +key+"']('" + message + "')" , 0);                     }              }       }};

在AndroidHtml5中,有幾個地方我們需要注意的。
a)大家還記得我們在WebView初始化時設定的AppJavascriptInterface嗎?當時自訂的名稱就是"nintf",而在此時,在javascript中,我們就可以直接來運用這個對象所有的方法。
window.nintf.setCmds(cmd, key);window.nintf.setArgs(args, key);

我們也看一下這個AppJavascriptInterface中的方法,如下:
public class AppJavascriptInterface implements java.io.Serializable {                private static Hashtable<String,String> CMDS = new Hashtable<String,String>();        private static Hashtable<String,String> ARGS = new Hashtable<String,String>();                       @JavascriptInterface        public void setCmds(String cmds, String id) {               CMDS .put(id, cmds);       }                   @JavascriptInterface        public void setArgs(String args, String id) {               ARGS .put(id, args);       }            public static String getCmdOnce(String id) {              String result = CMDS .get(id);               CMDS .remove(id);               return result;       }        public static String getArgOnce(String id) {              String result = ARGS .get(id);               ARGS .remove(id);               return result;       }}

這個類是簡潔而不簡單,通過在Javascript中調用類中的set方法,將對應的cmd和args參數給儲存起來,目的是為了儲存非同步請求中多次的命令和操作,然後在Android原生環境中再取出來。

b)第二步呢,也是最重要的一步,會建立一個Iframe,在Iframe中申明一個url,而且是以androidhtml: 開頭的。
在上面我們提過,WebView在初始化的時候,會設定一個WebViewClient,這個類的主要作用就是,當在html頁面中發生url載入的時候,我們可以攔截這個載入事件,進行處理,重寫這次載入事件。
而我們正好是利用了這一點,利用一個Iframe來觸發一次Url的攔截事件。
我們來看一下WebViewClient中是如何?這個非同步請求的實現的。

class WebServerViewClient extends WebViewClient {              Handler myHandler = new Handler() {               ...       };        @Override        public boolean shouldOverrideUrlLoading(WebView view, String url) {               if (url != null && url.startsWith( "androidhtml")) {                    String id = url.substring(url.indexOf( "id=" ) + 3);                    JSONObject cmd = null ;                    JSONObject arg = null ;                     try {                           String cmds = AppJavascriptInterface.getCmdOnce(id);                           String args = AppJavascriptInterface.getArgOnce(id);                           cmd = new JSONObject(cmds);                           arg = new JSONObject(args);                    } catch (JSONException e1) {                           e1.printStackTrace();                            return false ;                    }                    //另起線程處理請求                     try {                           AsynServiceHandler asyn = new AsynServiceHandlerImpl();                           asyn.setKey(id);                            asyn.setService(cmd.getString( "service" ));                           asyn.setAction(cmd.getString( "action" ));                           asyn.setArgs(arg);                           asyn.setWebView( mWebView);                           asyn.setMessageHandler( myHandler );                           Thread thread = new Thread(asyn, "asyn_" + (threadIdCounter ++));                           thread.start();                    } catch (Exception e) {                           e.printStackTrace();                            return false;                    }                     return true ;              }              //如果url不是以Androidhtml開頭的,則由WebView繼續去處理。              view.loadUrl(url);              return true ;       }       }
我們可以看到,在這方法中,首先只有以androidhtml開頭的url才會被攔截處理,而其他的url則還是由WebView進行處理。

而通過AppJavascriptInterface,我們將在Javascript中儲存的cmds和args等資料都拿出來了,並由AsynServiceHandler新啟一個線程去處理。

我們再來看看AsynServiceHandlerImpl是怎麼實現的,

public class AsynServiceHandlerImpl implements AsynServiceHandler {        @Override        public void run() {                        try {              final String responseBody = PluginManager.getInstance().exec(service,  action,args);                                   handler.post( new Runnable() {                      public void run() {                                  webView .loadUrl( "javascript:AndroidHtml5.callBackJs('"+responseBody+ "','" +key +"')" );                      }              });           } catch (PluginNotFoundException e) {               e.printStackTrace();           }       }
可以看到,當調用PluginManager操作完對應的命令和資料之後,會通過WebView的loadUrl方法,去執行AndroidHtml5的callBackJs方法。

通過key值,我們就可以在AndroidHtml5中的callBackJs方法中找回到對應的回調方法,進行處理。

因此,通過一次Iframe的構建,載入以androidhtml開頭的url,再利用WebView的WebViewClient介面對象,我們就能夠在Html頁面中和Android原生環境進行非同步互動了。

在這一篇文章中,我們幾處地方講到了PluginManager這個類,這是一個管理HTML和Android原生環境互動介面的類。

因為如果把所有的邏輯都放在WebViewClient或者WebChromeClient這兩個都來處理,這是不合理的,亂,複雜,看不懂。

所以我們需要把邏輯實現跟互動給分開來,這個機制才顯得漂亮,實用,易操作。


聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.