Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android與WebView的同步和異步訪問機制

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

編輯:關於Android編程

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

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

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

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

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



    


這個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窗口,這個邏輯也同樣會是在這裡處理,如下:

@Override
public 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 CMDS = new Hashtable();
        private static Hashtable ARGS = new Hashtable();
               
        @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這兩個都來處理,這是不合理的,亂,復雜,看不懂。

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


  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved