標籤:android blog http io ar os 使用 java sp
PhoneGap,著名的跨平台Hybrid架構,旨在讓開發人員使用HTML、Javascript、CSS開發跨平台的App。
最近的工作,就是做Hybrid方面的,很自然,方案就從PhoneGap入手。
下面就切入正題,分析下PhoneGap的原理,需要說明的是,我只針對iOS版本的PhoneGap做分析,android版本的原理大同小異。
安裝PhoneGap
現在使用PhoneGap非常方便,只需要安裝node,用簡單的命令就能完成安裝和使用的工作。
安裝PhoneGap:
1 |
sudo npm install -g phonegap
|
建立phoneGap應用:
123 |
phonegap create my-appcd my-appphonegap run ios
|
具體可看phonegap官網進行學習。
PhoneGap與Cordova的關係
Cordova是PhoneGap貢獻給Apache後的開源項目,是從PhoneGap中抽離出的核心代碼,是驅動PhoneGap的核心引擎。有點類似Webkit和Google Chrome的關係。
淵源就是:早在2011年10月,Adobe收購了Nitobi Software和它的PhoneGap產品,然後宣布這個移動Web開發架構將會繼續開源,並把它提交到Apache Incubator,以便完全接受ASF的管治。當然,由於Adobe擁有了PhoneGap商標,所以開源組織的這個PhoneGap v2.0版產品就更名為Apache Cordova。
為什麼說這個?因為下面的文章中,會出現Cordova這個命令,大家不要覺得奇怪。
js與native通訊的原理
但在切入正題前,需要先瞭解下iOS js與native通訊的原理。瞭解這個原理,是理解PhoneGap代碼的關鍵。
具體可以看我之前寫的iOS Js與native相互連信,這裡做簡單說明。
js –> native
在iOS中,js調用native並沒有提供原生的實現,只能通過UIWebView相關的UIWebViewDelegate協議的
1 |
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
|
方法來做攔截,並在這個方法中,根據url的協議或特徵字串來做調用方法或觸發事件等工作,如
12345678910111213141516171819 |
/** 方法的傳回值是BOOL值。* 返回YES:表示讓瀏覽器執行預設操作,比如某個a連結跳轉* 返回NO:表示不執行瀏覽器的預設操作,這裡因為通過url協議來判斷js執行native的操作,肯定不是瀏覽器預設操作,故返回NO* /- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { NSURL *url = [request URL]; if ([[url scheme] isEqualToString:@"callFunction") { //調用原生方法 return NO; } else if (([[url scheme] isEqualToString:@"sendEvent") { //觸發事件 return NO; } else { return YES; }}
|
值得注意的是,通過這個方式,js調用native是非同步的。
native –> js
native調用js非常簡潔方便,只需要
1 |
[webView stringByEvaluatingJavaScriptFromString:@"alert(‘hello world!‘)"];
|
並且該方法是同步的。
native調用js非常簡單直接,所以PhoneGap解決的主要是js調用native的問題。
PhoneGap js –> native
我們通過一個js調用native的Dialog的例子做說明。
Dialog是一個PhoneGap的外掛程式,可以看dialog 外掛程式文檔,學習下載並使用該外掛程式。
123 |
這裡有個很重要的事需要說明一下:目前PhoneGap的文檔更新非常不及時,特別是外掛程式的使用方面,比如Dialog外掛程式的使用,文檔中寫的是使用navigator.notification.alert,但是經過我的摸索,因為現在PhoneGap使用AMD的方式來管理外掛程式,所以應該是使用cordova.require("cordova/plugin/notification").alert的方式來調用。外掛程式的合并方面,也有很多坑,主要是文檔不全 - -|||
|
js部分
在html上添加一個button,然後通過下列代碼調用:
123456789101112 |
function alertDismissed() { // do something}function showAlert() { cordova.require("cordova/plugin/notification").alert( ‘You are the winner!‘, // message alertDismissed, // callback ‘Game Over‘, // title ‘Done‘ // buttonName );}
|
再看下對應的cordova/plugin/notification的代碼:
123456789101112131415161718192021 |
var exec = cordova.require(‘cordova/exec‘);var platform = cordova.require(‘cordova/platform‘);module.exports = { /** * Open a native alert dialog, with a customizable title and button text. * * @param {String} message Message to print in the body of the alert * @param {Function} completeCallback The callback that is called when user clicks on a button. * @param {String} title Title of the alert dialog (default: Alert) * @param {String} buttonLabel Label of the close button (default: OK) */ alert: function(message, completeCallback, title, buttonLabel) { var _title = (title || "Alert"); var _buttonLabel = (buttonLabel || "OK"); exec(completeCallback, null, "Notification", "alert", [message, _title, _buttonLabel]); }}....
|
可以看到alert最終其實是調用了exec方法來調用native代碼的,exec方法非常關鍵,是PhoneGap js調用native的核心代碼。
然後在源碼中搜尋exec對應的cordova/exec,查看exec方法的源碼。
因為對應的cordova/exec源碼非常長,我只能截取最關鍵的代碼並做說明:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253 |
define("cordova/exec", function(require, exports, module) { ... function iOSExec() { ... var successCallback, failCallback, service, action, actionArgs, splitCommand; var callbackId = null; ... // 格式化傳入參數 successCallback = arguments[0]; //成功的回呼函數 failCallback = arguments[1]; //失敗的回呼函數 service = arguments[2]; //表示調用native類的類名 action = arguments[3]; //表示調用native類的一個方法 actionArgs = arguments[4]; //參數 //預設callbackId為‘INVALID‘,表示不需要回調 callbackId = ‘INVALID‘; ... //如果傳入參數有successCallback或failCallback,說明需要回調,就設定callbackId,並儲存對應的回呼函數 if (successCallback || failCallback) { callbackId = service + cordova.callbackId++; cordova.callbacks[callbackId] = {success:successCallback, fail:failCallback}; } //格式化傳入的service、action、actionArgs,並儲存,準備native代碼來調用 actionArgs = massageArgsJsToNative(actionArgs); var command = [callbackId, service, action, actionArgs]; commandQueue.push(JSON.stringify(command)); ... //通過建立一個iframe並設定src,給native代碼一個指令,開始執行js調用native的過程 execIframe = execIframe || createExecIframe(); if (!execIframe.contentWindow) { execIframe = createExecIframe(); } execIframe.src = "gap://ready"; ... } module.exports = iOSExec;});
|
為了調用native方法,exec方法做了大量初始化的工作,這麼做的原因,還是因為iOS沒有提供直接的方法來執行js調用native,不能把參數直接傳遞給native,所以只能通過js端儲存對應操作的所有參數,然後通過指令來讓native代碼來回調的方式間接完成。
native部分
之後,就走到了native代碼的部分。
CDVViewController
前面js通過建立一個iframe並發送gap://ready這個指令來告訴native開始執行操作。native中對應的操作在CDVViewController.m檔案中的webView:shouldStartLoadWithRequest:navigationType:方法:
12345678910111213141516171819 |
- (BOOL)webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType{ NSURL* url = [request URL]; /* * 判斷url的協議以"gap"開頭 * 執行在js端調用cordova.exec()的command隊列 * 註:這裡的command表示js調用native */ if ([[url scheme] isElaqualToString:@"gap"]) { //_commandQueue即CDVCommandQueue類 //從js端拉取command,即儲存在js端commandQueue數組中的資料 [_commandQueue fetchCommandsFromJs]; //開始執行command [_commandQueue executePending]; return NO; }...}
|
到這裡,其實已經走完js調用native的主要過程了。
之後,讓我們再看下CDVCommandQueue中的fetchCommandsFromJs方法與executePending方法中做的事。
CDVCommandQueue
1234567 |
- (void)fetchCommandsFromJs{ // 擷取js端儲存的command,並在native暫存 NSString* queuedCommandsJSON = [_viewController.webView stringByEvaluatingJavaScriptFromString: @"cordova.require(‘cordova/exec‘).nativeFetchMessages()"]; [self enqueueCommandBatch:queuedCommandsJSON];}
|
fetchCommandsFromJs方法非常簡單,不細說了。
executePending方法稍微複雜些,因為js是單線程的,而iOS是典型的多線程,所以executePending方法做的工作主要是讓command一個一個執行,防止線程問題。
executePending方法其實與之後的execute方法緊密相連,這裡一起列出,只保留關鍵代碼:
1234567891011121314151617181920212223242526272829303132333435 |
- (void)executePending{ ... //_queue即command隊列,依次執行 while ([_queue count] > 0) { ... //取出從js中擷取的command字串,解析為native端的CDVInvokedUrlCommand類 CDVInvokedUrlCommand* command = [CDVInvokedUrlCommand commandFromJson:jsonEntry]; ... //執行command [self execute:command]) ... }}- (BOOL)execute:(CDVInvokedUrlCommand*)command{ ... BOOL retVal = YES; //擷取plugin對應的執行個體 CDVPlugin* obj = [_viewController.commandDelegate getCommandInstance:command.className]; //調用plugin執行個體的方法名 NSString* methodName = [NSString stringWithFormat:@"%@:", command.methodName]; SEL normalSelector = NSSelectorFromString(methodName); if ([obj respondsToSelector:normalSelector]) { //訊息發送,執行plugin執行個體對應的方法,並傳遞參數 objc_msgSend(obj, normalSelector, command); } else { // There‘s no method to call, so throw an error. NSLog(@"ERROR: Method ‘%@‘ not defined in Plugin ‘%@‘", methodName, command.className); retVal = NO; } ... return retVal;}
|
可以看到js調用native plugin最終執行的是objc_msgSend(obj, normalSelector, command);這塊代碼,這裡我們再拿js端的代碼來進行理解。
之前js中的showAlert方法中我們書寫了 exec(completeCallback, null, "Notification", "alert", [message, _title, _buttonLabel]);
故,這裡的對應關係:
- obj:“Notification”
- normalSelector:“alert”
- command:[message, title, buttonLabel]
CDVNotification
“Notification”真正對應的iOS類是CDVNotification。js端調用的外掛程式名字”Notification”與真正的native類名並非完全對應,因為native因為平台的不同,有不同的命名規範。
看下CDVNotification的代碼:
123456789 |
- (void)alert:(CDVInvokedUrlCommand*)command{ NSString* callbackId = command.callbackId; NSString* message = [command argumentAtIndex:0]; NSString* title = [command argumentAtIndex:1]; NSString* buttons = [command argumentAtIndex:2]; [self showDialogWithMessage:message title:title buttons:@[buttons] defaultText:nil callbackId:callbackId dialogType:DIALOG_TYPE_ALERT];}
|
前面用objc_msgSend(obj, normalSelector, command);做訊息發送,執行的便是這塊代碼,代碼很好理解,就是對command再做解析,並顯示。
最終效果:
點擊”Done”,native會再回調執行js端的成功回調,這裡對應的就是js裡設定的alertDismissed方法。
到此為止,我們已經走完從js端調用native alert的全部過程了。
列下過程的核心代碼:
- js部分:
- cordova.js中的iOSExec()方法,指定js調用native的初始化工作,並發送開始執行的指令
- native部分:
- CDVViewController:攔截js調用native的url協議,執行調用
- CDVCommandQueue:執行js調用native的隊列,調用對應的plugin
時序圖
以上Dialog例子中,PhoneGap js調用native的時序圖:
結語
PhoneGap還是很給力的,能做到主流平台全相容著實不容易。
iOS端因為沒有提供js調用native的直接方法,做的處理也算合理到位。
特別是外掛程式化的支援做的很好,但是文檔著實不夠給力。
PhoneGap原理分析