跨平台PhoneGap技術架構四:PhoneGap底層原理(下)


晚上被朋友拉去看了海上煙花,煙花易冷,熱情難卻。散場時候人太多,車都打不着,回來都23點了,差點把寫博客計划給耽擱了。

廢話不多說,上篇博客講到了PhoneGap的核心組件WebView,如果園友有覺得講得不夠清楚的地方,歡迎提出您的意見。

本篇主要講解PhoneGap的兩個重要的類,PluginManager類和CallbackServer類。

PluginManager類

PluginManager類實現了PhoneGap的插件管理機制,並在CordovaWebView提供出JavaScript接口。在CordovaChromeClient類的onJsPrompt()方法中截獲JavaScript消息,根據消息具體執行某一個插件。代碼如下,

String r = this.appView.pluginManager.exec(service, action, callbackId, message, async);

在上篇博客講解CordovaWebView類時曾提到,PluginManager的實例化是在CordovaWebView的初始化init()函數調用setup()方法時執行的。

1         //Start up the plugin manager
2         try {
3             this.pluginManager = new PluginManager(this, this.cordova);
4         } catch (Exception e) {
5             // TODO Auto-generated catch block
6             e.printStackTrace();
7         }

我們首先看一下PluginManager類的構造函數:

View Code
 1     /**
 2      * Constructor.
 3      *
 4      * @param app
 5      * @param ctx
 6      */
 7     public PluginManager(CordovaWebView app, CordovaInterface ctx) {
 8         this.ctx = ctx;
 9         this.app = app;
10         this.firstRun = true;
11     }

傳入參數有兩個CordovaWebView類型的app和CordovaInterface類型的ctx。這兩個參數比較重要,PluginManager是和CordovaWebView綁定的,要求必須是CordovaWebView類型,第二個參數必須是實現了CordovaInterface的Context。為什么是必須呢?這里留下一個懸念,如果你只做PhoneGap層的應用,可以不關心這個問題。初始化firstRun為true,這個Flag用來判斷初始化時是否需要加載插件。

並在CordovaWebView的loadUrlIntoView()方法中進行了初始化工作。

this.pluginManager.init();

那在初始化中到底完成了哪些工作?看一下代碼就很好理解了。

init
 1     /**
 2      * Init when loading a new HTML page into webview.
 3      */
 4     public void init() {
 5         LOG.d(TAG, "init()");
 6 
 7         // If first time, then load plugins from plugins.xml file
 8         if (this.firstRun) {
 9             this.loadPlugins();
10             this.firstRun = false;
11         }
12 
13         // Stop plugins on current HTML page and discard plugin objects
14         else {
15             this.onPause(false);
16             this.onDestroy();
17             this.clearPluginObjects();
18         }
19 
20         // Start up all plugins that have onload specified
21         this.startupPlugins();
22     }

首先判斷firstRun是否為true,如果為true,則執行this.loadPlugins()加載插件,並把firstRun置為false。否則停止plugin工作並銷毀對象。

最后執行this.startupPlugins()啟動插件。

下面我們來看看如何加載插件的。

loadPlugins
 1     /**
 2      * Load plugins from res/xml/plugins.xml
 3      */
 4     public void loadPlugins() {
 5         int id = this.ctx.getActivity().getResources().getIdentifier("plugins", "xml", this.ctx.getActivity().getPackageName());
 6         if (id == 0) {
 7             this.pluginConfigurationMissing();
 8         }
 9         XmlResourceParser xml = this.ctx.getActivity().getResources().getXml(id);
10         int eventType = -1;
11         String service = "", pluginClass = "";
12         boolean onload = false;
13         PluginEntry entry = null;
14         while (eventType != XmlResourceParser.END_DOCUMENT) {
15             if (eventType == XmlResourceParser.START_TAG) {
16                 String strNode = xml.getName();
17                 if (strNode.equals("plugin")) {
18                     service = xml.getAttributeValue(null, "name");
19                     pluginClass = xml.getAttributeValue(null, "value");
20                     // System.out.println("Plugin: "+name+" => "+value);
21                     onload = "true".equals(xml.getAttributeValue(null, "onload"));
22                     entry = new PluginEntry(service, pluginClass, onload);
23                     this.addService(entry);
24                 } else if (strNode.equals("url-filter")) {
25                     this.urlMap.put(xml.getAttributeValue(null, "value"), service);
26                 }
27             }
28             try {
29                 eventType = xml.next();
30             } catch (XmlPullParserException e) {
31                 e.printStackTrace();
32             } catch (IOException e) {
33                 e.printStackTrace();
34             }
35         }
36     }

 PluginManager類加載插件是通過讀取res/xml/plugins.xml文件實現的。首先取得xml文件id,然后通過id取得xml文件的內容。接下來是解析xml文件,每次都會讀取出插件Plugin的service(如Camera、Notification等)、pluginClass(如org.apache.cordova.CameraLauncher、org.apache.cordova.Notification等)和onload(判斷插件初始化時是否需要創建插件的flag)。最后通過addService將讀取到的插件添加到PluginEntry的list中去。

