PhoneGap原理分析

來源:互聯網
上載者:User

標籤: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原理分析

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.