PhoneGap,著名的跨平臺Hybrid框架,旨在讓開發者使用HTML、Javascript、CSS開發跨平臺的App。html
最近的工做,就是作Hybrid方面的,很天然,方案就從PhoneGap入手。node
下面就切入正題,分析下PhoneGap的原理,須要說明的是,我只針對iOS版本的PhoneGap作分析,android版本的原理大同小異。android
如今使用PhoneGap很是方便,只須要安裝node,用簡單的命令就能完成安裝和使用的工做。ios
安裝PhoneGap:git
1 |
sudo npm install -g phonegap |
建立phoneGap應用:github
1 2 3 |
phonegap create my-app cd my-app phonegap run ios |
具體可看phonegap官網進行學習。web
Cordova是PhoneGap貢獻給Apache後的開源項目,是從PhoneGap中抽離出的核心代碼,是驅動PhoneGap的核心引擎。有點相似Webkit和Google Chrome的關係。apache
淵源就是:早在2011年10月,Adobe收購了Nitobi Software和它的PhoneGap產品,而後宣佈這個移動Web開發框架將會繼續開源,並把它提交到Apache Incubator,以便徹底接受ASF的管治。固然,因爲Adobe擁有了PhoneGap商標,因此開源組織的這個PhoneGap v2.0版產品就改名爲Apache Cordova。npm
爲何說這個?由於下面的文章中,會出現Cordova
這個命令,你們不要以爲奇怪。json
但在切入正題前,須要先了解下iOS js與native通訊的原理。瞭解這個原理,是理解PhoneGap代碼的關鍵
。
具體能夠看我以前寫的iOS Js與native相互通訊,這裏作簡單說明。
在iOS中,js調用native並無提供原生的實現,只能經過UIWebView相關的UIWebViewDelegate協議的
1 |
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType |
方法來作攔截,並在這個方法中,根據url的協議或特徵字符串來作調用方法或觸發事件等工做,如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/* * 方法的返回值是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很是簡潔方便,只須要
1 |
[webView stringByEvaluatingJavaScriptFromString:@"alert('hello world!')"]; |
而且該方法是同步
的。
native調用js很是簡單直接,因此PhoneGap解決的主要是js調用native的問題。
咱們經過一個js調用native的Dialog的例子作說明。
Dialog是一個PhoneGap的插件,能夠看dialog 插件文檔,學習下載並使用該插件。
1 2 3 |
這裏有個很重要的事須要說明一下: 目前PhoneGap的文檔更新很是不及時,特別是插件的使用方面,好比Dialog插件的使用,文檔中寫的是使用navigator.notification.alert,可是通過個人摸索,由於如今PhoneGap使用AMD的方式來管理插件,因此應該是使用cordova.require("cordova/plugin/notification").alert的方式來調用。 插件的合併方面,也有不少坑,主要是文檔不全 - -||| |
在html上添加一個button,而後經過下列代碼調用:
1 2 3 4 5 6 7 8 9 10 11 12 |
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
的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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
源碼很是長,我只能截取最關鍵的代碼並作說明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
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代碼的部分。
前面js經過建立一個iframe併發送gap://ready
這個指令來告訴native開始執行操做。native中對應的操做在CDVViewController.m
文件中的webView:shouldStartLoadWithRequest:navigationType:
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (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方法中作的事。
1 2 3 4 5 6 7 |
- (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方法緊密相連,這裏一塊兒列出,只保留關鍵代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
- (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]);
故,這裏的對應關係:
「Notification」真正對應的iOS類是CDVNotification。js端調用的插件名字」Notification」與真正的native類名並不是徹底對應,由於native由於平臺的不一樣,有不一樣的命名規範。
看下CDVNotification的代碼:
1 2 3 4 5 6 7 8 9 |
- (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的所有過程了。
列下過程的核心代碼:
以上Dialog例子中,PhoneGap js調用native的時序圖:
PhoneGap仍是很給力的,能作到主流平臺全兼容着實不容易。
iOS端由於沒有提供js調用native的直接方法,作的處理也算合理到位。
特別是插件化的支持作的很好,可是文檔着實不夠給力。