在插件實體類PluginEntry中有一個屬性onload可能讓人有些迷惑,在plugins.xml中並沒有關於onload的節點,也就是說在解析xml文件時

onload = "true".equals(xml.getAttributeValue(null, "onload"));

這里的onload是被賦值為false的。個人猜測是為以后的插件擴展預留的接口,在初始化時需要先創建這個插件。

在startupPlugins()函數中,主要是做了onload被設置成true時(目前還沒有這種情況)的插件創建工作。

startupPlugins
 1     /**
 2      * Create plugins objects that have onload set.
 3      */
 4     public void startupPlugins() {
 5         for (PluginEntry entry : this.entries.values()) {
 6             if (entry.onload) {
 7                 entry.createPlugin(this.app, this.ctx);
 8             }
 9         }
10     }

 

好了,插件初始化完畢,萬事具備只欠東風了。這里的“東風”是什么?當然是插件的執行了。

exec
 1     /**
 2      * Receives a request for execution and fulfills it by finding the appropriate
 3      * Java class and calling it's execute method.
 4      *
 5      * PluginManager.exec can be used either synchronously or async. In either case, a JSON encoded
 6      * string is returned that will indicate if any errors have occurred when trying to find
 7      * or execute the class denoted by the clazz argument.
 8      *
 9      * @param service       String containing the service to run
10      * @param action        String containt the action that the class is supposed to perform. This is
11      *                      passed to the plugin execute method and it is up to the plugin developer
12      *                      how to deal with it.
13      * @param callbackId    String containing the id of the callback that is execute in JavaScript if
14      *                      this is an async plugin call.
15      * @param args          An Array literal string containing any arguments needed in the
16      *                      plugin execute method.
17      * @param async         Boolean indicating whether the calling JavaScript code is expecting an
18      *                      immediate return value. If true, either Cordova.callbackSuccess(...) or
19      *                      Cordova.callbackError(...) is called once the plugin code has executed.
20      *
21      * @return              JSON encoded string with a response message and status.
22      */
23     public String exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) {
24         PluginResult cr = null;
25         boolean runAsync = async;
26         try {
27             final JSONArray args = new JSONArray(jsonArgs);
28             final IPlugin plugin = this.getPlugin(service);
29             //final CordovaInterface ctx = this.ctx;
30             if (plugin != null) {
31                 runAsync = async && !plugin.isSynch(action);
32                 if (runAsync) {
33                     // Run this on a different thread so that this one can return back to JS
34                     Thread thread = new Thread(new Runnable() {
35                         public void run() {
36                             try {
37                                 // Call execute on the plugin so that it can do it's thing
38                                 PluginResult cr = plugin.execute(action, args, callbackId);
39                                 int status = cr.getStatus();
40 
41                                 // If no result to be sent and keeping callback, then no need to sent back to JavaScript
42                                 if ((status == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
43                                 }
44 
45                                 // Check the success (OK, NO_RESULT & !KEEP_CALLBACK)
46                                 else if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
47                                     app.sendJavascript(cr.toSuccessCallbackString(callbackId));
48                                 }
49 
50                                 // If error
51                                 else {
52                                     app.sendJavascript(cr.toErrorCallbackString(callbackId));
53                                 }
54                             } catch (Exception e) {
55                                 PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
56                                 app.sendJavascript(cr.toErrorCallbackString(callbackId));
57                             }
58                         }
59                     });
60                     thread.start();
61                     return "";
62                 } else {
63                     // Call execute on the plugin so that it can do it's thing
64                     cr = plugin.execute(action, args, callbackId);
65 
66                     // If no result to be sent and keeping callback, then no need to sent back to JavaScript
67                     if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
68                         return "";
69                     }
70                 }
71             }
72         } catch (JSONException e) {
73             System.out.println("ERROR: " + e.toString());
74             cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
75         }
76         // if async we have already returned at this point unless there was an error...
77         if (runAsync) {
78             if (cr == null) {
79                 cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);
80             }
81             app.sendJavascript(cr.toErrorCallbackString(callbackId));
82         }
83         return (cr != null ? cr.getJSONString() : "{ status: 0, message: 'all good' }");
84     }

