傳送門:
討論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
的實現方式並不相同。
- var r = prompt(JSON.stringify(args), "gap:"+JSON.stringify([service, action, callbackId, true]));
-
這句
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
的註釋中
,
做者寫道
- The native side can return:
- Synchronous: PluginResult object as a JSON string
- 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
方法。以下代碼是一個極爲簡單的實現本地啓動界面的插件。
- package org.apache.cordova;
-
- import org.apache.cordova.api.Plugin;
- import org.apache.cordova.api.PluginResult;
- import org.json.JSONArray;
-
- public class SplashScreen extends Plugin {
-
- @Override
- public PluginResult execute(String action, JSONArray args, String callbackId) {
- PluginResult.Status status = PluginResult.Status.OK;
- String result = "";
-
- if (action.equals("hide")) {
- ((DroidGap)this.ctx).removeSplashScreen();
- }
- else {
- status = PluginResult.Status.INVALID_ACTION;
- }
- return new PluginResult(status, result);
- }
- }
上述實例展示了一個典型的
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
看起。
- public String exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) {
- PluginResult cr = null;
- boolean runAsync = async;
- try {
- final JSONArray args = new JSONArray(jsonArgs);
- final IPlugin plugin = this.getPlugin(service);
- final CordovaInterface ctx = this.ctx;
- if (plugin != null) {
- runAsync = async && !plugin.isSynch(action);
- if (runAsync) {
-
- Thread thread = new Thread(new Runnable() {
- public void run() {
- try {
-
- PluginResult cr = plugin.execute(action, args, callbackId);
- int status = cr.getStatus();
-
-
- if ((status == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
- }
-
-
- else if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
- ctx.sendcr.toSuccessCallbackString(callbackId)); < /span>
- }
-
-
- else {
- ctx.sendcr.toErrorCallbackString(callbackId)); < /span>
- }
- } catch (Exception e) {
- PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
- ctx.sendcr.toErrorCallbackString(callbackId)); < /span>
- }
- }
- });
- thread.start();
- return "";
- } else {
-
- cr = plugin.execute(action, args, callbackId);
-
-
- if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
- return "";
- }
- }
- }
- } catch (JSONException e) {
- System.out.println("ERROR: " + e.toString());
- cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
- }
-
- if (runAsync) {
- if (cr == null) {
- cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);
- }
- ctx.sendcr.toErrorCallbackString(callbackId)); < /span>
- }
- 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
插件中看到這樣一段代碼
:
-
-
-
-
-
- private void sendUpdate(String type) {
- PluginResult result = new PluginResult(PluginResult.Status.OK, type);
- result.setKeepCallback(true);
- this.success(result, this.connectionCallbackId);
-
-
- 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
的通知後
,
怎麼異步回調的呢
?
/**
- * Updates the JavaScript side whenever the battery changes
- *
- * @param batteryIntent the current battery information
- * @return
- */
- private void updateBatteryInfo(Intent batteryIntent) {
- sendUpdate(this.getBatteryInfo(batteryIntent), true);
- }
-
-
-
-
-
-
- private void sendUpdate(JSONObject info, boolean keepCallback) {
- if (this.batteryCallbackId != null) {
- PluginResult result = new PluginResult(PluginResult.Status.OK, info);
- result.setKeepCallback(keepCallback);
- this.success(result, this.batteryCallbackId);
- }
- }
其最終調用了
success
方法。
Success
方法將
PluginResult
包裝成回調語句
,
並經過
DroidGap
向
CallBackServer sendJavaScript
。
由此爲止,本地層的異步
sendJavaScript
已經完成了。接下來的問題即是
,JS
層如何
getJavaScript
呢
?
在
lib/android/plugin/android/polling.js
中
,
能夠看到
js
層獲取回調的輪詢實現。
-
- polling = function() {
-
- if (cordova.shuttingDown) {
- return;
- }
-
-
- if (!cordova.UsePolling) {
- require('cordova/plugin/android/callback')();
- return;
- }
-
- var msg = prompt("", "gap_poll:");
- if (msg) {
- setTimeout(function() {
- try {
- var t = eval(""+msg);
- }
- catch (e) {
- console.log("JSCallbackPolling: Message from Server: " + msg);
- console.log("JSCallbackPolling Error: "+e);
- }
- }, 1);
- setTimeout(polling, 1);
- }
- else {
- setTimeout(polling, period);
- }
- };
經過
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