PhoneGap原理分析

 

PhoneGap,著名的跨平臺Hybrid框架,旨在讓開發者使用HTML、Javascript、CSS開發跨平臺的App。html

最近的工做,就是作Hybrid方面的,很天然,方案就從PhoneGap入手。node

下面就切入正題,分析下PhoneGap的原理,須要說明的是,我只針對iOS版本的PhoneGap作分析,android版本的原理大同小異。android

安裝PhoneGap

如今使用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

PhoneGap與Cordova的關係

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

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的協議或特徵字符串來作調用方法或觸發事件等工做,如

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

native調用js很是簡潔方便,只須要

1
[webView stringByEvaluatingJavaScriptFromString:@"alert('hello world!')"];

而且該方法是同步的。

native調用js很是簡單直接,因此PhoneGap解決的主要是js調用native的問題。

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的方式來調用。
插件的合併方面,也有不少坑,主要是文檔不全 - -|||

js部分

在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部分

以後,就走到了native代碼的部分。

CDVViewController

前面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方法中作的事。

CDVCommandQueue

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]);

故,這裏的對應關係:

  • obj:「Notification」
  • normalSelector:「alert」
  • command:[message, title, buttonLabel]

CDVNotification

「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再作解析,並顯示。

最終效果:

alert

點擊」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

結語

PhoneGap仍是很給力的,能作到主流平臺全兼容着實不容易。

iOS端由於沒有提供js調用native的直接方法,作的處理也算合理到位。

特別是插件化的支持作的很好,可是文檔着實不夠給力。

相關文章
相關標籤/搜索