iOS版PhoneGap原理分析

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

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

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

安装PhoneGap

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

安装PhoneGap:

sudo npm install -g phonegap

创建phoneGap应用:

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容