本篇開始已經提到過,插件的執行是在CordovaChromeClient的onJsPrompt()方法中截獲JavaScript消息,根據消息具體執行某一個插件來調用的。幾個傳入參數再說明一下

service:需要執行的某一項服務,如Camera、Notification、Battery等等;

action:服務執行的具體動作,如takePhone、getPicture等

callbackId:如果插件是異步執行的話,那么插件執行完成之后回調執行的JavaScript代碼的Id存放在callbackId中(好長的定語⊙﹏⊙b汗)。

jsonArgs:即上篇提到過的message消息,存放着服務的調用信息。

async:布爾變量,標明JavaScript端是否需要執行回調。如果是true,則Cordova.callbackSuccess(...)或者Cordova.callbackError(...)需要被執行。

    實際這個值傳入時默認都是true的,插件是否是異步執行是通過plugin.isSynch(action)來判斷的。估計開始時想讓開發者指定插件是否要異步執行,后來添加了plugin.isSynch()的方法來判斷。

首先判斷插件是否是異步執行,如果是ture的話,新起一個線程執行。

PluginResult cr = plugin.execute(action, args, callbackId);

通過PluginResult類型返回值cr的取得status和 cr.getKeepCallback()是否執行app.sendJavascript()方法。

如果插件不是異步執行,則直接調用cr = plugin.execute(action, args, callbackId)方法執行插件服務。

CallbackServer類

如果說前面所講的幾個類都是為后台JAVA端服務的話,那么CallbackServer類就是為前台JavaScript服務的了。

 下面我們看看CallbackServer類官方文檔的解釋:

CallbackServer 實現了 Runnable 接口,具體的功能就是維護一個數據的隊列,並且建立一個服務器,用於 XHR 的數據傳遞,對數據的隊列的維護利用的是 LinkedList<String>。

XHR處理流程:

1. JavaScript 發起一個異步的XHR 請求.

2. 服務器保持該鏈接打開直到有可用的數據。

3. 服務器把數據寫入客戶端並關閉鏈接。

4. 服務器立即開始監聽下一個XHR請求。

5. 客戶端接收到XHR回復,並處理該回復。

6. 客戶端發送新的異步XHR 請求。


如果說手持設備設置了代理,那么XHR是不可用的,這時候需要使用Pollibng輪詢模式。
該使用哪種模式,可通過 CallbackServer.usePolling()獲取。


Polling調用流程:

1.客戶端調用CallbackServer.getJavascript()來獲取要執行的Javascript 語句。
2. 如果有需要執行的JS語句,那么客戶端就會執行它。
3. 客戶端在循環中執行步驟1.

前面已經講到過,在CordovaWebViewClient類的onPageStarted方法中實現了CallbackServer的實例化和初始化。

按照慣例,首先來看一下CallbalServer類的構造函數:

CallbackServer
 1     /**
 2      * Constructor.
 3      */
 4     public CallbackServer() {
 5         //Log.d(LOG_TAG, "CallbackServer()");
 6         this.active = false;
 7         this.empty = true;
 8         this.port = 0;
 9         this.javascript = new LinkedList<String>();
10     }

構造函數比較簡單,初始化的時候設置活動狀態this.active = false;JavaScript隊列為空this.empty = true;端口初始化為0 this.port = 0;新建一個LinkedList<String>類型的javascript隊列。

然后再看一下初始化方法

init
 1    /**
 2      * Init callback server and start XHR if running local app.
 3     *
 4      * If Cordova app is loaded from file://, then we can use XHR
 5      * otherwise we have to use polling due to cross-domain security restrictions.
 6      *
 7      * @param url            The URL of the Cordova app being loaded
 8      */
 9     public void init(String url) {
10         //System.out.println("CallbackServer.start("+url+")");
11         this.active = false;
12         this.empty = true;
13         this.port = 0;
14         this.javascript = new LinkedList<String>();
15 
16         // Determine if XHR or polling is to be used
17         if ((url != null) && !url.startsWith("file://")) {
18             this.usePolling = true;
19             this.stopServer();
20         }
21         else if (android.net.Proxy.getDefaultHost() != null) {
22             this.usePolling = true;
23             this.stopServer();
24         }
25         else {
26             this.usePolling = false;
27             this.startServer();
28         }
29     }

