android PhoneGap源碼詳解

傳送門:
討論q羣:248908795
PhoneGap 源碼解析
以前有一位前輩已經寫了 PhoneGap android 源碼的解析。可是,前輩寫得比較簡單,只是把通訊原理提了一提。本篇源碼解析,會對 PhoneGap 作一個全面的介紹。
關於 Java/JS 互調,鄙人接觸也有一段時間了。在 android sdk 文檔中 , 也有用 JsInterface loadUrl 作到交互的示例。但令我驚訝的是 ,PhoneGap 並無選擇用 JsInterface ,而是使用攔截 prompt 這種 hack 作法。
PhoneGap android 源碼寫得稍稍有點凌亂和囉嗦,後面會詳細解析。好了,不廢話了。開始正文了
1、JS 層與 Native 層之間通訊原理
在講解這部分以前,我先解釋 PhoneGap 中的插件的概念。
Plugin: 插件。插件具有標準 js 沒有的功能,如打電話、查看電池狀態。這部分功能須要經過本地代碼調用實現。每一個插件都會對外提供至少一個方法。
lib/common/notification.js 這個插件。它具有了 alert,confirm,vibrate( 震動 ),beep( 蜂鳴 ) 這幾個方法。
很顯然,編寫插件有兩個要點。首先 , 須要編寫一個實現插件功能的本地代碼。其次,須要編寫一個暴露調用接口的 js 代碼來供使用插件者調用。
當編寫完插件後,問題就來了。 Js 接口代碼怎麼去調用本地代碼 ? 本地代碼執行完畢後,怎麼去回調 Js? 如何處理同步回調和異步回調 ? 這些通訊問題的解決纔是 PhoneGap 框架的精華所在。
下面咱們逐一看看 phoneGap 是如何解決這些問題的。
      1. Js 接口代碼怎麼去調用本地代碼 ?
lib/android/exec.js, 咱們找到一個稱爲 exec 的關鍵模塊。它是 js 層調用本地代碼的入口。
它的定義是 exec(success, fail, service, action, args) 。順便多說一句 , 雖然 exec PhoneGap 的一個關鍵模塊,但因爲受到平臺差別影響,各個平臺 exec 的實現方式並不相同。
 
 
Java代碼   收藏代碼
  1. var r = prompt(JSON.stringify(args), "gap:"+JSON.stringify([service, action, callbackId, true]));  
  2.    
 
 
這句 prompt 便實現了本地代碼調用。本地代碼經過 WebChromeClient 攔截 onJsPrompt 回調,利用 gap: 開頭標誌得知是調用本地插件請求 , 而後向 PluginManager 轉發該請求。 PluginManager 將會根據參數來查找並執行具體插件方法。 關於 PluginManager, 後面會作更詳細的解釋。
      2. 本地代碼怎麼去回調 Js?
PhoneGap 並無簡單的用 loadUrl 來實現回調,而是在本地層創建了一個 CallBackServer 。由 Js 層不斷向 CallBackServer 請求回調語句 , 而後 eval 執行該回調。
CallBackServer 提供了兩種模式 , 一種是基於 XMLHttpRequst ,一種是基於輪詢。 XHR 的方式即 js 層不斷向 CallBackServer 發送 XMLHttpRequest 請求 , CallBackServer 則將回調語句返回給 js 層。
輪詢方式則是 js 層經過 prompt 向本地發送 poll 請求 , 本地將從 CallBackServer 中拿出下一個回調返回給 js 層。
Js 層相關的 XHR 和輪詢實現請參考 lib/android/plugin/android/callback.js, 以及 lib/android/plugin/android/polling.js
經過閱讀 CallBackServer 的源碼可知,當 url 爲本地路徑時,默認將啓用 XHR 方式。
           3.  如何處理同步回調和異步回調 ?
先說同步處理。從 js prompt WebChromeClient onJSPrompt 是一個跨線程的同步調用。圖示以下



經過 prompt 即可以直接獲得 Plugin 執行的結果。後續作同步回調便也很是簡單了。
接着再說說異步回調是如何實現的。注意在 exec.js 的註釋中 , 做者寫道
 
Java代碼   收藏代碼
  1. The native side can return:  
  2. Synchronous: PluginResult object as a JSON string  
  3. Asynchrounous: Empty string ""  
 
 
