晚上被朋友拉去看了海上煙花,煙花易冷,熱情難卻。散場時候人太多,車都打不着,回來都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類的構造函數:

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();
那在初始化中到底完成了哪些工作?看一下代碼就很好理解了。

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()啟動插件。
下面我們來看看如何加載插件的。

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時(目前還沒有這種情況)的插件創建工作。

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 }
好了,插件初始化完畢,萬事具備只欠東風了。這里的“東風”是什么?當然是插件的執行了。

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類的構造函數:

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隊列。
然后再看一下初始化方法

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() 方法的具體內容簡介如下:

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,一個是對隊列的維護。