如何理解苹果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-appcd my-appphonegap run ios
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代码的关键。
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的直接方法,做的处理也算合理到位。
特别是插件化的支持做的很好,但是文档着实不够给力。
转载于:https://www.cnblogs.com/lxf1016/p/4753938.html
如何理解苹果iOS版PhoneGap原理分析相关推荐
- 服务器同时登入会被挤下来吗_「英雄联盟手游」苹果iOS版LOL日服公测,日本服务器卡爆!...
前几天写了如何下载海外LOL的文章,但是今天下午去登录,发现无法登录,而且创建LOL账号入口也没有了.本来日服因服务器压力问题,对外推迟29号才能下载,但是下午群里面沸腾了,说能下载了. 沉默十年:英 ...
- App Inventor 2能编译出苹果iOS版App吗?
App Inventor 2 编译iOS app? 如题,首先可以明确地说目前并不支持,只支持iOS版AI伴侣进行测试,但是AI伴侣的版本更新一直都是落后于安卓版的,导致测试时会有一些不兼容或一些奇怪 ...
- android要比ios耗电,Aislelabs:苹果iOS版iBeacon比Android版更耗电
研究发现,Moto G的电池续航时间最佳.比较低标记信号强度上的Nexus 5和iPhone 5系列手机(两者使用类似的低功耗蓝牙芯片),Nexus的电池消耗水平要稍低一些.随着信号强度的增加,两者的 ...
- 微信跳一跳辅助之JAVA版(最容易理解的算法)实现原理分析
上几周更新微信后,进入欢迎界面就提示出让玩一把微信小游戏<跳一跳>.一向不爱玩游戏的我(除了经典QQ飞车.CS外),当时抱着没兴趣的态度简单看了下,没有玩.与朋友玩耍时,常听他们聊起这个小 ...
- iOS证书签名原理分析
在iOS真机调试和发布上线的时候,我们可能已经习惯了配置各种证书.描述文件,等这一繁琐的步骤.但是对于背后我们为什么要配置这些东西,以及其背后的原理之前一直没有做过分析研究,最近有空就简单的研究了一下 ...
- addonsmaker怎么制作_addons制作器 v1.14.3 苹果IOS版
基本简介 我的世界addons制作器是一款一键制作minecraftpe Addon的软件 我` 8 /的世界addons制作软件允许用户] j x v + G 7更改在游戏中的人物和物品offfic ...
- 必应词典手机版(IOS版)与有道词典(IOS版)之软件分析【功能篇】【用户体验篇】...
1.序言: 随着手机功能的不断更新和推广,手机应用市场的竞争变得愈发激烈.这次我们选择必应词典和有道词典的苹果客户端作对比,进一步分析这两款词典的客户端在功能和用户体验方面的利弊.这次测评的主要评测人 ...
- iOS CALayer动画原理分析
一.引出问题 在开始分析原理之前,我们先来看一个问题: 我们都知道 UIView与 CALayer之间的关系,通俗的来说,UIView内部封装了一个 CALayer, 其中 CALayer负责展示UI ...
- 苹果iOS版手机云便签无法同意用户协议正常使用怎么解决?
苹果手机用户在使用敬业签云便签的时候,下载软件打开后需要先同意<用户协议及隐私政策>后,才能登录账号正常使用便签的相关功能.部分苹果手机版敬业签用户手机无法同意协议,不显示同意按钮导致无法 ...
最新文章
- wxWidgets:wxScopedCharTypeBuffer< T >类模板用法
- BackgroundWorker
- NSArray文件读写
- JS中[感叹号]function(){}()的理解
- Java Collection类型的forEach方法
- springboot listener_Springboot 监听redis key的过期事件
- mysql运维机制_《MySQL运维内参》节选 | InnoDB日志管理机制(一)
- Tcpdump个人实战总结
- 我经历的IT公司面试及离职感受(转)
- 重载 重载递增递和减运算符
- 利用dropbox来Host你的silverlight应用
- 在线ppt转换pdf转换器
- 【TeeChart .NET教程】(六)使用系列
- 黑马程序员提供得教程
- TI 库仑计高级篇之Chemistry 创建
- 松柏先生: 做品牌自己挣钱不算什么, 能让1000名山区绣娘都挣钱才牛!
- EVO Evaluation of SLAM 5 --- ORB-SLAM3 精度和性能效果评估
- LeetCode-347. Top K Frequent Elements [C++][Java]
- Web前端-JavaScript
- ibm服务器报错代码大全_IBM x系列服务器报错代码
热门文章
- Linux中sort、uniq、cut、wc命令详解
- 安装win7和centos6.7双系统 引导加载安装位置问题
- Caused by: java.lang.ClassNotFoundException: org.objectweb.asm.ClassVisitor
- C/C++各种系统开发环境搭建
- Android 动画(一)
- Linux Ubuntu使用技巧
- 解决VS2010复制代码中文乱码的问题
- 设计模式—工厂模式之简单工厂模式
- Android中如何使控件保持固定宽高比
- 简单线性回归预测实现