爲了區別異步和同步。若 prompt 返回的是空字符串,那麼將認爲是異步調用。此時 PhoneGap 會在 JS 層保留回調函數,待本地層向 CallBackServer 發送回調後進行執行。
也許你會問 , 本地層怎麼區別哪一個回調啊 ?PhoneGap 對此的處理十分簡單,在 cordova.js 中定義了一個 callbackId 的自增種子,並將每一個 callBack 插入 callBacks 中去。不管同步異步,每一個 plugin 調用都將獲得一個流水號碼做爲回調標識。這個回調標識在 prompt 階段便傳遞到了本地層。當本地層的 Plugin 異步結束後,即可以根據該 callbackId 找到回調。並向 CallBackServer 發送回調通知。圖示以下


 
                           
2、 PhoneGap Native 層解析
與本地 Plugin 通訊密切相關的是 :Plugin,PluginManager,PluginResult,CallbackServer
Plugin 是本地層全部插件的抽象基類,全部子插件都必須繼承 Plugin 並實現 Plugin execute 方法。以下代碼是一個極爲簡單的實現本地啓動界面的插件。
 
Java代碼   收藏代碼
  1. package org.apache.cordova;  
  2.    
  3. import org.apache.cordova.api.Plugin;  
  4. import org.apache.cordova.api.PluginResult;  
  5. import org.json.JSONArray;  
  6.    
  7. public class SplashScreen extends Plugin {  
  8.    
  9.     @Override  
  10.     public PluginResult execute(String action, JSONArray args, String callbackId) {  
  11.         PluginResult.Status status = PluginResult.Status.OK;  
  12.         String result = "";  
  13.    
  14.         if (action.equals("hide")) {  
  15.             ((DroidGap)this.ctx).removeSplashScreen();  
  16.         }  
  17.         else {  
  18.             status = PluginResult.Status.INVALID_ACTION;  
  19.         }  
  20.         return new PluginResult(status, result);  
  21.     }  
  22. }  
 
 
上述實例展示了一個典型的 execute 處理流程。首先 , 根據 action 判斷插件須要執行的動做方法,處理後返回一個 PluginResult
 
PluginResult 表示插件執行結果的實體。它主要包含了三個字段,分別是 status: 狀態碼, message,keepCallBack
最基本的 status 狀態碼分別是 OK( 成功 ),NO_RESULT( 沒有結果 ),Error( 失敗 ) ,另外 status 還定義許多失敗的具體異常碼。
message 是返回的結果實體, message 將做爲參數傳入回調函數中。
keepCallBack 表示是否須要保持回調。若是該項爲 false ,那麼在 JS 層在執行回調後將當即刪除回調以釋放資源。
其兩個工具方法 :toSuccessCallBackString toErrorCallbackString 將生成一個 JS 回調語句。配合 CallBackServer 實現了 Native JS 回調。
 
全部的 Plugin 都由 PluginManager 託管。 Js 端調用 Native 代碼時 ,onJSPrompt 會將請求轉發給 PluginManager, PluginManager 便會負責查找並執行 Plugin
 
從上面所說能夠看出, PluginManager 很是重要。首先 , 從最重要的 exec 看起。
 