CallbalServer類支持兩種執行模式,一種是Polling,另為一種是XHR(xmlHttpRequest)。在初始化init方法中先判斷使用哪種模式。如果是打開的本地文件或者設置設置了代理,則使用polling的模式,否則,使用XHR模式,並啟動Server。

由於CallbalServer類實現的是 Runnable 接口,在 CallbackServer 中,最主要的方法就是 run() 方法,run() 方法的具體內容簡介如下:

run
 1     /**
 2      * Start running the server.  
 3      * This is called automatically when the server thread is started.
 4      */
 5     public void run() {
 6 
 7         // Start server
 8         try {
 9             this.active = true;
10             String request;
11             ServerSocket waitSocket = new ServerSocket(0);
12             this.port = waitSocket.getLocalPort();
13             //Log.d(LOG_TAG, "CallbackServer -- using port " +this.port);
14             this.token = java.util.UUID.randomUUID().toString();
15             //Log.d(LOG_TAG, "CallbackServer -- using token "+this.token);
16 
17             while (this.active) {
18                 //Log.d(LOG_TAG, "CallbackServer: Waiting for data on socket");
19                 Socket connection = waitSocket.accept();
20                 BufferedReader xhrReader = new BufferedReader(new InputStreamReader(connection.getInputStream()), 40);
21                 DataOutputStream output = new DataOutputStream(connection.getOutputStream());
22                 request = xhrReader.readLine();
23                 String response = "";
24                 //Log.d(LOG_TAG, "CallbackServerRequest="+request);
25                 if (this.active && (request != null)) {
26                     if (request.contains("GET")) {
27 
28                         // Get requested file
29                         String[] requestParts = request.split(" ");
30 
31                         // Must have security token
32                         if ((requestParts.length == 3) && (requestParts[1].substring(1).equals(this.token))) {
33                             //Log.d(LOG_TAG, "CallbackServer -- Processing GET request");
34 
35                             // Wait until there is some data to send, or send empty data every 10 sec 
36                             // to prevent XHR timeout on the client 
37                             synchronized (this) {
38                                 while (this.empty) {
39                                     try {
40                                         this.wait(10000); // prevent timeout from happening
41                                         //Log.d(LOG_TAG, "CallbackServer>>> break <<<");
42                                         break;
43                                     } catch (Exception e) {
44                                     }
45                                 }
46                             }
47 
48                             // If server is still running
49                             if (this.active) {
50 
51                                 // If no data, then send 404 back to client before it times out
52                                 if (this.empty) {
53                                     //Log.d(LOG_TAG, "CallbackServer -- sending data 0");
54                                     response = "HTTP/1.1 404 NO DATA\r\n\r\n "; // need to send content otherwise some Android devices fail, so send space
55                                 }
56                                 else {
57                                     //Log.d(LOG_TAG, "CallbackServer -- sending item");
58                                     response = "HTTP/1.1 200 OK\r\n\r\n";
59                                     String js = this.getJavascript();
60                                     if (js != null) {
61                                         response += encode(js, "UTF-8");
62                                     }
63                                 }
64                             }
65                             else {
66                                 response = "HTTP/1.1 503 Service Unavailable\r\n\r\n ";
67                             }
68                         }
69                         else {
70                             response = "HTTP/1.1 403 Forbidden\r\n\r\n ";
71                         }
72                     }
73                     else {
74                         response = "HTTP/1.1 400 Bad Request\r\n\r\n ";
75                     }
76                     //Log.d(LOG_TAG, "CallbackServer: response="+response);
77                     //Log.d(LOG_TAG, "CallbackServer: closing output");
78                     output.writeBytes(response);
79                     output.flush();
80                 }
81                 output.close();
82                 xhrReader.close();
83             }
84         } catch (IOException e) {
85             e.printStackTrace();
86         }
87         this.active = false;
88         //Log.d(LOG_TAG, "CallbackServer.startServer() - EXIT");
89     }

 

首先利用 ServerSocket 監聽端口,具體端口則自由分配。
在 accept 后則是對 HTTP 協議的解析,和對應的返回 status code。
在驗證正確后,利用 getJavascript 方法得到維護的 LinkedList<String>() 中的保存的 js 代碼,如果為空則返回 null。
這些具體的 string 類型的 js 代碼則利用 socket 作為 response 返回給前端。
之后就是對隊列維護的方法,這時理解之前的 sendJavaScript 則很簡單,該方法與 getJavaScript 相反,一個是從 LinkedList 中取出 js 代碼,一個則是加入。
綜上,CallbackServer 實現的是兩個功能,一個是 XHR 的 SocketServer,一個是對隊列的維護。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM