iOS版PhoneGap原理分析

PhoneGap,著名的跨平台Hybrid框架,旨在让开发者使用HTML、Javascript、CSS开发跨平台的App。

最近的工作,就是做Hybrid方面的,很自然,方案就从PhoneGap入手。

下面就切入正题,分析下PhoneGap的原理,需要说明的是,我只针对iOS版本的PhoneGap做分析,android版本的原理大同小异。

安装PhoneGap

现在使用PhoneGap非常方便,只需要安装node,用简单的命令就能完成安装和使用的工作。

安装PhoneGap:

1
sudo npm install -g phonegap

创建phoneGap应用:

1
2
3
phonegap create my-app
cd my-app
phonegap 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协议的

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

方法来做拦截,并在这个方法中,根据url的协议或特征字符串来做调用方法或触发事件等工作,如

/*
* 方法的返回值是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非常简洁方便,只需要

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

并且该方法是同步的。

native调用js非常简单直接,所以PhoneGap解决的主要是js调用native的问题。

PhoneGap js –> native

我们通过一个js调用native的Dialog的例子做说明。

Dialog是一个PhoneGap的插件,可以看dialog 插件文档,学习下载并使用该插件。

这里有个很重要的事需要说明一下: 目前PhoneGap的文档更新非常不及时,特别是插件的使用方面,比如Dialog插件的使用,文档中写的是使用navigator.notification.alert,但是经过我的摸索,因为现在PhoneGap使用AMD的方式来管理插件,所以应该是使用cordova.require("cordova/plugin/notification").alert的方式来调用。 插件的合并方面,也有很多坑,主要是文档不全

js部分

在html上添加一个button,然后通过下列代码调用:

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的代码:

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源码非常长,我只能截取最关键的代码并做说明:

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:方法:

- (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

- (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方法紧密相连,这里一起列出,只保留关键代码:

- (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的代码:

- (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的直接方法,做的处理也算合理到位。

特别是插件化的支持做的很好,但是文档着实不够给力。

时间: 2024-10-10 20:16:00

iOS版PhoneGap原理分析的相关文章

PhoneGap原理分析

PhoneGap,著名的跨平台Hybrid框架,旨在让开发者使用HTML.Javascript.CSS开发跨平台的App. 最近的工作,就是做Hybrid方面的,很自然,方案就从PhoneGap入手. 下面就切入正题,分析下PhoneGap的原理,需要说明的是,我只针对iOS版本的PhoneGap做分析,android版本的原理大同小异. 安装PhoneGap 现在使用PhoneGap非常方便,只需要安装node,用简单的命令就能完成安装和使用的工作. 安装PhoneGap: 1 sudo np

iOS App Crash原理分析

预备知识:OS X系统分析 1.内核XNU是Darwin的核心,也是整个OS X的核心.XNU本身由以下几个组件构成: Mach微核心 BSD层 libKern I/O Kit 此外,内核是模块化的,允许根据需要动态加载插件形式的内核扩展. 2.Mach:XNU的核心,Mach仅能处理操作系统最基本的职责: 进程和线程抽象. 虚拟内存管理 任务调度 进程间通信和消息传递机制(例如:NSMachPort) 3.所以OS X是在Mach内核的基础上构建的,苹果不鼓励直接只用Mach的API,但是Ma

必应词典手机版(IOS版)与有道词典(IOS版)之软件分析【功能篇】

1.序言: 随着手机功能的不断更新和推广,手机应用市场的竞争变得愈发激烈.这次我们选择必应词典和有道词典的苹果客户端作对比,进一步分析这两款词典的客户端在功能和用户体验方面的利弊.这次测评的主要评测人是团队PM,另有其他同学给出建议. 2.软件分析与测评: 我们选择的是必应词典(version3.2.2 for ios)和有道词典(version 5.1.2 for ios) 2.1核心功能: 2.1.1词典功能: 众所周知词典的基本功能就是查词,在查词的基础上会给出相应的英文解释,例句以及用法

iOS版 PhoneGap 跳转网页问题

技术问题:系统自动检查更新,有新版本进行提示,并跳转到App Store,发现无法跳转到App Strore,但是可以打开百度等网页 安装插件:org.apache.cordova.inappbrowser 插件地址:http://plugins.cordova.io/#/package/org.apache.cordova.inappbrowser 安装插件方法:cordova plugin add org.apache.cordova.inappbrowser 使用方法: window.op

必应词典手机版(IOS版)与有道词典(IOS版)之问卷分析

我们制定了一个调查问卷: 1.年龄分布: 2.地域分布: 3.是否用过必应词典? 对于必应词典还是没用过的人数更多. 4.是否用过有道词典? 有道词典的使用率更高一点. 5.对于必应的基本功能给几分? 对于必应的基本功能的打分,大多数人给出了4分,说明用过的人对必应词典的基本功能设计还是满意的. 6.是否用过必应的附加功能? 用过必应的人对附加功能不是很关注,仍有30%左右的人没有用过. 7.对于必应的附加功能给几分? 对于附加功能大部分人比较满意. 8.给有道的基本功能几分? 因为用过有道的人

深入理解HTTP协议、HTTP协议原理分析

深入理解HTTP协议.HTTP协议原理分析 目录(?)[+] http协议学习系列 1. 基础概念篇 1.1 介绍 HTTP是Hyper Text Transfer Protocol(超文本传输协议)的缩写.它的发展是万维网协会(World Wide Web Consortium)和Internet工作小组IETF(Internet Engineering Task Force)合作的结果,(他们)最终发布了一系列的RFC,RFC 1945定义了HTTP/1.0版本.其中最著名的就是RFC 26

HTML5 移动应用开发环境搭建及原理分析

开发环境搭建: 一.Android 开发平台搭建 安装java jdk:\\10.194.151.132\Mewfile\tmp\ADT 配置java jdk 1)  新建系统变量,JAVA_HOME,C:\Program Files\Java\jdk1.8.0_25 2)  新建系统变量,classpath,;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar 3)  Path,%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin 4)  测试

iOS设计模式(代码分析系列2:简单工厂模式)

简单工厂模式示例代码下载地址, 1.简述 首先需要说明一下,简单工厂模式不属于23种GOF设计模式之一.它也称作静态工作方法模式,是工厂方法模式的特殊实现(也就是说工厂模式包含简单工厂模式).这里对简单工厂模式进行介绍,是为后面的工厂方法和抽象工厂模式做一个引子. 2.定义 "专门定义一个类来负责创建其他类的实例,被创建的实例通常具有共同的父类." 世界上就是由一个工厂类,根据传入的参数,动态地决定创建出哪一个产品类的实例. 3.结构图 简要分析结构图: ConcreteProduct

MySQL数据库InnoDB存储引擎多版本控制(MVCC)实现原理分析

文/何登成 导读:   来自网易研究院的MySQL内核技术研究人何登成,把MySQL数据库InnoDB存储引擎的多版本控制(简称:MVCC)实现原理,做了深入的研究与详细的文字图表分析,方便大家理解InnoDB存储引擎实现的多版本控制技术(简称:MVCC). 基本知识 假设对于多版本控制(MVCC)的基础知识,有所了解.MySQL数据库InnoDB存储引擎为了实现多版本的一致性读,采用的是基于回滚段的协议. 行结构 MySQL数据库InnoDB存储引擎表数据的组织方式为主键聚簇索引.由于采用索引