Java代碼   收藏代碼
  1. public String exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) {  
  2.     PluginResult cr = null;  
  3.     boolean runAsync = async;  
  4.     try {  
  5.         final JSONArray args = new JSONArray(jsonArgs);  
  6.         final IPlugin plugin = this.getPlugin(service);  
  7.         final CordovaInterface ctx = this.ctx;  
  8.         if (plugin != null) {  
  9.             runAsync = async && !plugin.isSynch(action);  
  10.             if (runAsync) {  
  11.                 // Run this on a different thread so that this one can return back to JS  
  12.                 Thread thread = new Thread(new Runnable() {  
  13.                     public void run() {  
  14.                         try {  
  15.                             // Call execute on the plugin so that it can do it's thing  
  16.                             PluginResult cr = plugin.execute(action, args, callbackId);  
  17.                             int status = cr.getStatus();  
  18.   
  19.                             // If no result to be sent and keeping callback, then no need to sent back to JavaScript  
  20.                             if ((status == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {  
  21.                             }  
  22.   
  23.                             // Check the success (OK, NO_RESULT & !KEEP_CALLBACK)  
  24.                             else if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {  
  25.                                 ctx.sendcr.toSuccessCallbackString(callbackId));  < /span>
  26.                             }  
  27.   
  28.                             // If error  
  29.                             else {  
  30.                                 ctx.sendcr.toErrorCallbackString(callbackId));  < /span>
  31.                             }  
  32.                         } catch (Exception e) {  
  33.                             PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());  
  34.                             ctx.sendcr.toErrorCallbackString(callbackId));  < /span>
  35.                         }  
  36.                     }  
  37.                 });  
  38.                 thread.start();  
  39.                 return "";  
  40.             } else {  
  41.                 // Call execute on the plugin so that it can do it's thing  
  42.                 cr = plugin.execute(action, args, callbackId);  
  43.   
  44.                 // If no result to be sent and keeping callback, then no need to sent back to JavaScript  
  45.                 if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {  
  46.                     return "";  
  47.                 }  
  48.             }  
  49.         }  
  50.     } catch (JSONException e) {  
  51.         System.out.println("ERROR: " + e.toString());  
  52.         cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);  
  53.     }  
  54.     // if async we have already returned at this point unless there was an error...  
  55.     if (runAsync) {  
  56.         if (cr == null) {  
  57.             cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);  
  58.         }  
  59.         ctx.sendcr.toErrorCallbackString(callbackId));  < /span>
  60.     }  
  61.     return (cr != null ? cr.getJSONString() : "{ status: 0, message: 'all good' }");  
 
 
exec.js,PluginManager,Plugin 構成了經典的 Command/Action 模式。


exec.js 便對應着玉皇大帝,其面向的是 client, 指望調用的是具體 plugin( 美猴王 ) 的具體方法 ( 上天 ) 。然而 exec.js 只管向 PluginManager( 太白金星 ) 發送指示。 PluginManager( 太白金星 ) 管理全部的 Plugin( 小仙 ) 。它接到通知後,將會根據指示向具體的 Plugin 發出通知。具體的 Plugin( 美猴王 ) 接到通知後,執行動做 (execute) ,並根據 action 來區分具體操做。
因爲 PluginManager 自身對全部的 Plugin 進行了管理,所以其能夠很輕鬆的經過 service 找到對應的 Plugin 。而後想 Plugin 轉發該 action
其中的 asyn 參數比較特殊,其封裝了 Plugin 的異步執行模式。要想 Plugin execute 在線程中執行,必須具有兩個條件。其一是 js 「下旨」給 PluginManager 的時候表示但願異步調用。其二是 Plugin 自身是容許異步執行的。經過查看源代碼,能夠發現 js 端默認都是但願異步調用,所以是否開啓異步模式將由 Plugin isSync 決定。
PluginManager 載入 Plugin 的方式其實很是簡單。主要是經過讀取 plugins.xml 中的配置。配置中的 name service 對應 ,value Plugin 的類路徑對應。 PluginManager 載入 Plugin 是經過反射空構造器實現,所以須要特別注意自定義的 Plugin 不要有帶參構造器。
PluginManager Plugin 的管理還包含廣播生命週期以及廣播消息的功能。其中生命週期方法 onResume,onPause,onDestroy 實際上是和 web 頁面生命週期密切相關的。 ( 而不是 Activity, 注意與 js 層的 onResume,onPause 有很大區別 !) 這點從 DroidGap loadUrlIntoView 中能夠看出。至於廣播消息,則是 Plugin 框架的一個比較有趣的地方。
咱們在 NetWorkManager 插件中看到這樣一段代碼 :
 
Java代碼   收藏代碼
  1. /** 
  2.  * Create a new plugin result and send it back to JavaScript 
  3.  * 
  4.  * @param connection the network info to set as navigator.connection 
  5.  */  
  6. private void sendUpdate(String type) {  
  7.     PluginResult result = new PluginResult(PluginResult.Status.OK, type);  
  8.     result.setKeepCallback(true);  
  9.     this.success(result, this.connectionCallbackId);  
  10.      
  11.     // Send to all plugins  
  12.     this.ctx.postMessage("networkconnection", type);  
 
 
DroidGap 代理了 PluginManager postMessage 方法,此處實際是請求 PluginManager 向全部的 Plugin 廣播網絡切換的事件。若是其餘的 Plugin 關心網絡切換事件 , 只須要覆蓋 onMessage 方法便可。這樣就實現了 Plugin 插件之間的交互。
 
最後一塊硬骨頭是 CallBackServer 。代碼行數其實一點也嚇不倒人,短短 400 行而已。首先從輪詢模式開講,當載入的 url 不是本地頁面時,因爲受到跨域限制,將強制切換成輪詢模式。注意 getJavascript sendJavascript 這兩個方法。
前面說過, CallBackServer 是異步回調的基礎。咱們來看看輪詢下的異步回調到底是怎麼玩兒的。
來看看 BatteryListener 插件 , 下面是它的 execute 方法
注意 action start 時候 PluginResult 的返回。它返回了 NO_Result keepCallback PluginResult exec.js 接到該返回後將保持該回調。在 start 的同時 ,batteryListener 還保存了 callbackId 。那麼 , 當接到 BroadCastReceiver 的通知後 , 怎麼異步回調的呢 ?
  /**
Java代碼   收藏代碼
  1.  * Updates the JavaScript side whenever the battery changes  
  2.  *  
  3.  * @param batteryIntent the current battery information  
  4.  * @return  
  5.  */  
  6. private void updateBatteryInfo(Intent batteryIntent) {     
  7.     sendUpdate(this.getBatteryInfo(batteryIntent), true);  
  8. }  
  9.   
  10. /** 
  11.  * Create a new plugin result and send it back to JavaScript 
  12.  * 
  13.  * @param connection the network info to set as navigator.connection 
  14.  */  
  15. private void sendUpdate(JSONObject info, boolean keepCallback) {  
  16.           if (this.batteryCallbackId != null) {  
  17.                    PluginResult result = new PluginResult(PluginResult.Status.OK, info);  
  18.                    result.setKeepCallback(keepCallback);  
  19.                    this.success(result, this.batteryCallbackId);  
  20.           }  
  21. }  
 
其最終調用了 success 方法。 Success 方法將 PluginResult 包裝成回調語句 , 並經過 DroidGap CallBackServer sendJavaScript
由此爲止,本地層的異步 sendJavaScript 已經完成了。接下來的問題即是 ,JS 層如何 getJavaScript ? lib/android/plugin/android/polling.js , 能夠看到 js 層獲取回調的輪詢實現。
 
Java代碼   收藏代碼
  1.    
  2.    polling = function() {  
  3.       // Exit if shutting down app  
  4.       if (cordova.shuttingDown) {  
  5.           return;  
  6.       }  
  7.    
  8.       // If polling flag was changed, stop using polling from now on and switch to XHR server / callback  
  9.       if (!cordova.UsePolling) {  
  10.           require('cordova/plugin/android/callback')();  
  11.           return;  
  12.       }  
  13.    
  14.       var msg = prompt("""gap_poll:");  
  15.       if (msg) {  
  16.           setTimeout(function() {  
  17.               try {  
  18.                   var t = eval(""+msg);  
  19.               }  
  20.               catch (e) {  
  21.                   console.log("JSCallbackPolling: Message from Server: " + msg);  
  22.                   console.log("JSCallbackPolling Error: "+e);  
  23.               }  
  24.           }, 1);  
  25.           setTimeout(polling, 1);  
  26.       }  
  27.       else {  
  28.           setTimeout(polling, period);  
  29.       }  
  30. };  
 
 
經過 setTimeout 構成了一個死循環,經過 prompt 不斷向本地層請求 gap_poll 。本地層收到 gap_poll 請求後,將會調用 CallBackServer getJavaScript 並同步返回給 polling Polling 接到回調後 , 經過 eval 便完成了 js 端回調代碼的執行。
XHR 的方式與輪詢其實相似 ,js 端的源碼能夠查看 lib/android/plugins/android/callback.js
最後給一張簡單的靜態結構圖



 
CordovaInterface 中包含一些雞肋的 url 白名單以及啓動 Dialog 。雖然寫得很是長 , 但若是瞭解整套 Plugin 機制的話,看下來仍是小 case 的,這裏就不贅述了。
3、 PhoneGap js 層源碼
PhoneGap js 層源碼的模塊化機制和啓動仍是挺有趣的。下次碼好字了傳給你們看 J
相關文章
相關標籤/搜索